{"type":"title","v":1,"title":"","updatedAt":"2026-07-17T06:57:30.306Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f6edd-b0c5-7001-8cfd-2a8165dbf235","timestamp":"2026-07-17T06:57:30.306Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"268bf9fe","parentId":null,"timestamp":"2026-07-17T06:57:30.356Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"3d757be0","parentId":"268bf9fe","timestamp":"2026-07-17T06:57:30.357Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"1df476ba","parentId":"3d757be0","timestamp":"2026-07-17T06:57:30.359Z","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: **Brain Performance 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\nIdentify why deployed spt v0.36 broker PID 72488 and brain PID 58272 sustain CPU and extreme I/O with 4-5 hosted sessions.\n# Constraints\nRead-only investigation. Do not edit. Skip formatters, linters, and tests. Evidence: broker 532-541 threads but only 4 CPU-active over 5s; brain 18-24 threads with 5 active. 20s means: brain 15.5% one-core CPU and 63.91 MiB/s reads; broker 11.4% one-core CPU and 3.74 MiB/s writes. daemon log grows only ~563 B/s. Current release is v0.36.0 after REDISPATCH fixes.\n# Contract\nReturn exact source paths/lines, loop semantics, and falsifiable probes that distinguish normal activity from leaks/spins.\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 `BrainCpuRca`. Currently visible peers:\n- `Main` — main (main, running)\n- `RefreshLifecycle` — Daemon Lifecycle Investigator (sub, parked)\n- `PtyControl` — PTY Transport Investigator (sub, parked)\n- `ReproHarness` — Integration Test Investigator (sub, parked)\n- `HyphenMessage` — CLI Parsing Investigator (sub, parked)\n- `ElevatedRc` — Remote Control Investigator (sub, parked)\n- `ElevatedMessaging` — Messaging Lifecycle Investigator (sub, parked)\n- `RcSplitBrain` — Registry Session Investigator (sub, parked)\n- `ElevatedHosted` — Windows Token Investigator (sub, parked)\n- `ShortformDifferential` — Delivery Differential Investigator (sub, parked)\n- `AdapterFreshSemantics` — Claude Adapter Investigator (sub, parked)\n- `PerriDurableState` — State Forensics Investigator (sub, parked)\n- `CoreFreshSemantics` — Core Lifecycle Investigator (sub, parked)\n- `RestStateRca` — Endpoint Lifecycle Investigator (sub, parked)\n- `CreateNoopRca` — CLI Semantics Investigator (sub, parked)\n- `ControlledStampRca` — Broker Control-State Investigator (sub, parked)\n- `EmphRestartRca` — Daemon Restart Investigator (sub, parked)\n- `HarnessExitRca` — Harness Lifecycle Investigator (sub, parked)\n- `V034DiffRca` — Regression Archaeologist (sub, parked)\n- `V034ReleaseRca` — Release Contract Investigator (sub, parked)\n- `LiveLogRca` — Runtime Telemetry Investigator (sub, parked)\n- `PtyPathRca` — PTY Concurrency Investigator (sub, parked)\n- `BrokerCpuRca` — Broker Concurrency Investigator (sub, running)\n- `RedispatchCpuRca` — Release Regression 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\nBrain loops in crates/spt-daemon including project index, registry, dispatch, stream subscribers, and state scans. Non-goal: broker PTY ownership.\n# Change\nFind loops capable of reading ~64 MiB/s or polling at sub-second cadence. Check whether any loop repeatedly scans repository/transcript/log files under active sessions.\n# Acceptance\nReturn ranked mechanisms with exact cadence, files read, citations, and live discriminating probes.","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":"785ad3b3","parentId":"1df476ba","timestamp":"2026-07-17T06:57:30.365Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\nBrain loops in crates/spt-daemon including project index, registry, dispatch, stream subscribers, and state scans. Non-goal: broker PTY ownership.\n# Change\nFind loops capable of reading ~64 MiB/s or polling at sub-second cadence. Check whether any loop repeatedly scans repository/transcript/log files under active sessions.\n# Acceptance\nReturn ranked mechanisms with exact cadence, files read, citations, and live discriminating probes."}],"attribution":"agent","timestamp":1784271450360}}
{"type":"message","id":"ed9438b9","parentId":"785ad3b3","timestamp":"2026-07-17T06:57:33.484Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning read-only investigation**"},{"type":"toolCall","id":"call_CbGmjEDddg6oHGCrT6900Ajj|fc_038f28d3e93bbfd4016a59d25d139c81968195e82f69342218","name":"read","arguments":{"i":"Reading diagnosis workflow","path":"skill://diagnose","selector":"1-220"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14906,"output":65,"cacheRead":0,"cacheWrite":0,"totalTokens":14971,"reasoningTokens":31,"cost":{"input":0.07453000000000001,"output":0.0019500000000000001,"cacheRead":0,"cacheWrite":0,"total":0.07648}},"stopReason":"toolUse","timestamp":1784271450388,"responseId":"resp_038f28d3e93bbfd4016a59d25af1bc81968b964478ae844004","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d25bd0b88196953174d6987e8ff7","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdJd8wZC6KJ1FUYtvIzw9jcrK8IaQ8oFWP0dWPxlglXGTk15E7QLJ3_OErchWOylohAqnSQtlKvwMhErNdHRUtvrpt-tu6sdge-t4ePHqSRoZ9-Jrs5Ye-6oZqyhWxkX9RhqYXZtqKmSgeNdzCrnhBGCSwvmAj0-jxCat1MFY1Rs_-ER3mfo14oGHSDS1FrHlelUUh795QH6E63sOLlxQ-PZQS5h6YcTFKRPeTRXOeIGjMhW8N6YH3nDfqAiksmtnTZAEG0SCS_ZQpun-siTGSMt3XLf3HnFa4ghejvFnFzRL1fs94wCjI2plUzXQfw54suRd65oQkL94LAMXaexSa8_5LwuqwzvbjftU4tWkZDbqtWAe7vuePvXqMSsLVJ_cHXQKEKPUiBPmi7QMGmOkD6PG0kXRpBsGucmW4zjLiRjv0v8uF6i1GUds8vUnk-Af37UBjpsoz_AlmHTr4JVZ4ACeYBLqd5qWQFFWFwi9BNinNx6X0xteGobP_b8gdNkQhUZf-ElJIcvRMh65WwTXTclDHTaUEbuvK2u959CLm9nCaBKC-iRlI9RF2rXWMrhAiToHW8CGRwPNUKtjz1_7Oa3bti7l1HnYzP6gKM4BuZj5CP48VShmbb0wApVtnnHlupXhY9G-MbsQJyL9D0zjCBBRU2ErVsUC4TJGxdcGT4yTmkbBHedT8GzM6JB7_x6fSKHMsPMQbJjzlg5t2Qcd-QukwZBgoW_EukWQ_E3m2CnkXxbz3acHXEB3q5F9Sz2Rf4OgAmNYc5ESeh1NUItliWF0vSKxCWIPkJgTlF8SAZCdhF-LsdRykkTW3gDa__n7dQ3-yZK9fc7ANBkdnayHe9viQ8nsCf5rOdvLyDN0yBlrYz4aBXDOiCaVkIAEx8o6jXFfPwMRoYmULAMDyBjHUKrpxGnBPWDJOvboIZTPhcwmZNpAib9HzcsG-rwEovJf_uvuEif_cTzeQXCt4xGQAEHqRuxnGHAPzkCQ2YxrRfTkd_PaAd3f8BGV7EYtfWWCx7ovWXrhjHBBe5ELr5gfMDDPC0zgb7ZSciKlggYcDvT0TZY8Yqr2OqgeOUFHgukmmOYT0C3RQ3fghuD7kJ64v8HcA==","summary":[{"type":"summary_text","text":"**Planning read-only investigation**"}]},{"id":"fc_038f28d3e93bbfd4016a59d25d139c81968195e82f69342218","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading diagnosis workflow\",\"path\":\"skill://diagnose\",\"selector\":\"1-220\"}","call_id":"call_CbGmjEDddg6oHGCrT6900Ajj","name":"read"}]},"duration":3087.027199998498,"ttft":1452.69810000062,"contextSnapshot":{"promptTokens":14906,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CbGmjEDddg6oHGCrT6900Ajj|fc_038f28d3e93bbfd4016a59d25d139c81968195e82f69342218","toolName":"read","startedAt":"2026-07-17T06:57:33.484Z","args":{"path":"skill://diagnose"},"intent":"Reading diagnosis workflow"},"id":"892d9619","parentId":"ed9438b9","timestamp":"2026-07-17T06:57:33.484Z"}
{"type":"message","id":"6a772c36","parentId":"892d9619","timestamp":"2026-07-17T06:57:33.485Z","message":{"role":"toolResult","toolCallId":"call_CbGmjEDddg6oHGCrT6900Ajj|fc_038f28d3e93bbfd4016a59d25d139c81968195e82f69342218","toolName":"read","content":[{"type":"text","text":"---\nname: diagnose\ndescription: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.\n---\n\n# Diagnose\n\nA discipline for hard bugs. Skip phases only when explicitly justified.\n\nWhen exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.\n\n## Phase 1 — Build a feedback loop\n\n**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.\n\nSpend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**\n\n### Ways to construct one — try them in roughly this order\n\n1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.\n2. **Curl / HTTP script** against a running dev server.\n3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.\n4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.\n5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.\n6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.\n7. **Property / fuzz loop.** If the bug is \"sometimes wrong output\", run 1000 random inputs and look for the failure mode.\n8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate \"boot at state X, check, repeat\" so you can `git bisect run` it.\n9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.\n10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.\n\nBuild the right feedback loop, and the bug is 90% fixed.\n\n### Iterate on the loop itself\n\nTreat the loop as a product. Once you have _a_ loop, ask:\n\n- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)\n- Can I make the signal sharper? (Assert on the specific symptom, not \"didn't crash\".)\n- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)\n\nA 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.\n\n### Non-deterministic bugs\n\nThe goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.\n\n### When you genuinely cannot build a loop\n\nStop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.\n\nDo not proceed to Phase 2 until you have a loop you believe in.\n\n## Phase 2 — Reproduce\n\nRun the loop. Watch the bug appear.\n\nConfirm:\n\n- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.\n- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).\n- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.\n\nDo not proceed until you reproduce the bug.\n\n## Phase 3 — Hypothesise\n\nGenerate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.\n\nEach hypothesis must be **falsifiable**: state the prediction it makes.\n\n> Format: \"If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse.\"\n\nIf you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.\n\n**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly (\"we just deployed a change to #3\"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.\n\n## Phase 4 — Instrument\n\nEach probe must map to a specific prediction from Phase 3. **Change one variable at a time.**\n\nTool preference:\n\n1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.\n2. **Targeted logs** at the boundaries that distinguish hypotheses.\n3. Never \"log everything and grep\".\n\n**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.\n\n**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.\n\n## Phase 5 — Fix + regression test\n\nWrite the regression test **before the fix** — but only if there is a **correct seam** for it.\n\nA correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.\n\n**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.\n\nIf a correct seam exists:\n\n1. Turn the minimised repro into a failing test at that seam.\n2. Watch it fail.\n3. Apply the fix.\n4. Watch it pass.\n5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.\n\n## Phase 6 — Cleanup + post-mortem\n\nRequired before declaring done:\n\n- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)\n- [ ] Regression test passes (or absence of seam is documented)\n- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)\n- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)\n- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns\n\n**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.\n"}],"details":{"resolvedPath":"C:\\Users\\decid\\.claude\\skills\\diagnose\\SKILL.md","contentType":"text/markdown","displayContent":{"text":"---\r\nname: diagnose\r\ndescription: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.\r\n---\r\n\r\n# Diagnose\r\n\r\nA discipline for hard bugs. Skip phases only when explicitly justified.\r\n\r\nWhen exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.\r\n\r\n## Phase 1 — Build a feedback loop\r\n\r\n**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.\r\n\r\nSpend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**\r\n\r\n### Ways to construct one — try them in roughly this order\r\n\r\n1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.\r\n2. **Curl / HTTP script** against a running dev server.\r\n3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.\r\n4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.\r\n5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.\r\n6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.\r\n7. **Property / fuzz loop.** If the bug is \"sometimes wrong output\", run 1000 random inputs and look for the failure mode.\r\n8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate \"boot at state X, check, repeat\" so you can `git bisect run` it.\r\n9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.\r\n10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.\r\n\r\nBuild the right feedback loop, and the bug is 90% fixed.\r\n\r\n### Iterate on the loop itself\r\n\r\nTreat the loop as a product. Once you have _a_ loop, ask:\r\n\r\n- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)\r\n- Can I make the signal sharper? (Assert on the specific symptom, not \"didn't crash\".)\r\n- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)\r\n\r\nA 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.\r\n\r\n### Non-deterministic bugs\r\n\r\nThe goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.\r\n\r\n### When you genuinely cannot build a loop\r\n\r\nStop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.\r\n\r\nDo not proceed to Phase 2 until you have a loop you believe in.\r\n\r\n## Phase 2 — Reproduce\r\n\r\nRun the loop. Watch the bug appear.\r\n\r\nConfirm:\r\n\r\n- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.\r\n- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).\r\n- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.\r\n\r\nDo not proceed until you reproduce the bug.\r\n\r\n## Phase 3 — Hypothesise\r\n\r\nGenerate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.\r\n\r\nEach hypothesis must be **falsifiable**: state the prediction it makes.\r\n\r\n> Format: \"If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse.\"\r\n\r\nIf you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.\r\n\r\n**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly (\"we just deployed a change to #3\"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.\r\n\r\n## Phase 4 — Instrument\r\n\r\nEach probe must map to a specific prediction from Phase 3. **Change one variable at a time.**\r\n\r\nTool preference:\r\n\r\n1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.\r\n2. **Targeted logs** at the boundaries that distinguish hypotheses.\r\n3. Never \"log everything and grep\".\r\n\r\n**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.\r\n\r\n**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.\r\n\r\n## Phase 5 — Fix + regression test\r\n\r\nWrite the regression test **before the fix** — but only if there is a **correct seam** for it.\r\n\r\nA correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.\r\n\r\n**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.\r\n\r\nIf a correct seam exists:\r\n\r\n1. Turn the minimised repro into a failing test at that seam.\r\n2. Watch it fail.\r\n3. Apply the fix.\r\n4. Watch it pass.\r\n5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.\r\n\r\n## Phase 6 — Cleanup + post-mortem\r\n\r\nRequired before declaring done:\r\n\r\n- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)\r\n- [ ] Regression test passes (or absence of seam is documented)\r\n- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)\r\n- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)\r\n- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns\r\n\r\n**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118]},"meta":{"source":{"type":"internal","value":"skill://diagnose"}}},"isError":false,"timestamp":1784271453484}}
{"type":"message","id":"0e454542","parentId":"6a772c36","timestamp":"2026-07-17T06:57:42.318Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Locating crates and daemon code**"},{"type":"toolCall","id":"call_BaTu47wPefe6kljgRuqT9aLm|fc_038f28d3e93bbfd4016a59d266415c8196a9f6d58792227fff","name":"glob","arguments":{"i":"Mapping daemon sources","path":"crates/spt-daemon/**/*.{rs,toml}","hidden":true,"gitignore":true,"limit":500}},{"type":"toolCall","id":"call_rPv8MsUYGn52opcykJ8Yg1Ps|fc_038f28d3e93bbfd4016a59d266417881969dd637c898e6c264","name":"grep","arguments":{"i":"Finding polling loops","pattern":"\\b(loop|while|interval|sleep|tick|poll|scan|refresh)\\b","path":"crates/spt-daemon","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Mvb47KlTjKvnWPmoc4wD5YER|fc_038f28d3e93bbfd4016a59d26641808196a95b8eceecd712d9","name":"grep","arguments":{"i":"Finding filesystem reads","pattern":"read_dir|read_to_string|File::open|OpenOptions|BufReader|metadata\\(|canonicalize|walkdir|WalkDir|fs::read|tokio::fs|std::fs","path":"crates/spt-daemon","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_5lWxaNTh64JxxrX8MU4mulf6|fc_038f28d3e93bbfd4016a59d26641888196895416dd5a9ee175","name":"grep","arguments":{"i":"Finding task spawns","pattern":"tokio::spawn|spawn_blocking|thread::spawn|JoinSet|FuturesUnordered|watch::|broadcast::|mpsc::|Notify","path":"crates/spt-daemon","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_nWDSOEEC4gHGvGwnBEjBFTfx|fc_038f28d3e93bbfd4016a59d26641908196b3153b54559a2350","name":"grep","arguments":{"i":"Finding project registry dispatch","pattern":"project|registry|dispatch|subscriber|stream|state|transcript|log","path":"crates/spt-daemon/src","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2800,"output":413,"cacheRead":13824,"cacheWrite":0,"totalTokens":17037,"reasoningTokens":55,"cost":{"input":0.014,"output":0.01239,"cacheRead":0.006912,"cacheWrite":0,"total":0.033302}},"stopReason":"toolUse","timestamp":1784271453500,"responseId":"resp_038f28d3e93bbfd4016a59d25dcf3c81968c5dfd9d7ec29347","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d25ecbdc819691d9c28f00134a17","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdJmztmzmZpd0dTzfJb7aTNAi59rPrI0wF4TJuUWWtxkX9SqRqUAGshYz9V466t7pygMKWLFV-LkMKPSGlWK3110uMldFTnKUV9rGZoFFFwb3bu73u0_IljSi_fTlvT8NfHL5Pt458jByT0Yug26_Vq4E8HMfY8dlkaECmJQYLpgGUSkjRsQWXrh5F3193CA8LmY3zOddKiJP1HJFucMX04s7hViZNeBCq0FV-uzc0q9lpdf1fIuod7kw6AwqyFecLfeQzAwtct11T-qKPXzeqpjAjJEYN0ywRRqP1yPbZzzYs22IFPQXgtd6fX-_KCPpGh0LnHH-C1RbcDXfy7Swvy23vwyBKYepXIeAu_8-BWQRL_2L510BFUSdHqn_js5vwS9ZAuoQAsVrYSOr3Q74Bu4OQalXFU8b4HwBKS5tQ0eXFhFfR-NB8fBwmpX3PPpyVs2xnZoEWBvV9ARqCGNdx-W07Cbq5xa4XDJXwaoWa89P2wHSA1epbwtdR_eR_usjuU_q3ZsIXSe6FqBlWAC2Of6s_buFNvrmgR_O2y1TmSAfccfg0jCr_5fSKY4eLezhTEfZMK_oRFznFDxWBuEDhQcIuTRGKy_4l4_aUZeIhq_fAeOu_bb4KeEWsARaclyDCt8VPd2vAleMvDgBPo2mbfG-xVd-LL_ZmoW6YZJksI2ET3kKSEdvci7bdvnry8O5gz2l9g0dvxdWZGNc7aHWkYPHpCBy2sICnXW9IGyZRzQHbLuzQXp9zvfUThK11vZYG3-3Uohqi3WfRjoRrsQbqb5Hak7VNpHldnTD61X9BmhC4Panfi_rzKd2tqSGRFZpJ_yNG0F0VBTo3XcMHNUNvj8KOFe7lNX0gSpdEhlVT5H98qQB9vsCWjSBri7MQn-Rv8gnsoAxMlyM7lpc_EPjaofgwtU1Lu9i74r3kLdbmaYWr800kX9YowcjoBf-yGmhvK8wFDSFqyrPvE-jDGJD4IHmyvt4yia6u5KBfZFCdkzHOTG_xmzvU4QdL4ws3KXXj5pchfEOW0XRSEbKG-qDU0wFWui_eBQaiDf7h7LeijC59zar8wncoIH3qAJk2lyXEeuAsQBfDQn5trBUGLSrpoKtAWUN-CaiKFxv3GgIl-OeVpH79TXcjxmUxYAgQbu8VAkx4mDJndm_67KhdkuRlt-bTb3KPMdFTEo9Dmkks0XK8VFkNr08as9g30vj3iWd13Wal7sfdWzN4oanVpV7YejixVCbIKXhJUrTTdMMOVu7y8iHdiAjVqQI17RUB8rzdol","summary":[{"type":"summary_text","text":"**Locating crates and daemon code**"}]},{"id":"fc_038f28d3e93bbfd4016a59d266415c8196a9f6d58792227fff","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping daemon sources\",\"path\":\"crates/spt-daemon/**/*.{rs,toml}\",\"hidden\":true,\"gitignore\":true,\"limit\":500}","call_id":"call_BaTu47wPefe6kljgRuqT9aLm","name":"glob"},{"id":"fc_038f28d3e93bbfd4016a59d266417881969dd637c898e6c264","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding polling loops\",\"pattern\":\"\\\\b(loop|while|interval|sleep|tick|poll|scan|refresh)\\\\b\",\"path\":\"crates/spt-daemon\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_rPv8MsUYGn52opcykJ8Yg1Ps","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d26641808196a95b8eceecd712d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding filesystem reads\",\"pattern\":\"read_dir|read_to_string|File::open|OpenOptions|BufReader|metadata\\\\(|canonicalize|walkdir|WalkDir|fs::read|tokio::fs|std::fs\",\"path\":\"crates/spt-daemon\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Mvb47KlTjKvnWPmoc4wD5YER","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d26641888196895416dd5a9ee175","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding task spawns\",\"pattern\":\"tokio::spawn|spawn_blocking|thread::spawn|JoinSet|FuturesUnordered|watch::|broadcast::|mpsc::|Notify\",\"path\":\"crates/spt-daemon\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_5lWxaNTh64JxxrX8MU4mulf6","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d26641908196b3153b54559a2350","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding project registry dispatch\",\"pattern\":\"project|registry|dispatch|subscriber|stream|state|transcript|log\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_nWDSOEEC4gHGvGwnBEjBFTfx","name":"grep"}]},"duration":8817.882799997926,"ttft":1316.0058000087738,"contextSnapshot":{"promptTokens":16624,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BaTu47wPefe6kljgRuqT9aLm|fc_038f28d3e93bbfd4016a59d266415c8196a9f6d58792227fff","toolName":"glob","startedAt":"2026-07-17T06:57:42.318Z","args":{"path":"crates/spt-daemon/**/*.{rs,toml}"},"intent":"Mapping daemon sources"},"id":"dd39ee8d","parentId":"0e454542","timestamp":"2026-07-17T06:57:42.318Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rPv8MsUYGn52opcykJ8Yg1Ps|fc_038f28d3e93bbfd4016a59d266417881969dd637c898e6c264","toolName":"grep","startedAt":"2026-07-17T06:57:42.318Z","args":{"path":"crates/spt-daemon"},"intent":"Finding polling loops"},"id":"7bb6d925","parentId":"dd39ee8d","timestamp":"2026-07-17T06:57:42.318Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Mvb47KlTjKvnWPmoc4wD5YER|fc_038f28d3e93bbfd4016a59d26641808196a95b8eceecd712d9","toolName":"grep","startedAt":"2026-07-17T06:57:42.319Z","args":{"path":"crates/spt-daemon"},"intent":"Finding filesystem reads"},"id":"cb25487f","parentId":"7bb6d925","timestamp":"2026-07-17T06:57:42.319Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5lWxaNTh64JxxrX8MU4mulf6|fc_038f28d3e93bbfd4016a59d26641888196895416dd5a9ee175","toolName":"grep","startedAt":"2026-07-17T06:57:42.319Z","args":{"path":"crates/spt-daemon"},"intent":"Finding task spawns"},"id":"f076a6f0","parentId":"cb25487f","timestamp":"2026-07-17T06:57:42.319Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nWDSOEEC4gHGvGwnBEjBFTfx|fc_038f28d3e93bbfd4016a59d26641908196b3153b54559a2350","toolName":"grep","startedAt":"2026-07-17T06:57:42.319Z","args":{"path":"crates/spt-daemon/src"},"intent":"Finding project registry dispatch"},"id":"786300d4","parentId":"f076a6f0","timestamp":"2026-07-17T06:57:42.319Z"}
{"type":"message","id":"51199f9d","parentId":"786300d4","timestamp":"2026-07-17T06:57:42.336Z","message":{"role":"toolResult","toolCallId":"call_BaTu47wPefe6kljgRuqT9aLm|fc_038f28d3e93bbfd4016a59d266415c8196a9f6d58792227fff","toolName":"glob","content":[{"type":"text","text":"# crates/spt-daemon/\nCargo.toml\n## src/\nnethost.rs\ndispatch.rs\nbroker.rs\nattach.rs\nbrain.rs\nmsg.rs\nseedproofx.rs\nregistryhost.rs\nlib.rs\nendpoint.rs\nsync.rs\nprojwriter.rs\nbrainproc.rs\napplyhost.rs\ndocshost.rs\ndaemon.rs\nconfig.rs\nupdate.rs\nrelcache.rs\nrelease.rs\nnotif.rs\nrelay.rs\npropagate.rs\nlivehost.rs\ninject.rs\nconn.rs\nautostart.rs\nstderrlog.rs\nlifecycle.rs\ndigest.rs\npairhost.rs\nxfer.rs\nshellwake.rs\nshellchan.rs\nseedmap.rs\nresting.rs\nresthost.rs\nlinkhost.rs\neffect.rs\nadapter_update.rs\nwan.rs\ntunnelhub.rs\ntransport.rs\ntranslation.rs\ntest_home.rs\nshellhost.rs\nshelldisc.rs\nservice.rs\nserveprobe.rs\nrollback_compat.rs\nreconcile.rs\nreap.rs\npsyrelay.rs\npresence.rs\nnotifsync.rs\nmachineid.rs\nharnesshost.rs\ngrants.rs\nframe.rs\ndrivehub.rs\ndigesthub.rs\ndeelevate.rs\ndeadline.rs\ncrc_swap.rs\nconsent.rs\ncodec.rs\naccess.rs\n### pump/\nmod.rs\nhealth.rs\nupdate.rs\nsync.rs\nregistry.rs\nnotif.rs\n### bin/\nxlate_choreo_fixture.rs\n## tests/\ninject_control_wedge.rs\nattach.rs\nredispatch_stall.rs\nfalse_promote.rs\nconn_blackhole_lifecycle.rs\nbroker.rs\nbrain_decouple.rs\nmesh_recovery.rs\nredispatch.rs\ninput_ack_deadlock.rs\ndocs_server_e2e.rs\npropagate.rs\npumpdeadline.rs\npump.rs\nnet_worker_starve.rs\nbrain_resume_conn_deadlock.rs\ntwo_origin_spanning.rs\nwake_single_flight.rs\nendpoint_survival.rs\nxfer.rs\nwanmsg.rs\ntwohost.rs\nreplicate.rs\nsync.rs\nnotifsync.rs\nnetstream.rs\nnetbroker.rs\nidempotent.rs\ndispatch.rs\ndaemon_e2e.rs\naccess.rs\nshellchan.rs\nseedproofx.rs\nrosterprop.rs\nresume.rs\nreseed.rs\npsyche_residency_expectation_e2e.rs\npsyche_event_turn_e2e.rs\npsyche_context_file_e2e.rs\npresence.rs\npairjoin.rs\nmesh.rs\nlegacy_resident_sweep_e2e.rs\nhandoff.rs\ndriven_by_selfheal.rs\ndigest.rs\ndaemon_lifecycle_real_brain.rs\ncontrol_stamp_lifetime.rs\nbudget.rs\nbrain_swap.rs\n### fixtures/\ndispatch_fixture.rs"}],"details":{"scopePath":"crates/spt-daemon","fileCount":126,"files":["crates/spt-daemon/src/nethost.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/tests/redispatch_stall.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/tests/mesh_recovery.rs","crates/spt-daemon/src/seedproofx.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/pump/health.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/tests/redispatch.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/fixtures/dispatch_fixture.rs","crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/sync.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/applyhost.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-daemon/tests/propagate.rs","crates/spt-daemon/src/update.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/release.rs","crates/spt-daemon/src/notif.rs","crates/spt-daemon/tests/pumpdeadline.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/net_worker_starve.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/src/relay.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/autostart.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-daemon/src/pairhost.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/idempotent.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/daemon_e2e.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/linkhost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/adapter_update.rs","crates/spt-daemon/tests/shellchan.rs","crates/spt-daemon/tests/seedproofx.rs","crates/spt-daemon/tests/rosterprop.rs","crates/spt-daemon/tests/resume.rs","crates/spt-daemon/tests/reseed.rs","crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","crates/spt-daemon/tests/psyche_event_turn_e2e.rs","crates/spt-daemon/tests/psyche_context_file_e2e.rs","crates/spt-daemon/tests/presence.rs","crates/spt-daemon/tests/pairjoin.rs","crates/spt-daemon/tests/mesh.rs","crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","crates/spt-daemon/tests/handoff.rs","crates/spt-daemon/tests/driven_by_selfheal.rs","crates/spt-daemon/tests/digest.rs","crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs","crates/spt-daemon/tests/control_stamp_lifetime.rs","crates/spt-daemon/tests/budget.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-daemon/src/wan.rs","crates/spt-daemon/src/tunnelhub.rs","crates/spt-daemon/src/transport.rs","crates/spt-daemon/src/translation.rs","crates/spt-daemon/src/test_home.rs","crates/spt-daemon/src/shellhost.rs","crates/spt-daemon/src/shelldisc.rs","crates/spt-daemon/src/service.rs","crates/spt-daemon/src/serveprobe.rs","crates/spt-daemon/src/rollback_compat.rs","crates/spt-daemon/src/reconcile.rs","crates/spt-daemon/src/reap.rs","crates/spt-daemon/src/pump/update.rs","crates/spt-daemon/src/pump/sync.rs","crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/pump/notif.rs","crates/spt-daemon/src/psyrelay.rs","crates/spt-daemon/src/presence.rs","crates/spt-daemon/src/notifsync.rs","crates/spt-daemon/src/machineid.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/grants.rs","crates/spt-daemon/src/frame.rs","crates/spt-daemon/src/drivehub.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/consent.rs","crates/spt-daemon/src/codec.rs","crates/spt-daemon/src/access.rs"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784271462336}}
{"type":"message","id":"b491a5ee","parentId":"51199f9d","timestamp":"2026-07-17T06:57:42.352Z","message":{"role":"toolResult","toolCallId":"call_nWDSOEEC4gHGvGwnBEjBFTfx|fc_038f28d3e93bbfd4016a59d26641908196b3153b54559a2350","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## access.rs\n 4|//! order: subnet *visibility* (is the endpoint routable here at all? —\n*5|//! `spt_store::visibility`, applied inside registry resolution) → the\n 6|//! **endpoint access whitelist** (may *your node* remotely reach it? — THIS\n 7|//! module) → *capability grants* (may this agent do this high-risk thing? —\n 8|//! [`crate::consent`], default-deny). The whitelist is the outer gate before\n...\n 10|//!\n*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12|//!\n 13|//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14|//!   *originates from* (where the operator is acting) — never the sender\n 15|//!   endpoint's identity or home. The subject value MUST be the QUIC\n*16|//!   handshake-proven remote node id from the broker's conn/stream table,\n 17|//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).\n 18|//! - **Outbound** → any visible node: the whitelist never restricts who an\n 19|//!   endpoint talks *to*.\n...\n 22|//!   (\"established/related\").\n*23|//!   Classification reads the durable [`RecentOutbound`] log\n 24|//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].\n 25|//! - **Unsolicited/direct inbound** → only from a whitelisted node.\n 26|//! - **Same-node operation is always allowed** — the gate sits on the\n...\n 35|//! an `spt access` edit takes effect on the next inbound without a daemon\n*36|//! restart, and the checks run at message/stream-establishment rate where a\n 37|//! small-file read is noise.\n 38|//!\n 39|//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies\n...\n 47|\n*48|/// How an inbound interaction reached this node, for the stateful-firewall\n 49|/// split.\n 50|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 51|pub enum InboundClass {\n...\n 112|/// Classify an inbound *message* for the gate: [`InboundClass::Reply`] iff the\n*113|/// target endpoint's own recent outbound (the durable [`RecentOutbound`] log)\n 114|/// names the origin node within the reply window — else the conservative\n 115|/// [`InboundClass::Unsolicited`].\n 116|// [impl->REQ-SEC-1]\n...\n 126|/// [`classify_inbound`] later correlates a reply against. Call on every\n*127|/// outbound WAN send (the sending leg's side of the stateful firewall).\n 128|/// Best-effort: a failed write only costs a future reply its exemption (it\n 129|/// will classify unsolicited — the conservative direction).\n 130|// [impl->REQ-SEC-1]\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 136|\n 137|#[cfg(test)]\n...\n 193|\n*194|    // [unit->REQ-SEC-1] the stateful-firewall exemption: a Reply classifies\n 195|    // past a whitelist that would refuse the same node unsolicited — and\n 196|    // classification itself turns on the recorded outbound within the window.\n 197|    #[test]\n## adapter_update.rs\n 7|//! its manifest `[update]` block ([`Update`]); this module turns that directive\n*8|//! into a per-adapter [`AdapterUpdateOutcome`], dispatched by avenue:\n 9|//!\n 10|//! - **`file_pull`** — spt-core controls the bytes, so it *verifies* them: the\n 11|//!   pulled payload is checked against the adapter's **own** Ed25519 key (declared\n...\n 163|/// is complete here. Soft-deregistered adapters are excluded by\n*164|/// [`spt_runtime::registry::registered`].\n 165|// [impl->REQ-INSTALL-4]\n 166|pub fn ripple_registered(adapters_dir: &std::path::Path) -> Vec<(String, AdapterUpdateOutcome)> {\n*167|    let mut set = spt_runtime::registry::registered(adapters_dir);\n 168|    set.sort_by_key(|(r, _)| r.registered_at_ms);\n 169|    set.iter()\n 170|        .map(|(r, m)| {\n...\n 408|        // Register out of name order to prove registration-time ordering.\n*409|        spt_runtime::registry::register(&adapters_dir, &updatable, 100).unwrap();\n*410|        spt_runtime::registry::register(&adapters_dir, &plain, 200).unwrap();\n*411|        spt_runtime::registry::register(&adapters_dir, &gone, 300).unwrap();\n*412|        spt_runtime::registry::deregister(&adapters_dir, \"gone\").unwrap();\n 413|\n 414|        let out = ripple_registered(&adapters_dir);\n 415|        assert_eq!(\n## applyhost.rs\n 34|//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n*35|//!    subscriber while the live brain kept running the OLD code.\n 36|//!\n 37|//! New code runs **immediately** — the supervised respawn lands on the new\n*38|//! executable, no manual bounce, no logon. Readiness-gated promotion +\n 39|//! auto-rollback on a brain that fails to come up is D6.\n 40|// [impl->REQ-UPD-3]\n 41|// [impl->REQ-UPD-4]\n...\n 140|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*141|    let applied_state = cache.applied_state();\n 142|    if let Some(AppliedRecord::RolledBack {\n 143|        quarantine_version, ..\n*144|    }) = &applied_state\n 145|    {\n 146|        if *quarantine_version == version {\n 147|            return Ok(Prepared::Done(ApplyStagedOutcome::Quarantined { version }));\n...\n 155|    // to a friendly exit-0 no-op instead. Checked before verify/connect: an\n*156|    // already-applied node is up to date regardless of the staged bytes' state.\n 157|    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n*158|    if staged_already_applied(applied_state.as_ref(), cache.applied_version(), version) {\n 159|        return Ok(Prepared::Done(ApplyStagedOutcome::AlreadyApplied {\n 160|            version,\n 161|            product_version,\n...\n 279|    // [impl->REQ-UPD-6]\n*280|    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n 281|        version,\n 282|        rollback_binary: aside.to_string_lossy().into_owned(),\n 283|        candidate_started_ms: now_ms(),\n...\n 331|    // level. This replaces the old in-process `apply_brain_only` handoff (the\n*332|    // regression: it re-attached a CLI-side subscriber while the live brain kept\n 333|    // running the OLD code). `classify` already gated this as brain-only, so the\n 334|    // broker never moves; only the brain restarts.\n 335|    //\n...\n 356|/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n*357|/// construction. The recovery verb for wedged brain-held state that previously\n 358|/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n 359|/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\n 360|/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n...\n 412|fn staged_already_applied(\n*413|    applied_state: Option<&AppliedRecord>,\n 414|    applied_version: Option<u64>,\n 415|    staged_version: u64,\n 416|) -> bool {\n 417|    // A trial for this version (or newer) already swapped the binary on disk.\n*418|    if let Some(AppliedRecord::AppliedPending { version, .. }) = applied_state {\n 419|        if *version >= staged_version {\n 420|            return true;\n 421|        }\n...\n 528|    fn staged_already_applied_classifies_up_to_date_vs_newer() {\n*529|        // First-ever apply: nothing applied, no state → proceed.\n 530|        assert!(!staged_already_applied(None, None, 8));\n 531|        // A newer version is staged than what's applied → proceed (apply it).\n 532|        assert!(!staged_already_applied(None, Some(5), 8));\n...\n 567|    // COMPILED image version over IPC: a Brain connected to a live broker gets back\n*568|    // Some(the broker's own CARGO_PKG_VERSION) — the KIND_BROKER_IMAGE dispatch +\n 569|    // reply round-trip end to end (the new-broker leg; the old-broker→None leg rides\n 570|    // the router's `unknown command kind` default, caught client-side as None).\n 571|    #[test]\n...\n 612|        // Stamp the local platform so the apply guard (REQ-UPD-8) lets these\n*613|        // single-release paths through to the class/tamper logic under test;\n 614|        // the unstamped/mismatched refusals get their own dedicated test.\n 615|        cache\n 616|            .stage_single_with_platform(&signed, artifact, current_platform())\n...\n 760|        //    registered platform that is not this host — derived from the\n*761|        //    SUPPORTED_PLATFORMS registry (not a hardcoded win/linux pair), so a\n 762|        //    third platform (musl) is covered with no edit here.\n 763|        let other = crate::release::SUPPORTED_PLATFORMS\n 764|            .iter()\n...\n 766|            .find(|&t| t != current_platform())\n*767|            .expect(\"registry must have a platform other than the current host\");\n 768|        cache\n 769|            .stage_single_with_platform(&signed, b\"CROSS-OS BYTES\", other)\n 770|            .unwrap();\n...\n 950|            matches!(\n*951|                cache.applied_state(),\n 952|                Some(AppliedRecord::AppliedPending { version: 7, .. })\n 953|            ),\n 954|            \"daemonless apply records the open AppliedPending trial\"\n...\n 990|        cache\n*991|            .record_applied_state(&AppliedRecord::AppliedPending {\n 992|                version: 9,\n 993|                rollback_binary: \"spt-binary.old-8\".to_string(),\n 994|                candidate_started_ms: 0,\n...\n 1152|        cache\n*1153|            .record_applied_state(&AppliedRecord::RolledBack {\n 1154|                quarantine_version: 7,\n 1155|                running_version: 6,\n 1156|                rollback_binary: exe.with_file_name(\"spt-binary.old-7\").to_string_lossy().into(),\n...\n 1169|        cache8\n*1170|            .record_applied_state(&AppliedRecord::RolledBack {\n 1171|                quarantine_version: 7,\n 1172|                running_version: 6,\n 1173|                rollback_binary: exe.with_file_name(\"spt-binary.old-7\").to_string_lossy().into(),\n## attach.rs\n*1|//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n 2|//! onto a local session for an operator on another node.\n 3|//!\n*4|//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n 5|//! [`AttachRecord::Request`]; this module is the target-side pump between that\n*6|//! stream and the local broker-hosted session:\n 7|//!\n 8|//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n 9|//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n...\n 12|//!   journal (a replayed input record cannot double-type);\n*13|//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n 14|//!\n 15|//! ## Exactly-once lives at the EDGES, not the wire sends\n 16|//!\n*17|//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n*18|//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n 19|//! and *discards* everything interleaved — inside a full-duplex pump loop that\n 20|//! eats the operator's `Input` records (both sides starve; found the hard way\n 21|//! in `tests/attach.rs`). Idempotency holds without it because each direction\n...\n 23|//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n*24|//! session-log `seq` (the render cursor). A post-restart re-serve may\n 25|//! re-transmit output records on the wire — duplicates are dropped at the\n*26|//! render cursor, the same discipline the local `OutputLog` replay uses\n 27|//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n 28|//!\n 29|//! **Compute + files stay here**; the operator is a viewport (CONTEXT\n...\n 32|//! [`access_check`] sees the **handshake-proven** origin from the broker's\n*33|//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n 34|//! your own running instance is ungated by default (ADR-0009 default-open);\n 35|//! the D5d whitelist drops in at this same call.\n 36|//!\n 37|//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n*38|//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n 39|//! A restarted target brain re-attaches the session from its output cursor,\n*40|//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n 41|//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42|//! `Output.seq`.\n 43|\n...\n 54|\n*55|/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n 56|/// served viewport is a **driver** — attaching wakes the endpoint, the\n 57|/// viewport ending detaches it (active → dormant; the transition echo is\n 58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n...\n 73|    Refused,\n*74|    /// The session's child exited; the `Exit` record was sent and the stream\n 75|    /// finished.\n 76|    Exited(Option<i32>),\n 77|    /// The operator closed its send side (detached); the viewport ended with\n...\n 80|    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n*81|    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n 82|    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n 83|    Displaced,\n 84|}\n...\n 154|/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n*155|/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n 156|/// Size / Exit / …) pass through unchanged.\n 157|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 158|/// `gen` is the serve worker's attach generation (ADR-0038 Amendment fix 6):\n...\n 206|/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*207|/// (a stale registry row routed us an attach for something gone → the caller\n 208|/// refuses cleanly, D6).\n 209|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210|pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n...\n 220|/// One attach `Request` — the SINGLE handling path for both a wire-arriving\n*221|/// record and the dispatcher's durable-opener seed (ADR-0038 B; one\n 222|/// implementation so the two entries cannot drift):\n 223|///\n 224|/// - Unsolicited-inbound gate (ADR-0009) under the transport-proven origin;\n...\n 226|///   denial finishes our send side and ends the serve (`Some(Refused)`).\n*227|/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n 228|///   session id wins (the owning node is authoritative for its own table);\n 229|///   the LOCAL path uses the wire `session_id` unchanged.\n 230|/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n...\n 240|    wire: &mut Brain,\n*241|    stream_id: u64,\n 242|    endpoint: &str,\n 243|    origin_node: &str,\n 244|    remote_session: Option<u64>,\n...\n 254|    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n*255|        wire.net_stream_send(stream_id, &[], None, true)?;\n 256|        return Ok(Some(AttachServeOutcome::Refused));\n 257|    }\n 258|    let sid = remote_session.unwrap_or(session_id);\n## autostart.rs\n 6|//! operator marks an endpoint a **startup default** with `spt endpoint run\n*7|//! --save` (the endpoint analog of the shipped `subnet attach --save`\n 8|//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n 9|//! at start.\n 10|//!\n 11|//! Contract: a replay is best-effort and LOUD — an unresolvable adapter or a\n*12|//! failed launch logs its skip and moves on; nothing here can block daemon\n 13|//! start or the other replays. Each replay is a FRESH session (shape (c)\n 14|//! restore-what-was-up was rejected): the adapter option re-resolves against\n*15|//! the live registry, the session id is freshly minted, and home rides the\n 16|//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n 17|//! recorded or needed.\n 18|\n...\n 49|        let (record, manifest) =\n*50|            match spt_runtime::registry::resolve_option(&adapters_dir, &entry.adapter) {\n 51|                Ok((r, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => {\n 52|                    (r, m)\n 53|                }\n## brain.rs\n*1|//! The brain (B2) — the restartable logic half that attaches to the broker.\n 2|//!\n*3|//! The brain holds all the volatile logic; it may be killed and restarted at\n 4|//! any moment (a routine self-update swaps it; a crash kills it). It connects to\n 5|//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions\n 6|//! (spawn / input / resize / kill), and consumes their output. Two invariants\n...\n 11|//!   brain rehydrates its resume cursor and generation *counter* from prior\n*12|//!   state, but its `gen_start` is `now()`, **never** the stale value from that\n*13|//!   state — a rehydrated `gen_start` would fire time-based discriminators on\n*14|//!   the new generation. [`Brain::handoff`] takes the prior state by value yet\n 15|//!   ignores its `gen_start_ms`.\n 16|//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next\n 17|//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;\n...\n 31|\n*32|use interprocess::local_socket::{SendHalf, Stream};\n 33|\n 34|use crate::codec::{read_frame, write_frame};\n 35|use crate::frame::{Envelope, Role};\n...\n 40|    InputReq,\n*41|    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n*42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n*43|    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n*44|    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n 45|    ViewerEvictedEvent,\n 46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n 47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n...\n 50|    KIND_NET_SENT,\n*51|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*52|    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n*53|    KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED,\n*54|    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n 55|    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n*56|    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n 57|};\n 58|use spt_net::net::attach::AttachIntent;\n 59|use crate::transport::{send_hello, LocalSocketTransport};\n...\n 69|\n*70|/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n 71|/// advance the generation. `gen_start_ms` is recorded for observability but is\n 72|/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n 73|/// fresh `gen_start` from `now()` (2.4).\n...\n 78|/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n*79|/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n 80|/// solely because the integration tests (separate test target — `#[cfg(test)]`\n 81|/// would hide them) still drive the handoff-frame shape directly; there is **no\n 82|/// non-test caller** (KNOWN-HAZARDS 2.4 grep-clean close-out).\n 83|#[derive(Debug, Clone)]\n*84|pub struct BrainState {\n 85|    /// The session the brain was driving.\n 86|    pub session_id: u64,\n 87|    /// The generation counter (incremented on each handoff).\n...\n 143|    /// (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION) — the bare ack for\n*144|    /// [`Brain::submit_dial`]. The dial's real outcome rides the presence stream\n 145|    /// ([`Self::NetPresence`]), not this frame.\n 146|    NetDialSubmitted,\n*147|    /// A stream open durably landed — fresh or deduped (D4b).\n## brainproc.rs\n 9|//! `spt daemon brain` child. The child connects over the existing broker socket\n*10|//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n 11|//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n*12|//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n 13|//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n 14|//! verb. The digest hub serve + seed-control stay broker-side continuity\n 15|//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n...\n 138|/// The `spt daemon brain` child entry. Connect to the broker over the existing\n*139|/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n 140|/// holding the connection and probing broker liveness each heartbeat. A dropped\n 141|/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n 142|/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n...\n 144|///\n*145|/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n 146|/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n 147|/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n 148|/// broker — a brain respawn re-spawns them from the swapped binary. They are\n...\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 161|    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n 162|    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n 163|    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n 164|    // [impl->REQ-DAEMON-STDERR-PERSIST]\n*165|    let _ = crate::stderrlog::install(\"brain\", generation);\n 166|    let name = broker_socket_name();\n 167|    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n 168|    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n 169|    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n*170|    // brain→brain BrainState.generation, which retires in D4), and the reason is\n 171|    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n 172|    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n 173|    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n...\n 183|    // from its delivered cursor, the production replacement for the retired\n*184|    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n 185|    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n 186|    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n 187|    // (the live-agent adapter). A failure here is non-fatal — the brain still\n...\n 225|    // instance's `wake_command` as a supervised watcher child. Relocated from\n*226|    // the broker process to the brain child: shellwake is brain-owned logic\n 227|    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n 228|    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n 229|    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n...\n 247|\n*248|    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n 249|    // brain hosts the SOLE single-flight writer — load persisted index, boot\n 250|    // reconcile in the background (ready-without-warm is preserved because\n 251|    // write_ready already fired above), then drain/debounce/coalesce the\n...\n 254|    // rehydrates from the persisted index (warm start, fingerprint-gated).\n*255|    // [impl->REQ-PROJECT-INDEX-WRITER]\n 256|    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n 257|\n 258|    let mut consumers_started = false;\n...\n 275|            eprintln!(\n*276|                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n 277|            );\n 278|        }\n 279|        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n...\n 288|        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n*289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n 290|        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291|        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292|        // net_status probe above already owns broker-liveness). Self-applies: it drives\n...\n 303|/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n*304|/// semantics that keep a single dispatcher/peer-pump set running.\n 305|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 306|fn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n 307|    net_enabled && !already_started\n...\n 309|\n*310|/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n 311|/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n 312|/// the brain child (restoration D2-1):** both are standalone IPC clients\n 313|/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n*314|/// restartable brain. Each mints its own registry + stop flag and connects\n 315|/// independently; when the brain process exits these in-process threads die with\n 316|/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n 317|/// the wiring the broker boot used to do, `node_hex` now sourced from the\n...\n 321|fn spawn_net_consumers(node_hex: &str) {\n*322|    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323|    let stop = Arc::new(AtomicBool::new(false));\n*324|    let _ = crate::dispatch::spawn_dispatcher(\n 325|        broker_socket_name(),\n*326|        Arc::clone(&registry),\n 327|        Arc::clone(&stop),\n 328|    );\n 329|    let cfg = crate::config::DaemonConfig::load();\n 330|    if cfg.peer_pump {\n*331|        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n 332|    }\n 333|}\n 334|\n## broker.rs\n 3|//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n*4|//! would lose continuity on if the logic restarted: the PTY master+writer and\n 5|//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n*6|//! client / listening sockets. It runs **no logic** — no routing, registry,\n 7|//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n 8|//! forward its output to the brain, inject the brain's input, report exit.\n 9|//!\n...\n 13|//! crash). The broker — and every hosted child — must survive that untouched,\n*14|//! and the new brain must re-attach to the output stream **gaplessly and\n 15|//! exactly-once** (Spike #1 made real). Two mechanisms:\n 16|//!\n 17|//! - **Sequenced, buffered output.** Each session's output is appended to an\n*18|//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n*19|//!   in a bounded ring. The current subscriber (a brain connection) receives\n*20|//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n 21|//!   reach the brain in strict `seq` order with no interleave.\n 22|//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n*23|//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n 24|//!   live. The brain dedups by `seq`. Output produced *while no brain was\n 25|//!   attached* sits in the ring and is replayed on the next subscribe — so a\n 26|//!   brain restart loses nothing.\n...\n 28|//! Sessions are therefore **broker-owned and outlive any one connection**: a\n*29|//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n*30|//! output log persist. A session leaves the table only when its child exits\n 31|//! (reaped by the exit waiter) or is explicitly killed.\n 32|// [impl->REQ-DAEMON-2]\n 33|\n...\n 41|\n*42|use interprocess::local_socket::{prelude::*, Stream};\n 43|use spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n 44|\n 45|use spt_net::net::attach::AttachIntent;\n...\n 56|    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n*57|    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n*58|    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n*59|    NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n*60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62|    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63|    KIND_NET_SENT,\n...\n 65|    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n*66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n...\n 76|/// replay frames, command acks, and exit/error events all write through this.\n*77|/// `pub(crate)`: the net host's stream logs (D4b) live-send through the same\n 78|/// handle under the same gate discipline. Round-4\n 79|/// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): no longer a raw\n 80|/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n...\n 84|\n*85|/// Default bounded output-log depth (chunks). Large enough that a brain restart\n 86|/// window never evicts unreplayed output in practice; the bound is what keeps a\n 87|/// disconnected session from buffering without limit.\n*88|const DEFAULT_LOG_CHUNKS: usize = 4096;\n 89|\n 90|/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n 91|/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n 92|/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n*93|/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n 94|/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n 95|/// owns the replay batch), so a viewer attaching to a busy session is not\n 96|/// spuriously evicted.\n## codec.rs\n 5|//! framing) but generic over any [`Read`]/[`Write`] — the IPC carrier is a\n*6|//! local socket (named pipe / UDS), not a `TcpStream`, and tests exercise it\n 7|//! over in-memory buffers and socket pairs.\n 8|//!\n 9|//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n...\n 75|\n*76|    // [unit->REQ-DAEMON-2] multiple frames stream back-to-back without bleed.\n 77|    #[test]\n 78|    fn back_to_back_frames_decode_independently() {\n 79|        let mut buf: Vec<u8> = Vec::new();\n## config.rs\n 35|/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n*36|/// registry/notif feeds to trusted peers and pulls context syncs from them.\n 37|/// Conservative defaults — every loop is a thin call over an idempotent\n 38|/// driver, so a missed tick costs only latency, never correctness.\n*39|pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40|pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41|pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n 42|pub const DEFAULT_UPDATE_CHECK_PERIOD: Duration = Duration::from_secs(3_600);\n 43|\n*44|/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n 45|/// authored by a node not heard within this window are evicted on the next\n*46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n 47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n*50|pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n 51|\n 52|/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n 53|/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n...\n 67|/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\n*68|/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\n 69|/// design: the adapter option is re-resolved fresh at each replay (an adapter\n 70|/// update between restarts is honored, an uninstalled one loud-skips), and the\n 71|/// endpoint's home subnet is NOT recorded (home is durable on the perch —\n...\n 81|    pub adapter: String,\n*82|    /// The project cwd the harness PTY lands in (captured at `--save`).\n 83|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 84|    pub cwd: Option<String>,\n 85|}\n...\n 104|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*105|    registry_pump_period_ms: Option<u64>,\n 106|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*107|    registry_evict_after_ms: Option<u64>,\n 108|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 109|    notif_pump_period_ms: Option<u64>,\n 110|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n...\n 152|    pub peer_pump: bool,\n*153|    /// Registry feed push cadence (D9-1).\n*154|    pub registry_pump_period: Duration,\n*155|    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows from a\n*156|    /// node not heard within this window are evicted on the next registry\n 157|    /// pump tick — a dead node identity must not poison bare-id resolution\n*158|    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n*159|    pub registry_evict_after: Duration,\n 160|    /// Notif feed push cadence (D9-1).\n 161|    pub notif_pump_period: Duration,\n 162|    /// Context sync pull cadence — the anti-entropy loop (D9-1).\n...\n 178|    pub resources_blurb: Option<String>,\n*179|    /// Subnets whose serve-state STARTS detached (M8 decision 6,\n 180|    /// REQ-SUBNET-5): the daemon's boot reset seeds the live\n 181|    /// `attachment.json` from this list — `spt subnet detach/attach --save`\n 182|    /// writes it. Default empty = every held subnet starts attached.\n...\n 192|    /// REQ-ENDPOINT-AUTOSTART) — `spt endpoint run --save` writes this list.\n*193|    /// Default empty = nothing auto-starts. A replay failure logs loud and\n 194|    /// never blocks daemon start or the other replays.\n 195|    pub startup_endpoints: Vec<StartupEndpoint>,\n 196|    /// The docs server port override (ADR-0036 §4, REQ-DOCS-LOCAL-SERVER).\n...\n 210|            peer_pump: true,         // the subnet self-drives (D9-1)\n*211|            registry_pump_period: DEFAULT_REGISTRY_PUMP_PERIOD,\n*212|            registry_evict_after: DEFAULT_REGISTRY_EVICT_AFTER,\n 213|            notif_pump_period: DEFAULT_NOTIF_PUMP_PERIOD,\n 214|            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,\n 215|            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,\n...\n 270|            peer_pump: file.peer_pump.unwrap_or(d.peer_pump),\n*271|            registry_pump_period: file\n## conn.rs\n 11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n...\n 74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n*75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n 78|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n...\n 81|/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n*82|/// startup ([`init_log_anchor`], both broker and brain entries) so every\n 83|/// record in one process shares one base; lazily initialized on first stamp\n 84|/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n 85|/// not an absolute one.\n...\n 87|\n*88|/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n 89|/// thing by both daemon entries, next to `scrub_identity_env`.\n 90|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*91|pub(crate) fn init_log_anchor() {\n 92|    let _ = MONO_ANCHOR.set(Instant::now());\n 93|}\n 94|\n 95|/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n*96|/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n 97|/// stderr has no per-line timestamps and broker+brain share one file — without\n 98|/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n 99|/// to field incident times (wall).\n 100|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*101|pub(crate) fn log_stamp() -> String {\n 102|    let wall_ms = SystemTime::now()\n 103|        .duration_since(UNIX_EPOCH)\n 104|        .map(|d| d.as_millis())\n...\n 110|/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n*111|/// conn can be controller + viewer + presence subscriber over its life), and\n 112|/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113|const LABEL_CAP: usize = 240;\n 114|\n...\n 138|\n*139|/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n 140|/// the watchdog must not keep the conn alive).\n 141|struct Inner {\n 142|    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143|    id: u64,\n*144|    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145|    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n 146|    label: Mutex<String>,\n 147|    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n*148|    first_write_logged: AtomicBool,\n 149|    gate: Mutex<Gate>,\n 150|    gate_cv: Condvar,\n 151|    dog: Mutex<Dog>,\n...\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 175|    /// aborting this handle covers a parked read too. Valid for the whole\n 176|    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n 177|    /// `graveyard` until drop.\n...\n 183|\n*184|/// Recover a mutex guard even if a holder panicked — the state these guards\n 185|/// protect is a plain slot/flag pair, valid on every panic boundary.\n 186|fn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n 187|    match m.lock() {\n...\n 225|        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n*226|        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n 227|    }\n 228|\n 229|    /// Render the once-per-conn retirement record — the F-039 token split\n...\n 430|            label: Mutex::new(String::new()),\n*431|            first_write_logged: AtomicBool::new(false),\n 432|            gate: Mutex::new(Gate { half: Some(half) }),\n 433|            gate_cv: Condvar::new(),\n 434|            dog: Mutex::new(Dog {\n...\n 455|    /// The stable per-physical-conn id — what lifecycle records key on\n*456|    /// (`Arc::ptr_eq` identity does not survive a log line).\n 457|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 458|    pub(crate) fn id(&self) -> u64 {\n 459|        self.inner.id\n...\n 461|\n*462|    /// Accrete an attribution fact onto this conn's label (subscriber role,\n 463|    /// endpoint/session where known — e.g. `role=brain`,\n 464|    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n 465|    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n...\n 545|        // triplets with fresh ids).\n*546|        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n 547|            inner.lifecycle(\"write-start\", \"\");\n 548|        }\n 549|\n...\n 605|/// checked out) so every writer parks AT THE GATE until the pin drops — the\n*606|/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n 607|/// window (the P1c epoch-gate keystone unit relies on it).\n 608|#[cfg(test)]\n 609|pub(crate) struct TestGatePin {\n## consent.rs\n 12|//!   orchestrator should prompt.\n*13|//! - [`most_recently_active`] — resolve that target over the **v1 registry**\n 14|//!   (live perches + their `last_active_ms` recency stamp). This is the\n*15|//!   registry-resolution precursor CONTEXT names; the deferred PresenceChannel\n 16|//!   later generalizes it.\n 17|//!\n 18|//! What C2 deliberately does **not** build: the interactive prompt UX itself.\n...\n 28|/// most-recently-active live perch. Carries enough to address it (id + path) and\n*29|/// the recency that won it the selection (for logging / tie-break audit).\n 30|#[derive(Debug, Clone, PartialEq, Eq)]\n 31|pub struct ConsentTarget {\n 32|    /// The agent/perch id of the most-recently-active live session.\n...\n 71|///\n*72|/// This is the **v1 registry resolution** the consent flow rides — live perches +\n 73|/// their recency stamp, not the deferred PresenceChannel. The resolver core is\n 74|/// shared with the notif engine ([`crate::notif::most_recently_active_where`],\n 75|/// D8b — the subnet-scoped form); consent is the degenerate (unscoped) call.\n## crc_swap.rs\n 128|    // last copy). With the target present the `.old` is provably stale, so this is\n*129|    // safe; in the crash-recovery state the `.old` survives until a SUCCESSFUL commit\n 130|    // repopulates the target, and the NEXT run (target now present) sweeps it.\n 131|    // [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n 132|    for swap in plan {\n## daemon.rs\n 31|use crate::nethost::{NetConfig, NetHost};\n*32|use crate::seedmap::{self, SeedRegistry};\n 33|\n 34|/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\n 35|const START_TIMEOUT: Duration = Duration::from_secs(5);\n...\n 49|        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n*50|        // identity that is NEVER correct daemon state and would otherwise\n*51|        // leak into every spawn on the node. Scrub first, before any state\n 52|        // is touched or any child can be spawned.\n 53|        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n 54|        spt_runtime::scrub_identity_env();\n...\n 56|        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n*57|        crate::conn::init_log_anchor();\n 58|        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n 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...\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 80|                Err(e) => {\n 81|                    return Err(io::Error::other(format!(\n 82|                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n...\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...\n 120|\n*121|        let reg = Arc::new(SeedRegistry::new());\n 122|\n 123|        // Boot housekeeping (KNOWN-HAZARDS 1.3): sweep stale `index.lock`\n 124|        // wedges out of the git-touching runtime the daemon hosts — the\n*125|        // context store's seed repo + its agent/project worktrees (a crashed\n 126|        // git leaves a 0-byte lock that blocks every later commit forever).\n 127|        // Path composed via the layout authority without opening the full\n 128|        // ContextStore on the boot path; a lock-less (or store-less) home\n...\n 137|\n*138|        // Serve-state boot reset (M8 decision 6, REQ-SUBNET-5): the live\n*139|        // attachment state starts from the config's startup defaults — an\n 140|        // unsaved attach/detach flip deliberately does not survive a daemon\n 141|        // restart (`--save` is the persistence path).\n 142|        // [impl->REQ-SUBNET-5]\n...\n 195|                    });\n*196|                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n 197|                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n 198|                // so they live with the restartable brain and respawn with it.\n 199|                // The broker keeps only the NetHost bring-up and the boot-race\n...\n 217|                    // just bound. Background thread — a saved endpoint that fails\n*218|                    // to come up logs loud and never blocks daemon start, the\n 219|                    // seed channel, or the other replays.\n 220|                    // [impl->REQ-ENDPOINT-AUTOSTART]\n 221|                    thread::spawn(|| {\n...\n 225|                    // docs are up whenever the daemon is. Loopback only; a bind\n*226|                    // failure (port squatted) logs loud and the daemon runs on —\n 227|                    // docs are a degradable surface, never a boot blocker.\n 228|                    // [impl->REQ-DOCS-LOCAL-SERVER]\n 229|                    {\n...\n 253|\n*254|        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n 255|        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n*256|        // delta-stream subscribes against it. Served on a background thread like\n 257|        // the broker. The per-session feed that *populates* the hub is driven by\n*258|        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n 259|        // sessions are spawned through the broker; a bare daemon serves the\n 260|        // channel and answers \"no live digest\" until a session is hosted.\n 261|        let digest_hub = Arc::new(DigestHub::new());\n...\n 282|\n*283|        // The per-shell shell-TUNNEL registry (M11-W3, REQ-SHELL-4) — the opaque\n## deelevate.rs\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...\n 129|        pub gid: u32,\n*130|        /// The invoker's username (`SUDO_USER`), for `USER`/`LOGNAME`.\n 131|        pub user: Option<String>,\n*132|        /// The invoker's home dir (passwd lookup), for `HOME` — the state\n 133|        /// universe anchor (`spt_home` resolves through `$HOME`).\n 134|        pub home: Option<PathBuf>,\n 135|    }\n...\n 274|    /// `sudo spt` decides and every later one is deterministic. `None` =\n*275|    /// not elevated, or no identity to drop to (genuine root login).\n 276|    // [impl->REQ-INSTALL-6]\n 277|    pub fn daemon_target_user() -> Option<SudoInvoker> {\n 278|        if unsafe { libc::geteuid() } != 0 {\n...\n 314|            \"spt: first elevated run — which account should the spt daemon (and its \\\n*315|             state) run under? [{default_name}]: \"\n 316|        );\n 317|        let _ = std::io::stderr().flush();\n 318|        let mut s = String::new();\n...\n 363|        /// Stamp the invoker's identity env onto a [`std::process::Command`]\n*364|        /// (HOME/USER/LOGNAME; SUDO_* removed so the child never re-triggers\n*365|        /// de-elevation logic). uid/gid ride `CommandExt::uid/gid` at the\n 366|        /// call site — std drops supplementary groups when a uid is set.\n 367|        pub fn stamp_env(&self, cmd: &mut std::process::Command) {\n 368|            cmd.env_remove(\"SUDO_UID\")\n...\n 374|            if let Some(user) = &self.user {\n*375|                cmd.env(\"USER\", user).env(\"LOGNAME\", user);\n 376|            }\n 377|        }\n 378|\n...\n 404|                std::env::set_var(\"USER\", user);\n*405|                std::env::set_var(\"LOGNAME\", user);\n 406|            }\n 407|            Ok(())\n 408|        }\n...\n 468|            token: isize,\n*469|            logon_flags: u32,\n 470|            app: *const u16,\n 471|            cmdline: *mut u16,\n 472|            flags: u32,\n...\n 772|                    token,\n*773|                    1, // LOGON_WITH_PROFILE: the desktop user's profile (usually loaded; cheap then)\n 774|                    std::ptr::null(),\n 775|                    cmdline_w.as_mut_ptr(),\n 776|                    flags,\n## digest.rs\n*1|//! The session-digest **log projection** — the daemon-side, on-demand projection\n*2|//! of an endpoint's live activity buffer from its normalized session logs\n*3|//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n 4|//!\n*5|//! ## On-demand projection, not a byte feed\n 6|//!\n 7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n*8|//! on every output chunk. The digest is now a **projection of the same normalized\n 9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n*10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n*11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n 12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n*13|//! read the logs.\n 14|//!\n 15|//! ## Sources (one per endpoint)\n 16|//!\n 17|//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or\n\n[Showing lines 1-933 of 1154 (50.0KB limit). Use :934 to continue]"}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":273,"fileCount":20,"files":["crates/spt-daemon/src/access.rs","crates/spt-daemon/src/adapter_update.rs","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/codec.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/consent.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/docshost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/access.rs","count":12},{"path":"crates/spt-daemon/src/adapter_update.rs","count":7},{"path":"crates/spt-daemon/src/applyhost.rs","count":20},{"path":"crates/spt-daemon/src/attach.rs","count":20},{"path":"crates/spt-daemon/src/autostart.rs","count":4},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":5},{"path":"crates/spt-daemon/src/brain.rs","count":20},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/codec.rs","count":2},{"path":"crates/spt-daemon/src/config.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":20},{"path":"crates/spt-daemon/src/consent.rs","count":4},{"path":"crates/spt-daemon/src/crc_swap.rs","count":1},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/deelevate.rs","count":12},{"path":"crates/spt-daemon/src/digest.rs","count":20},{"path":"crates/spt-daemon/src/digesthub.rs","count":20},{"path":"crates/spt-daemon/src/dispatch.rs","count":20},{"path":"crates/spt-daemon/src/docshost.rs","count":6}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## access.rs\n   4│//! order: subnet *visibility* (is the endpoint routable here at all? —\n  *5│//! `spt_store::visibility`, applied inside registry resolution) → the\n   6│//! **endpoint access whitelist** (may *your node* remotely reach it? — THIS\n   7│//! module) → *capability grants* (may this agent do this high-risk thing? —\n   8│//! [`crate::consent`], default-deny). The whitelist is the outer gate before\n    │...\n  10│//!\n *11│//! ## Whitelist semantics (stateful firewall, not a blanket block)\n  12│//!\n  13│//! - **Origin-node gate:** the subject is the node the inbound interaction\n  14│//!   *originates from* (where the operator is acting) — never the sender\n  15│//!   endpoint's identity or home. The subject value MUST be the QUIC\n *16│//!   handshake-proven remote node id from the broker's conn/stream table,\n  17│//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).\n  18│//! - **Outbound** → any visible node: the whitelist never restricts who an\n  19│//!   endpoint talks *to*.\n    │...\n  22│//!   (\"established/related\").\n *23│//!   Classification reads the durable [`RecentOutbound`] log\n  24│//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].\n  25│//! - **Unsolicited/direct inbound** → only from a whitelisted node.\n  26│//! - **Same-node operation is always allowed** — the gate sits on the\n    │...\n  35│//! an `spt access` edit takes effect on the next inbound without a daemon\n *36│//! restart, and the checks run at message/stream-establishment rate where a\n  37│//! small-file read is noise.\n  38│//!\n  39│//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies\n    │...\n  47│\n *48│/// How an inbound interaction reached this node, for the stateful-firewall\n  49│/// split.\n  50│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n  51│pub enum InboundClass {\n    │...\n 112│/// Classify an inbound *message* for the gate: [`InboundClass::Reply`] iff the\n*113│/// target endpoint's own recent outbound (the durable [`RecentOutbound`] log)\n 114│/// names the origin node within the reply window — else the conservative\n 115│/// [`InboundClass::Unsolicited`].\n 116│// [impl->REQ-SEC-1]\n    │...\n 126│/// [`classify_inbound`] later correlates a reply against. Call on every\n*127│/// outbound WAN send (the sending leg's side of the stateful firewall).\n 128│/// Best-effort: a failed write only costs a future reply its exemption (it\n 129│/// will classify unsolicited — the conservative direction).\n 130│// [impl->REQ-SEC-1]\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 136│\n 137│#[cfg(test)]\n    │...\n 193│\n*194│    // [unit->REQ-SEC-1] the stateful-firewall exemption: a Reply classifies\n 195│    // past a whitelist that would refuse the same node unsolicited — and\n 196│    // classification itself turns on the recorded outbound within the window.\n 197│    #[test]\n## adapter_update.rs\n   7│//! its manifest `[update]` block ([`Update`]); this module turns that directive\n  *8│//! into a per-adapter [`AdapterUpdateOutcome`], dispatched by avenue:\n   9│//!\n  10│//! - **`file_pull`** — spt-core controls the bytes, so it *verifies* them: the\n  11│//!   pulled payload is checked against the adapter's **own** Ed25519 key (declared\n    │...\n 163│/// is complete here. Soft-deregistered adapters are excluded by\n*164│/// [`spt_runtime::registry::registered`].\n 165│// [impl->REQ-INSTALL-4]\n 166│pub fn ripple_registered(adapters_dir: &std::path::Path) -> Vec<(String, AdapterUpdateOutcome)> {\n*167│    let mut set = spt_runtime::registry::registered(adapters_dir);\n 168│    set.sort_by_key(|(r, _)| r.registered_at_ms);\n 169│    set.iter()\n 170│        .map(|(r, m)| {\n    │...\n 408│        // Register out of name order to prove registration-time ordering.\n*409│        spt_runtime::registry::register(&adapters_dir, &updatable, 100).unwrap();\n*410│        spt_runtime::registry::register(&adapters_dir, &plain, 200).unwrap();\n*411│        spt_runtime::registry::register(&adapters_dir, &gone, 300).unwrap();\n*412│        spt_runtime::registry::deregister(&adapters_dir, \"gone\").unwrap();\n 413│\n 414│        let out = ripple_registered(&adapters_dir);\n 415│        assert_eq!(\n## applyhost.rs\n   34│//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n  *35│//!    subscriber while the live brain kept running the OLD code.\n   36│//!\n   37│//! New code runs **immediately** — the supervised respawn lands on the new\n  *38│//! executable, no manual bounce, no logon. Readiness-gated promotion +\n   39│//! auto-rollback on a brain that fails to come up is D6.\n   40│// [impl->REQ-UPD-3]\n   41│// [impl->REQ-UPD-4]\n     │...\n  140│    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n *141│    let applied_state = cache.applied_state();\n  142│    if let Some(AppliedRecord::RolledBack {\n  143│        quarantine_version, ..\n *144│    }) = &applied_state\n  145│    {\n  146│        if *quarantine_version == version {\n  147│            return Ok(Prepared::Done(ApplyStagedOutcome::Quarantined { version }));\n     │...\n  155│    // to a friendly exit-0 no-op instead. Checked before verify/connect: an\n *156│    // already-applied node is up to date regardless of the staged bytes' state.\n  157│    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n *158│    if staged_already_applied(applied_state.as_ref(), cache.applied_version(), version) {\n  159│        return Ok(Prepared::Done(ApplyStagedOutcome::AlreadyApplied {\n  160│            version,\n  161│            product_version,\n     │...\n  279│    // [impl->REQ-UPD-6]\n *280│    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n  281│        version,\n  282│        rollback_binary: aside.to_string_lossy().into_owned(),\n  283│        candidate_started_ms: now_ms(),\n     │...\n  331│    // level. This replaces the old in-process `apply_brain_only` handoff (the\n *332│    // regression: it re-attached a CLI-side subscriber while the live brain kept\n  333│    // running the OLD code). `classify` already gated this as brain-only, so the\n  334│    // broker never moves; only the brain restarts.\n  335│    //\n     │...\n  356│/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n *357│/// construction. The recovery verb for wedged brain-held state that previously\n  358│/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n  359│/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\n  360│/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n     │...\n  412│fn staged_already_applied(\n *413│    applied_state: Option<&AppliedRecord>,\n  414│    applied_version: Option<u64>,\n  415│    staged_version: u64,\n  416│) -> bool {\n  417│    // A trial for this version (or newer) already swapped the binary on disk.\n *418│    if let Some(AppliedRecord::AppliedPending { version, .. }) = applied_state {\n  419│        if *version >= staged_version {\n  420│            return true;\n  421│        }\n     │...\n  528│    fn staged_already_applied_classifies_up_to_date_vs_newer() {\n *529│        // First-ever apply: nothing applied, no state → proceed.\n  530│        assert!(!staged_already_applied(None, None, 8));\n  531│        // A newer version is staged than what's applied → proceed (apply it).\n  532│        assert!(!staged_already_applied(None, Some(5), 8));\n     │...\n  567│    // COMPILED image version over IPC: a Brain connected to a live broker gets back\n *568│    // Some(the broker's own CARGO_PKG_VERSION) — the KIND_BROKER_IMAGE dispatch +\n  569│    // reply round-trip end to end (the new-broker leg; the old-broker→None leg rides\n  570│    // the router's `unknown command kind` default, caught client-side as None).\n  571│    #[test]\n     │...\n  612│        // Stamp the local platform so the apply guard (REQ-UPD-8) lets these\n *613│        // single-release paths through to the class/tamper logic under test;\n  614│        // the unstamped/mismatched refusals get their own dedicated test.\n  615│        cache\n  616│            .stage_single_with_platform(&signed, artifact, current_platform())\n     │...\n  760│        //    registered platform that is not this host — derived from the\n *761│        //    SUPPORTED_PLATFORMS registry (not a hardcoded win/linux pair), so a\n  762│        //    third platform (musl) is covered with no edit here.\n  763│        let other = crate::release::SUPPORTED_PLATFORMS\n  764│            .iter()\n     │...\n  766│            .find(|&t| t != current_platform())\n *767│            .expect(\"registry must have a platform other than the current host\");\n  768│        cache\n  769│            .stage_single_with_platform(&signed, b\"CROSS-OS BYTES\", other)\n  770│            .unwrap();\n     │...\n  950│            matches!(\n *951│                cache.applied_state(),\n  952│                Some(AppliedRecord::AppliedPending { version: 7, .. })\n  953│            ),\n  954│            \"daemonless apply records the open AppliedPending trial\"\n     │...\n  990│        cache\n *991│            .record_applied_state(&AppliedRecord::AppliedPending {\n  992│                version: 9,\n  993│                rollback_binary: \"spt-binary.old-8\".to_string(),\n  994│                candidate_started_ms: 0,\n     │...\n 1152│        cache\n*1153│            .record_applied_state(&AppliedRecord::RolledBack {\n 1154│                quarantine_version: 7,\n 1155│                running_version: 6,\n 1156│                rollback_binary: exe.with_file_name(\"spt-binary.old-7\").to_string_lossy().into(),\n     │...\n 1169│        cache8\n*1170│            .record_applied_state(&AppliedRecord::RolledBack {\n 1171│                quarantine_version: 7,\n 1172│                running_version: 6,\n 1173│                rollback_binary: exe.with_file_name(\"spt-binary.old-7\").to_string_lossy().into(),\n## attach.rs\n  *1│//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n   2│//! onto a local session for an operator on another node.\n   3│//!\n  *4│//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n   5│//! [`AttachRecord::Request`]; this module is the target-side pump between that\n  *6│//! stream and the local broker-hosted session:\n   7│//!\n   8│//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n   9│//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n    │...\n  12│//!   journal (a replayed input record cannot double-type);\n *13│//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n  14│//!\n  15│//! ## Exactly-once lives at the EDGES, not the wire sends\n  16│//!\n *17│//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n *18│//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n  19│//! and *discards* everything interleaved — inside a full-duplex pump loop that\n  20│//! eats the operator's `Input` records (both sides starve; found the hard way\n  21│//! in `tests/attach.rs`). Idempotency holds without it because each direction\n    │...\n  23│//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n *24│//! session-log `seq` (the render cursor). A post-restart re-serve may\n  25│//! re-transmit output records on the wire — duplicates are dropped at the\n *26│//! render cursor, the same discipline the local `OutputLog` replay uses\n  27│//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n  28│//!\n  29│//! **Compute + files stay here**; the operator is a viewport (CONTEXT\n    │...\n  32│//! [`access_check`] sees the **handshake-proven** origin from the broker's\n *33│//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n  34│//! your own running instance is ungated by default (ADR-0009 default-open);\n  35│//! the D5d whitelist drops in at this same call.\n  36│//!\n  37│//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n *38│//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n  39│//! A restarted target brain re-attaches the session from its output cursor,\n *40│//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n  41│//! a restarted operator brain resubscribes and dedups rendered chunks by\n  42│//! `Output.seq`.\n  43│\n    │...\n  54│\n *55│/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n  56│/// served viewport is a **driver** — attaching wakes the endpoint, the\n  57│/// viewport ending detaches it (active → dormant; the transition echo is\n  58│/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n    │...\n  73│    Refused,\n *74│    /// The session's child exited; the `Exit` record was sent and the stream\n  75│    /// finished.\n  76│    Exited(Option<i32>),\n  77│    /// The operator closed its send side (detached); the viewport ended with\n    │...\n  80│    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n *81│    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n  82│    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n  83│    Displaced,\n  84│}\n    │...\n 154│/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n*155│/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n 156│/// Size / Exit / …) pass through unchanged.\n 157│// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 158│/// `gen` is the serve worker's attach generation (ADR-0038 Amendment fix 6):\n    │...\n 206│/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*207│/// (a stale registry row routed us an attach for something gone → the caller\n 208│/// refuses cleanly, D6).\n 209│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210│pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    │...\n 220│/// One attach `Request` — the SINGLE handling path for both a wire-arriving\n*221│/// record and the dispatcher's durable-opener seed (ADR-0038 B; one\n 222│/// implementation so the two entries cannot drift):\n 223│///\n 224│/// - Unsolicited-inbound gate (ADR-0009) under the transport-proven origin;\n    │...\n 226│///   denial finishes our send side and ends the serve (`Some(Refused)`).\n*227│/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n 228│///   session id wins (the owning node is authoritative for its own table);\n 229│///   the LOCAL path uses the wire `session_id` unchanged.\n 230│/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n    │...\n 240│    wire: &mut Brain,\n*241│    stream_id: u64,\n 242│    endpoint: &str,\n 243│    origin_node: &str,\n 244│    remote_session: Option<u64>,\n    │...\n 254│    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n*255│        wire.net_stream_send(stream_id, &[], None, true)?;\n 256│        return Ok(Some(AttachServeOutcome::Refused));\n 257│    }\n 258│    let sid = remote_session.unwrap_or(session_id);\n## autostart.rs\n  6│//! operator marks an endpoint a **startup default** with `spt endpoint run\n *7│//! --save` (the endpoint analog of the shipped `subnet attach --save`\n  8│//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n  9│//! at start.\n 10│//!\n 11│//! Contract: a replay is best-effort and LOUD — an unresolvable adapter or a\n*12│//! failed launch logs its skip and moves on; nothing here can block daemon\n 13│//! start or the other replays. Each replay is a FRESH session (shape (c)\n 14│//! restore-what-was-up was rejected): the adapter option re-resolves against\n*15│//! the live registry, the session id is freshly minted, and home rides the\n 16│//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n 17│//! recorded or needed.\n 18│\n   │...\n 49│        let (record, manifest) =\n*50│            match spt_runtime::registry::resolve_option(&adapters_dir, &entry.adapter) {\n 51│                Ok((r, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => {\n 52│                    (r, m)\n 53│                }\n## brain.rs\n  *1│//! The brain (B2) — the restartable logic half that attaches to the broker.\n   2│//!\n  *3│//! The brain holds all the volatile logic; it may be killed and restarted at\n   4│//! any moment (a routine self-update swaps it; a crash kills it). It connects to\n   5│//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions\n   6│//! (spawn / input / resize / kill), and consumes their output. Two invariants\n    │...\n  11│//!   brain rehydrates its resume cursor and generation *counter* from prior\n *12│//!   state, but its `gen_start` is `now()`, **never** the stale value from that\n *13│//!   state — a rehydrated `gen_start` would fire time-based discriminators on\n *14│//!   the new generation. [`Brain::handoff`] takes the prior state by value yet\n  15│//!   ignores its `gen_start_ms`.\n  16│//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next\n  17│//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;\n    │...\n  31│\n *32│use interprocess::local_socket::{SendHalf, Stream};\n  33│\n  34│use crate::codec::{read_frame, write_frame};\n  35│use crate::frame::{Envelope, Role};\n    │...\n  40│    InputReq,\n *41│    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n *42│    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n *43│    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n *44│    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n  45│    ViewerEvictedEvent,\n  46│    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n  47│    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n    │...\n  50│    KIND_NET_SENT,\n *51│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n *52│    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n *53│    KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED,\n *54│    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n  55│    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n *56│    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n  57│};\n  58│use spt_net::net::attach::AttachIntent;\n  59│use crate::transport::{send_hello, LocalSocketTransport};\n    │...\n  69│\n *70│/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n  71│/// advance the generation. `gen_start_ms` is recorded for observability but is\n  72│/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n  73│/// fresh `gen_start` from `now()` (2.4).\n    │...\n  78│/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n *79│/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n  80│/// solely because the integration tests (separate test target — `#[cfg(test)]`\n  81│/// would hide them) still drive the handoff-frame shape directly; there is **no\n  82│/// non-test caller** (KNOWN-HAZARDS 2.4 grep-clean close-out).\n  83│#[derive(Debug, Clone)]\n *84│pub struct BrainState {\n  85│    /// The session the brain was driving.\n  86│    pub session_id: u64,\n  87│    /// The generation counter (incremented on each handoff).\n    │...\n 143│    /// (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION) — the bare ack for\n*144│    /// [`Brain::submit_dial`]. The dial's real outcome rides the presence stream\n 145│    /// ([`Self::NetPresence`]), not this frame.\n 146│    NetDialSubmitted,\n*147│    /// A stream open durably landed — fresh or deduped (D4b).\n## brainproc.rs\n   9│//! `spt daemon brain` child. The child connects over the existing broker socket\n *10│//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n  11│//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n *12│//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n  13│//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n  14│//! verb. The digest hub serve + seed-control stay broker-side continuity\n  15│//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n    │...\n 138│/// The `spt daemon brain` child entry. Connect to the broker over the existing\n*139│/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n 140│/// holding the connection and probing broker liveness each heartbeat. A dropped\n 141│/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n 142│/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n    │...\n 144│///\n*145│/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n 146│/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n 147│/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n 148│/// broker — a brain respawn re-spawns them from the swapped binary. They are\n    │...\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 161│    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n 162│    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n 163│    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n 164│    // [impl->REQ-DAEMON-STDERR-PERSIST]\n*165│    let _ = crate::stderrlog::install(\"brain\", generation);\n 166│    let name = broker_socket_name();\n 167│    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n 168│    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n 169│    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n*170│    // brain→brain BrainState.generation, which retires in D4), and the reason is\n 171│    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n 172│    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n 173│    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    │...\n 183│    // from its delivered cursor, the production replacement for the retired\n*184│    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n 185│    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n 186│    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n 187│    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    │...\n 225│    // instance's `wake_command` as a supervised watcher child. Relocated from\n*226│    // the broker process to the brain child: shellwake is brain-owned logic\n 227│    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n 228│    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n 229│    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    │...\n 247│\n*248│    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n 249│    // brain hosts the SOLE single-flight writer — load persisted index, boot\n 250│    // reconcile in the background (ready-without-warm is preserved because\n 251│    // write_ready already fired above), then drain/debounce/coalesce the\n    │...\n 254│    // rehydrates from the persisted index (warm start, fingerprint-gated).\n*255│    // [impl->REQ-PROJECT-INDEX-WRITER]\n 256│    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n 257│\n 258│    let mut consumers_started = false;\n    │...\n 275│            eprintln!(\n*276│                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n 277│            );\n 278│        }\n 279│        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n    │...\n 288│        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n*289│        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n 290│        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291│        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292│        // net_status probe above already owns broker-liveness). Self-applies: it drives\n    │...\n 303│/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n*304│/// semantics that keep a single dispatcher/peer-pump set running.\n 305│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 306│fn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n 307│    net_enabled && !already_started\n    │...\n 309│\n*310│/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n 311│/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n 312│/// the brain child (restoration D2-1):** both are standalone IPC clients\n 313│/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n*314│/// restartable brain. Each mints its own registry + stop flag and connects\n 315│/// independently; when the brain process exits these in-process threads die with\n 316│/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n 317│/// the wiring the broker boot used to do, `node_hex` now sourced from the\n    │...\n 321│fn spawn_net_consumers(node_hex: &str) {\n*322│    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323│    let stop = Arc::new(AtomicBool::new(false));\n*324│    let _ = crate::dispatch::spawn_dispatcher(\n 325│        broker_socket_name(),\n*326│        Arc::clone(&registry),\n 327│        Arc::clone(&stop),\n 328│    );\n 329│    let cfg = crate::config::DaemonConfig::load();\n 330│    if cfg.peer_pump {\n*331│        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n 332│    }\n 333│}\n 334│\n## broker.rs\n  3│//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n *4│//! would lose continuity on if the logic restarted: the PTY master+writer and\n  5│//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n *6│//! client / listening sockets. It runs **no logic** — no routing, registry,\n  7│//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n  8│//! forward its output to the brain, inject the brain's input, report exit.\n  9│//!\n   │...\n 13│//! crash). The broker — and every hosted child — must survive that untouched,\n*14│//! and the new brain must re-attach to the output stream **gaplessly and\n 15│//! exactly-once** (Spike #1 made real). Two mechanisms:\n 16│//!\n 17│//! - **Sequenced, buffered output.** Each session's output is appended to an\n*18│//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n*19│//!   in a bounded ring. The current subscriber (a brain connection) receives\n*20│//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n 21│//!   reach the brain in strict `seq` order with no interleave.\n 22│//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n*23│//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n 24│//!   live. The brain dedups by `seq`. Output produced *while no brain was\n 25│//!   attached* sits in the ring and is replayed on the next subscribe — so a\n 26│//!   brain restart loses nothing.\n   │...\n 28│//! Sessions are therefore **broker-owned and outlive any one connection**: a\n*29│//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n*30│//! output log persist. A session leaves the table only when its child exits\n 31│//! (reaped by the exit waiter) or is explicitly killed.\n 32│// [impl->REQ-DAEMON-2]\n 33│\n   │...\n 41│\n*42│use interprocess::local_socket::{prelude::*, Stream};\n 43│use spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n 44│\n 45│use spt_net::net::attach::AttachIntent;\n   │...\n 56│    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n*57│    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n*58│    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n*59│    NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n*60│    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61│    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62│    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63│    KIND_NET_SENT,\n   │...\n 65│    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n*66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70│};\n   │...\n 76│/// replay frames, command acks, and exit/error events all write through this.\n*77│/// `pub(crate)`: the net host's stream logs (D4b) live-send through the same\n 78│/// handle under the same gate discipline. Round-4\n 79│/// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): no longer a raw\n 80│/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n   │...\n 84│\n*85│/// Default bounded output-log depth (chunks). Large enough that a brain restart\n 86│/// window never evicts unreplayed output in practice; the bound is what keeps a\n 87│/// disconnected session from buffering without limit.\n*88│const DEFAULT_LOG_CHUNKS: usize = 4096;\n 89│\n 90│/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n 91│/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n 92│/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n*93│/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n 94│/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n 95│/// owns the replay batch), so a viewer attaching to a busy session is not\n 96│/// spuriously evicted.\n## codec.rs\n  5│//! framing) but generic over any [`Read`]/[`Write`] — the IPC carrier is a\n *6│//! local socket (named pipe / UDS), not a `TcpStream`, and tests exercise it\n  7│//! over in-memory buffers and socket pairs.\n  8│//!\n  9│//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n   │...\n 75│\n*76│    // [unit->REQ-DAEMON-2] multiple frames stream back-to-back without bleed.\n 77│    #[test]\n 78│    fn back_to_back_frames_decode_independently() {\n 79│        let mut buf: Vec<u8> = Vec::new();\n## config.rs\n  35│/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n *36│/// registry/notif feeds to trusted peers and pulls context syncs from them.\n  37│/// Conservative defaults — every loop is a thin call over an idempotent\n  38│/// driver, so a missed tick costs only latency, never correctness.\n *39│pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n  40│pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n  41│pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n  42│pub const DEFAULT_UPDATE_CHECK_PERIOD: Duration = Duration::from_secs(3_600);\n  43│\n *44│/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n  45│/// authored by a node not heard within this window are evicted on the next\n *46│/// registry pump tick. Sized to ~10 default pump cadences — generous against\n  47│/// transient drops (a peer re-advertises every cadence, so one admitted feed\n  48│/// inside the window keeps its rows), short enough that a dead node identity\n  49│/// stops poisoning bare-id resolution within minutes.\n *50│pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n  51│\n  52│/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n  53│/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n    │...\n  67│/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\n *68│/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\n  69│/// design: the adapter option is re-resolved fresh at each replay (an adapter\n  70│/// update between restarts is honored, an uninstalled one loud-skips), and the\n  71│/// endpoint's home subnet is NOT recorded (home is durable on the perch —\n    │...\n  81│    pub adapter: String,\n *82│    /// The project cwd the harness PTY lands in (captured at `--save`).\n  83│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n  84│    pub cwd: Option<String>,\n  85│}\n    │...\n 104│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*105│    registry_pump_period_ms: Option<u64>,\n 106│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*107│    registry_evict_after_ms: Option<u64>,\n 108│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 109│    notif_pump_period_ms: Option<u64>,\n 110│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    │...\n 152│    pub peer_pump: bool,\n*153│    /// Registry feed push cadence (D9-1).\n*154│    pub registry_pump_period: Duration,\n*155│    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows from a\n*156│    /// node not heard within this window are evicted on the next registry\n 157│    /// pump tick — a dead node identity must not poison bare-id resolution\n*158│    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n*159│    pub registry_evict_after: Duration,\n 160│    /// Notif feed push cadence (D9-1).\n 161│    pub notif_pump_period: Duration,\n 162│    /// Context sync pull cadence — the anti-entropy loop (D9-1).\n    │...\n 178│    pub resources_blurb: Option<String>,\n*179│    /// Subnets whose serve-state STARTS detached (M8 decision 6,\n 180│    /// REQ-SUBNET-5): the daemon's boot reset seeds the live\n 181│    /// `attachment.json` from this list — `spt subnet detach/attach --save`\n 182│    /// writes it. Default empty = every held subnet starts attached.\n    │...\n 192│    /// REQ-ENDPOINT-AUTOSTART) — `spt endpoint run --save` writes this list.\n*193│    /// Default empty = nothing auto-starts. A replay failure logs loud and\n 194│    /// never blocks daemon start or the other replays.\n 195│    pub startup_endpoints: Vec<StartupEndpoint>,\n 196│    /// The docs server port override (ADR-0036 §4, REQ-DOCS-LOCAL-SERVER).\n    │...\n 210│            peer_pump: true,         // the subnet self-drives (D9-1)\n*211│            registry_pump_period: DEFAULT_REGISTRY_PUMP_PERIOD,\n*212│            registry_evict_after: DEFAULT_REGISTRY_EVICT_AFTER,\n 213│            notif_pump_period: DEFAULT_NOTIF_PUMP_PERIOD,\n 214│            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,\n 215│            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,\n    │...\n 270│            peer_pump: file.peer_pump.unwrap_or(d.peer_pump),\n*271│            registry_pump_period: file\n## conn.rs\n  11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n *12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n  13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n  14│//! connection, so the writer (and every write queued behind the same gate)\n  15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n    │...\n  74│/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n *75│/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n  76│/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n  77│/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n  78│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n    │...\n  81│/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n *82│/// startup ([`init_log_anchor`], both broker and brain entries) so every\n  83│/// record in one process shares one base; lazily initialized on first stamp\n  84│/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n  85│/// not an absolute one.\n    │...\n  87│\n *88│/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n  89│/// thing by both daemon entries, next to `scrub_identity_env`.\n  90│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n *91│pub(crate) fn init_log_anchor() {\n  92│    let _ = MONO_ANCHOR.set(Instant::now());\n  93│}\n  94│\n  95│/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n *96│/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n  97│/// stderr has no per-line timestamps and broker+brain share one file — without\n  98│/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n  99│/// to field incident times (wall).\n 100│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*101│pub(crate) fn log_stamp() -> String {\n 102│    let wall_ms = SystemTime::now()\n 103│        .duration_since(UNIX_EPOCH)\n 104│        .map(|d| d.as_millis())\n    │...\n 110│/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n*111│/// conn can be controller + viewer + presence subscriber over its life), and\n 112│/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113│const LABEL_CAP: usize = 240;\n 114│\n    │...\n 138│\n*139│/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n 140│/// the watchdog must not keep the conn alive).\n 141│struct Inner {\n 142│    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143│    id: u64,\n*144│    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145│    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n 146│    label: Mutex<String>,\n 147│    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n*148│    first_write_logged: AtomicBool,\n 149│    gate: Mutex<Gate>,\n 150│    gate_cv: Condvar,\n 151│    dog: Mutex<Dog>,\n    │...\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 175│    /// aborting this handle covers a parked read too. Valid for the whole\n 176│    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n 177│    /// `graveyard` until drop.\n    │...\n 183│\n*184│/// Recover a mutex guard even if a holder panicked — the state these guards\n 185│/// protect is a plain slot/flag pair, valid on every panic boundary.\n 186│fn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n 187│    match m.lock() {\n    │...\n 225│        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n*226│        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n 227│    }\n 228│\n 229│    /// Render the once-per-conn retirement record — the F-039 token split\n    │...\n 430│            label: Mutex::new(String::new()),\n*431│            first_write_logged: AtomicBool::new(false),\n 432│            gate: Mutex::new(Gate { half: Some(half) }),\n 433│            gate_cv: Condvar::new(),\n 434│            dog: Mutex::new(Dog {\n    │...\n 455│    /// The stable per-physical-conn id — what lifecycle records key on\n*456│    /// (`Arc::ptr_eq` identity does not survive a log line).\n 457│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 458│    pub(crate) fn id(&self) -> u64 {\n 459│        self.inner.id\n    │...\n 461│\n*462│    /// Accrete an attribution fact onto this conn's label (subscriber role,\n 463│    /// endpoint/session where known — e.g. `role=brain`,\n 464│    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n 465│    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    │...\n 545│        // triplets with fresh ids).\n*546│        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n 547│            inner.lifecycle(\"write-start\", \"\");\n 548│        }\n 549│\n    │...\n 605│/// checked out) so every writer parks AT THE GATE until the pin drops — the\n*606│/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n 607│/// window (the P1c epoch-gate keystone unit relies on it).\n 608│#[cfg(test)]\n 609│pub(crate) struct TestGatePin {\n## consent.rs\n 12│//!   orchestrator should prompt.\n*13│//! - [`most_recently_active`] — resolve that target over the **v1 registry**\n 14│//!   (live perches + their `last_active_ms` recency stamp). This is the\n*15│//!   registry-resolution precursor CONTEXT names; the deferred PresenceChannel\n 16│//!   later generalizes it.\n 17│//!\n 18│//! What C2 deliberately does **not** build: the interactive prompt UX itself.\n   │...\n 28│/// most-recently-active live perch. Carries enough to address it (id + path) and\n*29│/// the recency that won it the selection (for logging / tie-break audit).\n 30│#[derive(Debug, Clone, PartialEq, Eq)]\n 31│pub struct ConsentTarget {\n 32│    /// The agent/perch id of the most-recently-active live session.\n   │...\n 71│///\n*72│/// This is the **v1 registry resolution** the consent flow rides — live perches +\n 73│/// their recency stamp, not the deferred PresenceChannel. The resolver core is\n 74│/// shared with the notif engine ([`crate::notif::most_recently_active_where`],\n 75│/// D8b — the subnet-scoped form); consent is the degenerate (unscoped) call.\n## crc_swap.rs\n 128│    // last copy). With the target present the `.old` is provably stale, so this is\n*129│    // safe; in the crash-recovery state the `.old` survives until a SUCCESSFUL commit\n 130│    // repopulates the target, and the NEXT run (target now present) sweeps it.\n 131│    // [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n 132│    for swap in plan {\n## daemon.rs\n  31│use crate::nethost::{NetConfig, NetHost};\n *32│use crate::seedmap::{self, SeedRegistry};\n  33│\n  34│/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\n  35│const START_TIMEOUT: Duration = Duration::from_secs(5);\n    │...\n  49│        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n *50│        // identity that is NEVER correct daemon state and would otherwise\n *51│        // leak into every spawn on the node. Scrub first, before any state\n  52│        // is touched or any child can be spawned.\n  53│        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n  54│        spt_runtime::scrub_identity_env();\n    │...\n  56│        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n *57│        crate::conn::init_log_anchor();\n  58│        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n  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    │...\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  80│                Err(e) => {\n  81│                    return Err(io::Error::other(format!(\n  82│                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n    │...\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    │...\n 120│\n*121│        let reg = Arc::new(SeedRegistry::new());\n 122│\n 123│        // Boot housekeeping (KNOWN-HAZARDS 1.3): sweep stale `index.lock`\n 124│        // wedges out of the git-touching runtime the daemon hosts — the\n*125│        // context store's seed repo + its agent/project worktrees (a crashed\n 126│        // git leaves a 0-byte lock that blocks every later commit forever).\n 127│        // Path composed via the layout authority without opening the full\n 128│        // ContextStore on the boot path; a lock-less (or store-less) home\n    │...\n 137│\n*138│        // Serve-state boot reset (M8 decision 6, REQ-SUBNET-5): the live\n*139│        // attachment state starts from the config's startup defaults — an\n 140│        // unsaved attach/detach flip deliberately does not survive a daemon\n 141│        // restart (`--save` is the persistence path).\n 142│        // [impl->REQ-SUBNET-5]\n    │...\n 195│                    });\n*196│                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n 197│                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n 198│                // so they live with the restartable brain and respawn with it.\n 199│                // The broker keeps only the NetHost bring-up and the boot-race\n    │...\n 217│                    // just bound. Background thread — a saved endpoint that fails\n*218│                    // to come up logs loud and never blocks daemon start, the\n 219│                    // seed channel, or the other replays.\n 220│                    // [impl->REQ-ENDPOINT-AUTOSTART]\n 221│                    thread::spawn(|| {\n    │...\n 225│                    // docs are up whenever the daemon is. Loopback only; a bind\n*226│                    // failure (port squatted) logs loud and the daemon runs on —\n 227│                    // docs are a degradable surface, never a boot blocker.\n 228│                    // [impl->REQ-DOCS-LOCAL-SERVER]\n 229│                    {\n    │...\n 253│\n*254│        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n 255│        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n*256│        // delta-stream subscribes against it. Served on a background thread like\n 257│        // the broker. The per-session feed that *populates* the hub is driven by\n*258│        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n 259│        // sessions are spawned through the broker; a bare daemon serves the\n 260│        // channel and answers \"no live digest\" until a session is hosted.\n 261│        let digest_hub = Arc::new(DigestHub::new());\n    │...\n 282│\n*283│        // The per-shell shell-TUNNEL registry (M11-W3, REQ-SHELL-4) — the opaque\n## deelevate.rs\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    │...\n 129│        pub gid: u32,\n*130│        /// The invoker's username (`SUDO_USER`), for `USER`/`LOGNAME`.\n 131│        pub user: Option<String>,\n*132│        /// The invoker's home dir (passwd lookup), for `HOME` — the state\n 133│        /// universe anchor (`spt_home` resolves through `$HOME`).\n 134│        pub home: Option<PathBuf>,\n 135│    }\n    │...\n 274│    /// `sudo spt` decides and every later one is deterministic. `None` =\n*275│    /// not elevated, or no identity to drop to (genuine root login).\n 276│    // [impl->REQ-INSTALL-6]\n 277│    pub fn daemon_target_user() -> Option<SudoInvoker> {\n 278│        if unsafe { libc::geteuid() } != 0 {\n    │...\n 314│            \"spt: first elevated run — which account should the spt daemon (and its \\\n*315│             state) run under? [{default_name}]: \"\n 316│        );\n 317│        let _ = std::io::stderr().flush();\n 318│        let mut s = String::new();\n    │...\n 363│        /// Stamp the invoker's identity env onto a [`std::process::Command`]\n*364│        /// (HOME/USER/LOGNAME; SUDO_* removed so the child never re-triggers\n*365│        /// de-elevation logic). uid/gid ride `CommandExt::uid/gid` at the\n 366│        /// call site — std drops supplementary groups when a uid is set.\n 367│        pub fn stamp_env(&self, cmd: &mut std::process::Command) {\n 368│            cmd.env_remove(\"SUDO_UID\")\n    │...\n 374│            if let Some(user) = &self.user {\n*375│                cmd.env(\"USER\", user).env(\"LOGNAME\", user);\n 376│            }\n 377│        }\n 378│\n    │...\n 404│                std::env::set_var(\"USER\", user);\n*405│                std::env::set_var(\"LOGNAME\", user);\n 406│            }\n 407│            Ok(())\n 408│        }\n    │...\n 468│            token: isize,\n*469│            logon_flags: u32,\n 470│            app: *const u16,\n 471│            cmdline: *mut u16,\n 472│            flags: u32,\n    │...\n 772│                    token,\n*773│                    1, // LOGON_WITH_PROFILE: the desktop user's profile (usually loaded; cheap then)\n 774│                    std::ptr::null(),\n 775│                    cmdline_w.as_mut_ptr(),\n 776│                    flags,\n## digest.rs\n  *1│//! The session-digest **log projection** — the daemon-side, on-demand projection\n  *2│//! of an endpoint's live activity buffer from its normalized session logs\n  *3│//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n   4│//!\n  *5│//! ## On-demand projection, not a byte feed\n   6│//!\n   7│//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n  *8│//! on every output chunk. The digest is now a **projection of the same normalized\n   9│//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n *10│//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n *11│//! records via [`project_endpoint_digest`], and the hub publishes the result.\n  12│//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n *13│//! read the logs.\n  14│//!\n  15│//! ## Sources (one per endpoint)\n  16│//!\n  17│//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or\n *18│//!   Path B native store) → the projection tails those normalized records. This\n *19│//!   is the **topology-independent** path: a Claude-Code-hosted endpoint's\n  20│//!   per-session JSONL is Path A's worked example.\n *21│//! - A **log-less** adapter pushes digest-records directly via `api digest-entry`\n *22│//!   into the perch's `digest.log` → the projection reads that.\n  23│//!\n  24│//! The records carry the published digest-record contract\n *25│//! ([`spt_term::DigestRecord`]); the projection folds them into the structured\n  26│//! [`Digest`] (N-turn window, collapsed tool sprints, ~25-char arg truncation).\n  27│//!\n  28│//! ## Snapshot vs delta\n  29│//!\n *30│//! A projection is diffed against the hub's prior snapshot\n *31│//! ([`crate::digesthub::DigestHub::project_and_publish`]) to produce a\n *32│//! [`DigestUpdate`]: `from` is where the changed turns begin, and the subscriber\n  33│//! applies it by **truncating its view to `from`, then appending `turns`**. The\n *34│//! delta-stream **contract** ships; its deltas are driven by pull / digest-entry\n  35│//! publishes (autonomous file-watch freshness is deferred to the consuming\n  36│//! frontend milestone — CONTEXT.md \"session digest\").\n  37│// [impl->REQ-TERM-4]\n    │...\n  48│\n *49│/// Bound for the projection's extractor run (the `[digest]` extractor is a\n  50│/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n  51│const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n  52│\n *53│/// How many recent sessions the spanning projection enumerates from the ledger\n  54│/// (REQ-TERM-6 — \"the last K\"). Bounds the per-session extractor runs; the visible\n  55│/// turn window ([`DigestConfig::window_turns`]) bounds what is shown across them.\n  56│const SPAN_SESSIONS: usize = 5;\n    │...\n 100│\n*101│/// A structured digest change to push to a subscriber. Sent only when the digest\n## digesthub.rs\n  3│//!\n *4│//! The hub holds the latest projected snapshot per endpoint and fans every change\n *5│//! out to live subscribers. The digest is **projected on demand** from the\n *6│//! endpoint's normalized records ([`project_endpoint_digest`]) — there is no live\n *7│//! feed; a pull or a `digest-entry` push triggers a projection, and\n *8│//! [`DigestHub::project_and_publish`] diffs it against the prior snapshot to\n  9│//! produce the [`DigestUpdate`] delta. The control channel is the access surface\n 10│//! ADR-0008 describes — two modes over one transport:\n 11│//!\n*12│//! - **snapshot pull** — `spt endpoint digest <id>` ([`pull_snapshot`]) projects\n 13│//!   the endpoint's current buffer on demand and returns it;\n*14│//! - **delta-stream** — `spt endpoint digest <id> --follow` ([`follow`])\n 15│//!   subscribes, receives a base snapshot, then **only the changes**\n*16│//!   ([`DigestUpdate`]s) as pulls / `digest-entry` pushes re-project. The stream\n 17│//!   contract ships; autonomous file-watch freshness is deferred to the consuming\n 18│//!   frontend milestone (CONTEXT.md \"session digest\").\n 19│//!\n   │...\n 21│//! handshake as the broker / seed-control IPC (a transient `spt` CLI connects as a\n*22│//! [`Role::Brain`] logic client). **Local addressing only** — the qualified\n 23│//! `[subnet:]id@node` cross-node form is a later milestone (rejected by the CLI\n 24│//! before it ever reaches here).\n 25│// [impl->REQ-TERM-4]\n   │...\n 36│use crate::codec::{read_frame, write_frame};\n*37│use crate::digest::{common_prefix_len, project_endpoint_digest, DigestOverride, DigestUpdate};\n 38│use crate::frame::{Envelope, Role};\n 39│use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};\n 40│\n*41│/// Client→daemon: pull the current structured digest for an endpoint (projects\n 42│/// it on demand from the endpoint's normalized records).\n 43│pub const KIND_DIGEST_SNAPSHOT: &str = \"digest_snapshot\";\n 44│/// Daemon→client: the snapshot reply (digest + version; absent ⇒ no digest).\n 45│pub const KIND_DIGEST_RESULT: &str = \"digest_result\";\n*46│/// Client→daemon: subscribe to an endpoint's delta-stream from a version floor.\n 47│pub const KIND_DIGEST_SUBSCRIBE: &str = \"digest_subscribe\";\n*48│/// Daemon→client: one structured digest change (the streamed delta / base).\n 49│pub const KIND_DIGEST_DELTA: &str = \"digest_delta\";\n*50│/// Client→daemon: re-project an endpoint now and publish any change to live\n*51│/// subscribers — the push-driven delta after an `api digest-entry` append (no\n*52│/// reply; the delta reaches subscribers via the fan-out, REQ-TERM-4).\n*53│pub const KIND_DIGEST_REPROJECT: &str = \"digest_reproject\";\n 54│\n 55│/// `digest_snapshot` / `digest_subscribe` request payload.\n 56│#[derive(Debug, Clone, Serialize, Deserialize)]\n   │...\n 59│    pub id: String,\n*60│    /// Subscribe floor: only stream changes with `version > from_version`. The\n*61│    /// base snapshot is sent regardless so a fresh subscriber has an apply base.\n 62│    #[serde(default)]\n 63│    pub from_version: u64,\n 64│    /// Consumer presentation override (ADR-0019) — overrides the adapter's\n   │...\n 80│\n*81│/// One endpoint's live digest state in the hub: the latest snapshot + version\n## dispatch.rs\n *1│//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\n  2│//! the hand-wired serve loops of D5–D8 into production behavior.\n  3│//!\n  4│//! Every E2E test so far drove the serve half manually (a probe brain calling\n  5│//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\n *6│//! a known stream). In production nothing did — a peer could dial and write\n *7│//! and the bytes sat in the broker's stream log unread. This module is the\n *8│//! missing consumer: a daemon-hosted loop that watches the broker's stream\n *9│//! table for **peer-initiated** streams, classifies each by its first NDJSON\n 10│//! record, and hands it to the right serve loop on its own worker thread.\n 11│//!\n 12│//! ## Direction (the gotcha-#9 fact, now first-class)\n 13│//!\n*14│//! The stream table holds locally-opened outbound streams too. The dispatcher\n 15│//! claims only rows with `initiated_locally == false` — serving our own\n*16│//! requester stream would deadlock the requester against ourselves. The flag\n*17│//! is broker-recorded at registration (`open_stream` vs the accept loop),\n 18│//! never inferred.\n 19│//!\n 20│//! ## Classification (first-record demux)\n 21│//!\n*22│//! Streams are single-purpose; the opener's first record names the protocol.\n 23│//! The kind-tagged families collide on variant names (`request` is both\n 24│//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n 25│//! demux keys on the first line's **shape**, not its kind alone:\n   │...\n 36│//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n*37│//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n 38│//! | anything else | unknown — dropped, loud |\n 39│//!\n 40│//! A family whose production wiring lands in a later slice is still\n 41│//! classified here (the table is complete) but routed to a typed\n*42│//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n 43│//!\n 44│//! ## Worker model\n 45│//!\n*46│//! Each claimed stream gets its own worker thread with its **own** brain IPC\n 47│//! connection (the broker is multi-client by design; the probe-brain pattern\n 48│//! every E2E test uses). The peek and the serve use **separate** connections:\n 49│//! the peek brain subscribes from 0 to read the first line and is dropped;\n 50│//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n*51│//! stream's event feed alone (no double-subscription interleave). Origin is\n 52│//! the table's `remote_id_hex` — handshake-proven, never payload\n 53│//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n 54│// [impl->REQ-NET-1]\n   │...\n 66│use spt_net::net::replicate::{\n*67│    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n 68│};\n 69│use spt_net::net::wanmsg::{WanDecoder, WanReply};\n 70│use spt_store::contextstore::ContextStore;\n   │...\n 78│use crate::propagate::serve_update;\n*79│use crate::registryhost::{RegistryGatePolicy, RegistryHost};\n 80│use crate::relcache::ReleaseCache;\n 81│use crate::sync::{serve_sync, SyncPolicy};\n 82│use crate::wan::receive_wan;\n   │...\n 84│\n*85│/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n*86│/// workers block on their stream's event feed).\n*87│pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n 88│\n*89│/// What protocol a peer-initiated stream speaks, by its first record's shape.\n 90│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n*91│pub enum StreamFamily {\n 92│    /// A context sync pull (`SyncRecord::Request`).\n 93│    Sync,\n 94│    /// An update pull (`UpdRecord::Query`).\n## docshost.rs\n 128│        \"woff\" => \"font/woff\",\n*129│        _ => \"application/octet-stream\",\n 130│    }\n 131│}\n 132│\n    │...\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    │...\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 234│        let root = docs_root.clone();\n 235│        tokio::task::spawn_local(async move {\n 236│            let service = service_fn(move |req| handle(req, root.clone()));\n    │...\n 248│/// actually-bound port (`port = 0` ⇒ ephemeral, for tests). A bind failure\n*249│/// returns the error — the caller logs loud and the daemon continues (docs\n 250│/// are a degradable surface, never a boot blocker).\n 251│// [impl->REQ-DOCS-LOCAL-SERVER]\n 252│pub fn start(docs_root: PathBuf, port: u16) -> io::Result<u16> {\n    │...\n 352│            content_type_for(Path::new(\"unknown.bin\")),\n*353│            \"application/octet-stream\"\n 354│        );\n 355│    }\n 356│\n\n## bin/\n### xlate_choreo_fixture.rs\n 31│//! Privacy assertion support (doyle gate #1): every stdin line we receive is\n*32│//! appended verbatim to the file named by env `XLATE_FIXTURE_STDIN_LOG` (when\n 33│//! set). The int test reads that file and asserts NO operator keystroke payload\n 34│//! (e.g. the controller's typed bytes) ever appears — only `init` / `event` /\n 35│//! content-free `input` lines do. The binary never sees PTY keystroke content.\n   │...\n 44│    let mode = std::env::var(\"XLATE_FIXTURE_MODE\").unwrap_or_else(|_| \"choreo\".to_string());\n*45│    let stdin_log = std::env::var(\"XLATE_FIXTURE_STDIN_LOG\").ok();\n 46│\n 47│    let stdin = io::stdin();\n 48│    let mut out = io::stdout();\n   │...\n 60│        // Record EVERY received stdin line for the privacy (content-free ping)\n*61│        // assertion. Best-effort: a log failure never changes our behavior.\n*62│        if let Some(path) = &stdin_log {\n 63│            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n 64│                let _ = writeln!(f, \"{trimmed}\");\n 65│            }\n   │...\n 68│        // Only `{\"type\":\"event\",…}` drives a response. `init` and the\n*69│        // content-free `input` ping are noted (logged above) and ignored.\n 70│        let v: serde_json::Value = match serde_json::from_str(trimmed) {\n 71│            Ok(v) => v,\n 72│            Err(_) => continue,","truncation":{"content":"# crates/spt-daemon/src/\n## access.rs\n 4|//! order: subnet *visibility* (is the endpoint routable here at all? —\n*5|//! `spt_store::visibility`, applied inside registry resolution) → the\n 6|//! **endpoint access whitelist** (may *your node* remotely reach it? — THIS\n 7|//! module) → *capability grants* (may this agent do this high-risk thing? —\n 8|//! [`crate::consent`], default-deny). The whitelist is the outer gate before\n...\n 10|//!\n*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12|//!\n 13|//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14|//!   *originates from* (where the operator is acting) — never the sender\n 15|//!   endpoint's identity or home. The subject value MUST be the QUIC\n*16|//!   handshake-proven remote node id from the broker's conn/stream table,\n 17|//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).\n 18|//! - **Outbound** → any visible node: the whitelist never restricts who an\n 19|//!   endpoint talks *to*.\n...\n 22|//!   (\"established/related\").\n*23|//!   Classification reads the durable [`RecentOutbound`] log\n 24|//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].\n 25|//! - **Unsolicited/direct inbound** → only from a whitelisted node.\n 26|//! - **Same-node operation is always allowed** — the gate sits on the\n...\n 35|//! an `spt access` edit takes effect on the next inbound without a daemon\n*36|//! restart, and the checks run at message/stream-establishment rate where a\n 37|//! small-file read is noise.\n 38|//!\n 39|//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies\n...\n 47|\n*48|/// How an inbound interaction reached this node, for the stateful-firewall\n 49|/// split.\n 50|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 51|pub enum InboundClass {\n...\n 112|/// Classify an inbound *message* for the gate: [`InboundClass::Reply`] iff the\n*113|/// target endpoint's own recent outbound (the durable [`RecentOutbound`] log)\n 114|/// names the origin node within the reply window — else the conservative\n 115|/// [`InboundClass::Unsolicited`].\n 116|// [impl->REQ-SEC-1]\n...\n 126|/// [`classify_inbound`] later correlates a reply against. Call on every\n*127|/// outbound WAN send (the sending leg's side of the stateful firewall).\n 128|/// Best-effort: a failed write only costs a future reply its exemption (it\n 129|/// will classify unsolicited — the conservative direction).\n 130|// [impl->REQ-SEC-1]\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 136|\n 137|#[cfg(test)]\n...\n 193|\n*194|    // [unit->REQ-SEC-1] the stateful-firewall exemption: a Reply classifies\n 195|    // past a whitelist that would refuse the same node unsolicited — and\n 196|    // classification itself turns on the recorded outbound within the window.\n 197|    #[test]\n## adapter_update.rs\n 7|//! its manifest `[update]` block ([`Update`]); this module turns that directive\n*8|//! into a per-adapter [`AdapterUpdateOutcome`], dispatched by avenue:\n 9|//!\n 10|//! - **`file_pull`** — spt-core controls the bytes, so it *verifies* them: the\n 11|//!   pulled payload is checked against the adapter's **own** Ed25519 key (declared\n...\n 163|/// is complete here. Soft-deregistered adapters are excluded by\n*164|/// [`spt_runtime::registry::registered`].\n 165|// [impl->REQ-INSTALL-4]\n 166|pub fn ripple_registered(adapters_dir: &std::path::Path) -> Vec<(String, AdapterUpdateOutcome)> {\n*167|    let mut set = spt_runtime::registry::registered(adapters_dir);\n 168|    set.sort_by_key(|(r, _)| r.registered_at_ms);\n 169|    set.iter()\n 170|        .map(|(r, m)| {\n...\n 408|        // Register out of name order to prove registration-time ordering.\n*409|        spt_runtime::registry::register(&adapters_dir, &updatable, 100).unwrap();\n*410|        spt_runtime::registry::register(&adapters_dir, &plain, 200).unwrap();\n*411|        spt_runtime::registry::register(&adapters_dir, &gone, 300).unwrap();\n*412|        spt_runtime::registry::deregister(&adapters_dir, \"gone\").unwrap();\n 413|\n 414|        let out = ripple_registered(&adapters_dir);\n 415|        assert_eq!(\n## applyhost.rs\n 34|//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n*35|//!    subscriber while the live brain kept running the OLD code.\n 36|//!\n 37|//! New code runs **immediately** — the supervised respawn lands on the new\n*38|//! executable, no manual bounce, no logon. Readiness-gated promotion +\n 39|//! auto-rollback on a brain that fails to come up is D6.\n 40|// [impl->REQ-UPD-3]\n 41|// [impl->REQ-UPD-4]\n...\n 140|    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*141|    let applied_state = cache.applied_state();\n 142|    if let Some(AppliedRecord::RolledBack {\n 143|        quarantine_version, ..\n*144|    }) = &applied_state\n 145|    {\n 146|        if *quarantine_version == version {\n 147|            return Ok(Prepared::Done(ApplyStagedOutcome::Quarantined { version }));\n...\n 155|    // to a friendly exit-0 no-op instead. Checked before verify/connect: an\n*156|    // already-applied node is up to date regardless of the staged bytes' state.\n 157|    // [impl->REQ-UPDATE-APPLY-ALREADY-APPLIED]\n*158|    if staged_already_applied(applied_state.as_ref(), cache.applied_version(), version) {\n 159|        return Ok(Prepared::Done(ApplyStagedOutcome::AlreadyApplied {\n 160|            version,\n 161|            product_version,\n...\n 279|    // [impl->REQ-UPD-6]\n*280|    let _ = cache.record_applied_state(&AppliedRecord::AppliedPending {\n 281|        version,\n 282|        rollback_binary: aside.to_string_lossy().into_owned(),\n 283|        candidate_started_ms: now_ms(),\n...\n 331|    // level. This replaces the old in-process `apply_brain_only` handoff (the\n*332|    // regression: it re-attached a CLI-side subscriber while the live brain kept\n 333|    // running the OLD code). `classify` already gated this as brain-only, so the\n 334|    // broker never moves; only the brain restarts.\n 335|    //\n...\n 356|/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n*357|/// construction. The recovery verb for wedged brain-held state that previously\n 358|/// needed a full daemon bounce. Nothing is verified, staged, or renamed: the\n 359|/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\n 360|/// rides the existing trial rollback semantics unchanged. `Ok(honored)`\n...\n 412|fn staged_already_applied(\n*413|    applied_state: Option<&AppliedRecord>,\n 414|    applied_version: Option<u64>,\n 415|    staged_version: u64,\n 416|) -> bool {\n 417|    // A trial for this version (or newer) already swapped the binary on disk.\n*418|    if let Some(AppliedRecord::AppliedPending { version, .. }) = applied_state {\n 419|        if *version >= staged_version {\n 420|            return true;\n 421|        }\n...\n 528|    fn staged_already_applied_classifies_up_to_date_vs_newer() {\n*529|        // First-ever apply: nothing applied, no state → proceed.\n 530|        assert!(!staged_already_applied(None, None, 8));\n 531|        // A newer version is staged than what's applied → proceed (apply it).\n 532|        assert!(!staged_already_applied(None, Some(5), 8));\n...\n 567|    // COMPILED image version over IPC: a Brain connected to a live broker gets back\n*568|    // Some(the broker's own CARGO_PKG_VERSION) — the KIND_BROKER_IMAGE dispatch +\n 569|    // reply round-trip end to end (the new-broker leg; the old-broker→None leg rides\n 570|    // the router's `unknown command kind` default, caught client-side as None).\n 571|    #[test]\n...\n 612|        // Stamp the local platform so the apply guard (REQ-UPD-8) lets these\n*613|        // single-release paths through to the class/tamper logic under test;\n 614|        // the unstamped/mismatched refusals get their own dedicated test.\n 615|        cache\n 616|            .stage_single_with_platform(&signed, artifact, current_platform())\n...\n 760|        //    registered platform that is not this host — derived from the\n*761|        //    SUPPORTED_PLATFORMS registry (not a hardcoded win/linux pair), so a\n 762|        //    third platform (musl) is covered with no edit here.\n 763|        let other = crate::release::SUPPORTED_PLATFORMS\n 764|            .iter()\n...\n 766|            .find(|&t| t != current_platform())\n*767|            .expect(\"registry must have a platform other than the current host\");\n 768|        cache\n 769|            .stage_single_with_platform(&signed, b\"CROSS-OS BYTES\", other)\n 770|            .unwrap();\n...\n 950|            matches!(\n*951|                cache.applied_state(),\n 952|                Some(AppliedRecord::AppliedPending { version: 7, .. })\n 953|            ),\n 954|            \"daemonless apply records the open AppliedPending trial\"\n...\n 990|        cache\n*991|            .record_applied_state(&AppliedRecord::AppliedPending {\n 992|                version: 9,\n 993|                rollback_binary: \"spt-binary.old-8\".to_string(),\n 994|                candidate_started_ms: 0,\n...\n 1152|        cache\n*1153|            .record_applied_state(&AppliedRecord::RolledBack {\n 1154|                quarantine_version: 7,\n 1155|                running_version: 6,\n 1156|                rollback_binary: exe.with_file_name(\"spt-binary.old-7\").to_string_lossy().into(),\n...\n 1169|        cache8\n*1170|            .record_applied_state(&AppliedRecord::RolledBack {\n 1171|                quarantine_version: 7,\n 1172|                running_version: 6,\n 1173|                rollback_binary: exe.with_file_name(\"spt-binary.old-7\").to_string_lossy().into(),\n## attach.rs\n*1|//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport\n 2|//! onto a local session for an operator on another node.\n 3|//!\n*4|//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent\n 5|//! [`AttachRecord::Request`]; this module is the target-side pump between that\n*6|//! stream and the local broker-hosted session:\n 7|//!\n 8|//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n 9|//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n...\n 12|//!   journal (a replayed input record cannot double-type);\n*13|//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n 14|//!\n 15|//! ## Exactly-once lives at the EDGES, not the wire sends\n 16|//!\n*17|//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n*18|//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n 19|//! and *discards* everything interleaved — inside a full-duplex pump loop that\n 20|//! eats the operator's `Input` records (both sides starve; found the hard way\n 21|//! in `tests/attach.rs`). Idempotency holds without it because each direction\n...\n 23|//! `Input.op_id`, and the operator's viewport dedups `Output` records by their\n*24|//! session-log `seq` (the render cursor). A post-restart re-serve may\n 25|//! re-transmit output records on the wire — duplicates are dropped at the\n*26|//! render cursor, the same discipline the local `OutputLog` replay uses\n 27|//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n 28|//!\n 29|//! **Compute + files stay here**; the operator is a viewport (CONTEXT\n...\n 32|//! [`access_check`] sees the **handshake-proven** origin from the broker's\n*33|//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of\n 34|//! your own running instance is ungated by default (ADR-0009 default-open);\n 35|//! the D5d whitelist drops in at this same call.\n 36|//!\n 37|//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n*38|//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n 39|//! A restarted target brain re-attaches the session from its output cursor,\n*40|//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n 41|//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42|//! `Output.seq`.\n 43|\n...\n 54|\n*55|/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a\n 56|/// served viewport is a **driver** — attaching wakes the endpoint, the\n 57|/// viewport ending detaches it (active → dormant; the transition echo is\n 58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n...\n 73|    Refused,\n*74|    /// The session's child exited; the `Exit` record was sent and the stream\n 75|    /// finished.\n 76|    Exited(Option<i32>),\n 77|    /// The operator closed its send side (detached); the viewport ended with\n...\n 80|    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n*81|    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream\n 82|    /// finished. The endpoint stays driven (by the taker) — no dormant edge.\n 83|    Displaced,\n 84|}\n...\n 154|/// spins. The helper swallows the `Subscribed` ack its own re-subscribe produces so\n*155|/// the repeated-gap detection is not masked. Non-`Output` events (NetStreamData /\n 156|/// Size / Exit / …) pass through unchanged.\n 157|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 158|/// `gen` is the serve worker's attach generation (ADR-0038 Amendment fix 6):\n...\n 206|/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*207|/// (a stale registry row routed us an attach for something gone → the caller\n 208|/// refuses cleanly, D6).\n 209|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210|pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n...\n 220|/// One attach `Request` — the SINGLE handling path for both a wire-arriving\n*221|/// record and the dispatcher's durable-opener seed (ADR-0038 B; one\n 222|/// implementation so the two entries cannot drift):\n 223|///\n 224|/// - Unsolicited-inbound gate (ADR-0009) under the transport-proven origin;\n...\n 226|///   denial finishes our send side and ends the serve (`Some(Refused)`).\n*227|/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n 228|///   session id wins (the owning node is authoritative for its own table);\n 229|///   the LOCAL path uses the wire `session_id` unchanged.\n 230|/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n...\n 240|    wire: &mut Brain,\n*241|    stream_id: u64,\n 242|    endpoint: &str,\n 243|    origin_node: &str,\n 244|    remote_session: Option<u64>,\n...\n 254|    if access_check(endpoint, origin_node, InboundClass::Unsolicited) == AccessDecision::Deny {\n*255|        wire.net_stream_send(stream_id, &[], None, true)?;\n 256|        return Ok(Some(AttachServeOutcome::Refused));\n 257|    }\n 258|    let sid = remote_session.unwrap_or(session_id);\n## autostart.rs\n 6|//! operator marks an endpoint a **startup default** with `spt endpoint run\n*7|//! --save` (the endpoint analog of the shipped `subnet attach --save`\n 8|//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n 9|//! at start.\n 10|//!\n 11|//! Contract: a replay is best-effort and LOUD — an unresolvable adapter or a\n*12|//! failed launch logs its skip and moves on; nothing here can block daemon\n 13|//! start or the other replays. Each replay is a FRESH session (shape (c)\n 14|//! restore-what-was-up was rejected): the adapter option re-resolves against\n*15|//! the live registry, the session id is freshly minted, and home rides the\n 16|//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n 17|//! recorded or needed.\n 18|\n...\n 49|        let (record, manifest) =\n*50|            match spt_runtime::registry::resolve_option(&adapters_dir, &entry.adapter) {\n 51|                Ok((r, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => {\n 52|                    (r, m)\n 53|                }\n## brain.rs\n*1|//! The brain (B2) — the restartable logic half that attaches to the broker.\n 2|//!\n*3|//! The brain holds all the volatile logic; it may be killed and restarted at\n 4|//! any moment (a routine self-update swaps it; a crash kills it). It connects to\n 5|//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions\n 6|//! (spawn / input / resize / kill), and consumes their output. Two invariants\n...\n 11|//!   brain rehydrates its resume cursor and generation *counter* from prior\n*12|//!   state, but its `gen_start` is `now()`, **never** the stale value from that\n*13|//!   state — a rehydrated `gen_start` would fire time-based discriminators on\n*14|//!   the new generation. [`Brain::handoff`] takes the prior state by value yet\n 15|//!   ignores its `gen_start_ms`.\n 16|//! - **Gapless, exactly-once resume.** The brain tracks `next_seq` — the next\n 17|//!   output sequence it still needs. On handoff it `subscribe`s from that cursor;\n...\n 31|\n*32|use interprocess::local_socket::{SendHalf, Stream};\n 33|\n 34|use crate::codec::{read_frame, write_frame};\n 35|use crate::frame::{Envelope, Role};\n...\n 40|    InputReq,\n*41|    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,\n*42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n*43|    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n*44|    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,\n 45|    ViewerEvictedEvent,\n 46|    KIND_ADAPTER_APPLY, KIND_APPLIED, KIND_BRAIN_RESTART, KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY, KIND_VIEWER_EVICTED,\n 47|    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,\n...\n 50|    KIND_NET_SENT,\n*51|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*52|    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n*53|    KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED,\n*54|    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n 55|    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n*56|    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n 57|};\n 58|use spt_net::net::attach::AttachIntent;\n 59|use crate::transport::{send_hello, LocalSocketTransport};\n...\n 69|\n*70|/// The brain's durable state across a handoff: enough to re-attach gaplessly and\n 71|/// advance the generation. `gen_start_ms` is recorded for observability but is\n 72|/// **deliberately not authoritative on restart** — [`Brain::handoff`] sets a\n 73|/// fresh `gen_start` from `now()` (2.4).\n...\n 78|/// ([`Brain::resume_sessions`] over the broker's cursor-of-record), never this\n*79|/// message. `BrainState` / [`Brain::handoff`] / [`Brain::snapshot`] stay `pub`\n 80|/// solely because the integration tests (separate test target — `#[cfg(test)]`\n 81|/// would hide them) still drive the handoff-frame shape directly; there is **no\n 82|/// non-test caller** (KNOWN-HAZARDS 2.4 grep-clean close-out).\n 83|#[derive(Debug, Clone)]\n*84|pub struct BrainState {\n 85|    /// The session the brain was driving.\n 86|    pub session_id: u64,\n 87|    /// The generation counter (incremented on each handoff).\n...\n 143|    /// (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION) — the bare ack for\n*144|    /// [`Brain::submit_dial`]. The dial's real outcome rides the presence stream\n 145|    /// ([`Self::NetPresence`]), not this frame.\n 146|    NetDialSubmitted,\n*147|    /// A stream open durably landed — fresh or deduped (D4b).\n## brainproc.rs\n 9|//! `spt daemon brain` child. The child connects over the existing broker socket\n*10|//! IPC, signals `ready`, hosts the brain-side logic loops, and idles. D2\n 11|//! migrates the broker-resident loops here one per commit; **D2-1 moved the net\n*12|//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n 13|//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n 14|//! verb. The digest hub serve + seed-control stay broker-side continuity\n 15|//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n...\n 138|/// The `spt daemon brain` child entry. Connect to the broker over the existing\n*139|/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n 140|/// holding the connection and probing broker liveness each heartbeat. A dropped\n 141|/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n 142|/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n...\n 144|///\n*145|/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n 146|/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n 147|/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n 148|/// broker — a brain respawn re-spawns them from the swapped binary. They are\n...\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 161|    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n 162|    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n 163|    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n 164|    // [impl->REQ-DAEMON-STDERR-PERSIST]\n*165|    let _ = crate::stderrlog::install(\"brain\", generation);\n 166|    let name = broker_socket_name();\n 167|    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n 168|    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n 169|    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n*170|    // brain→brain BrainState.generation, which retires in D4), and the reason is\n 171|    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n 172|    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n 173|    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n...\n 183|    // from its delivered cursor, the production replacement for the retired\n*184|    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n 185|    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n 186|    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n 187|    // (the live-agent adapter). A failure here is non-fatal — the brain still\n...\n 225|    // instance's `wake_command` as a supervised watcher child. Relocated from\n*226|    // the broker process to the brain child: shellwake is brain-owned logic\n 227|    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n 228|    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n 229|    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n...\n 247|\n*248|    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n 249|    // brain hosts the SOLE single-flight writer — load persisted index, boot\n 250|    // reconcile in the background (ready-without-warm is preserved because\n 251|    // write_ready already fired above), then drain/debounce/coalesce the\n...\n 254|    // rehydrates from the persisted index (warm start, fingerprint-gated).\n*255|    // [impl->REQ-PROJECT-INDEX-WRITER]\n 256|    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n 257|\n 258|    let mut consumers_started = false;\n...\n 275|            eprintln!(\n*276|                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n 277|            );\n 278|        }\n 279|        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n...\n 288|        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n*289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n 290|        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291|        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292|        // net_status probe above already owns broker-liveness). Self-applies: it drives\n...\n 303|/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n*304|/// semantics that keep a single dispatcher/peer-pump set running.\n 305|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 306|fn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n 307|    net_enabled && !already_started\n...\n 309|\n*310|/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n 311|/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n 312|/// the brain child (restoration D2-1):** both are standalone IPC clients\n 313|/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n*314|/// restartable brain. Each mints its own registry + stop flag and connects\n 315|/// independently; when the brain process exits these in-process threads die with\n 316|/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n 317|/// the wiring the broker boot used to do, `node_hex` now sourced from the\n...\n 321|fn spawn_net_consumers(node_hex: &str) {\n*322|    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n 323|    let stop = Arc::new(AtomicBool::new(false));\n*324|    let _ = crate::dispatch::spawn_dispatcher(\n 325|        broker_socket_name(),\n*326|        Arc::clone(&registry),\n 327|        Arc::clone(&stop),\n 328|    );\n 329|    let cfg = crate::config::DaemonConfig::load();\n 330|    if cfg.peer_pump {\n*331|        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n 332|    }\n 333|}\n 334|\n## broker.rs\n 3|//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n*4|//! would lose continuity on if the logic restarted: the PTY master+writer and\n 5|//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n*6|//! client / listening sockets. It runs **no logic** — no routing, registry,\n 7|//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n 8|//! forward its output to the brain, inject the brain's input, report exit.\n 9|//!\n...\n 13|//! crash). The broker — and every hosted child — must survive that untouched,\n*14|//! and the new brain must re-attach to the output stream **gaplessly and\n 15|//! exactly-once** (Spike #1 made real). Two mechanisms:\n 16|//!\n 17|//! - **Sequenced, buffered output.** Each session's output is appended to an\n*18|//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n*19|//!   in a bounded ring. The current subscriber (a brain connection) receives\n*20|//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n 21|//!   reach the brain in strict `seq` order with no interleave.\n 22|//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n*23|//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n 24|//!   live. The brain dedups by `seq`. Output produced *while no brain was\n 25|//!   attached* sits in the ring and is replayed on the next subscribe — so a\n 26|//!   brain restart loses nothing.\n...\n 28|//! Sessions are therefore **broker-owned and outlive any one connection**: a\n*29|//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n*30|//! output log persist. A session leaves the table only when its child exits\n 31|//! (reaped by the exit waiter) or is explicitly killed.\n 32|// [impl->REQ-DAEMON-2]\n 33|\n...\n 41|\n*42|use interprocess::local_socket::{prelude::*, Stream};\n 43|use spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n 44|\n 45|use spt_net::net::attach::AttachIntent;\n...\n 56|    size_envelope, subscribed_envelope, AdapterApplyReq, BrainRestarted, EndpointInputReq,\n*57|    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n*58|    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n*59|    NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n*60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n 62|    KIND_NET_DIAL_SUBMITTED, KIND_NET_PRESENCE_SUBSCRIBE,\n 63|    KIND_NET_SENT,\n...\n 65|    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n*66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n...\n 76|/// replay frames, command acks, and exit/error events all write through this.\n*77|/// `pub(crate)`: the net host's stream logs (D4b) live-send through the same\n 78|/// handle under the same gate discipline. Round-4\n 79|/// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): no longer a raw\n 80|/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n...\n 84|\n*85|/// Default bounded output-log depth (chunks). Large enough that a brain restart\n 86|/// window never evicts unreplayed output in practice; the bound is what keeps a\n 87|/// disconnected session from buffering without limit.\n*88|const DEFAULT_LOG_CHUNKS: usize = 4096;\n 89|\n 90|/// The viewer-channel depth (frames). A bounded SPSC queue per viewer; when it\n 91|/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n 92|/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n*93|/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n 94|/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n 95|/// owns the replay batch), so a viewer attaching to a busy session is not\n 96|/// spuriously evicted.\n## codec.rs\n 5|//! framing) but generic over any [`Read`]/[`Write`] — the IPC carrier is a\n*6|//! local socket (named pipe / UDS), not a `TcpStream`, and tests exercise it\n 7|//! over in-memory buffers and socket pairs.\n 8|//!\n 9|//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n...\n 75|\n*76|    // [unit->REQ-DAEMON-2] multiple frames stream back-to-back without bleed.\n 77|    #[test]\n 78|    fn back_to_back_frames_decode_independently() {\n 79|        let mut buf: Vec<u8> = Vec::new();\n## config.rs\n 35|/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n*36|/// registry/notif feeds to trusted peers and pulls context syncs from them.\n 37|/// Conservative defaults — every loop is a thin call over an idempotent\n 38|/// driver, so a missed tick costs only latency, never correctness.\n*39|pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40|pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41|pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n 42|pub const DEFAULT_UPDATE_CHECK_PERIOD: Duration = Duration::from_secs(3_600);\n 43|\n*44|/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n 45|/// authored by a node not heard within this window are evicted on the next\n*46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n 47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n*50|pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n 51|\n 52|/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n 53|/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n...\n 67|/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\n*68|/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\n 69|/// design: the adapter option is re-resolved fresh at each replay (an adapter\n 70|/// update between restarts is honored, an uninstalled one loud-skips), and the\n 71|/// endpoint's home subnet is NOT recorded (home is durable on the perch —\n...\n 81|    pub adapter: String,\n*82|    /// The project cwd the harness PTY lands in (captured at `--save`).\n 83|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 84|    pub cwd: Option<String>,\n 85|}\n...\n 104|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*105|    registry_pump_period_ms: Option<u64>,\n 106|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*107|    registry_evict_after_ms: Option<u64>,\n 108|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 109|    notif_pump_period_ms: Option<u64>,\n 110|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n...\n 152|    pub peer_pump: bool,\n*153|    /// Registry feed push cadence (D9-1).\n*154|    pub registry_pump_period: Duration,\n*155|    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows from a\n*156|    /// node not heard within this window are evicted on the next registry\n 157|    /// pump tick — a dead node identity must not poison bare-id resolution\n*158|    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n*159|    pub registry_evict_after: Duration,\n 160|    /// Notif feed push cadence (D9-1).\n 161|    pub notif_pump_period: Duration,\n 162|    /// Context sync pull cadence — the anti-entropy loop (D9-1).\n...\n 178|    pub resources_blurb: Option<String>,\n*179|    /// Subnets whose serve-state STARTS detached (M8 decision 6,\n 180|    /// REQ-SUBNET-5): the daemon's boot reset seeds the live\n 181|    /// `attachment.json` from this list — `spt subnet detach/attach --save`\n 182|    /// writes it. Default empty = every held subnet starts attached.\n...\n 192|    /// REQ-ENDPOINT-AUTOSTART) — `spt endpoint run --save` writes this list.\n*193|    /// Default empty = nothing auto-starts. A replay failure logs loud and\n 194|    /// never blocks daemon start or the other replays.\n 195|    pub startup_endpoints: Vec<StartupEndpoint>,\n 196|    /// The docs server port override (ADR-0036 §4, REQ-DOCS-LOCAL-SERVER).\n...\n 210|            peer_pump: true,         // the subnet self-drives (D9-1)\n*211|            registry_pump_period: DEFAULT_REGISTRY_PUMP_PERIOD,\n*212|            registry_evict_after: DEFAULT_REGISTRY_EVICT_AFTER,\n 213|            notif_pump_period: DEFAULT_NOTIF_PUMP_PERIOD,\n 214|            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,\n 215|            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,\n...\n 270|            peer_pump: file.peer_pump.unwrap_or(d.peer_pump),\n*271|            registry_pump_period: file\n## conn.rs\n 11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n 14|//! connection, so the writer (and every write queued behind the same gate)\n 15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n...\n 74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n*75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n 78|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n...\n 81|/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n*82|/// startup ([`init_log_anchor`], both broker and brain entries) so every\n 83|/// record in one process shares one base; lazily initialized on first stamp\n 84|/// otherwise (tests, in-process brokers) — correlation needs a COMMON base,\n 85|/// not an absolute one.\n...\n 87|\n*88|/// Anchor the monotonic half of [`log_stamp`] at process start — called first\n 89|/// thing by both daemon entries, next to `scrub_identity_env`.\n 90|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*91|pub(crate) fn init_log_anchor() {\n 92|    let _ = MONO_ANCHOR.set(Instant::now());\n 93|}\n 94|\n 95|/// Wall-clock + monotonic stamp for daemon-stderr correlation records\n*96|/// (`wall_ms` = Unix epoch millis; `mono_ms` = millis since [`init_log_anchor`]).\n 97|/// stderr has no per-line timestamps and broker+brain share one file — without\n 98|/// BOTH clocks, interleaved records cannot be ordered (monotonic) or matched\n 99|/// to field incident times (wall).\n 100|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*101|pub(crate) fn log_stamp() -> String {\n 102|    let wall_ms = SystemTime::now()\n 103|        .duration_since(UNIX_EPOCH)\n 104|        .map(|d| d.as_millis())\n...\n 110|/// Attribution-label cap: facts accrete per role attach (a long-lived brain\n*111|/// conn can be controller + viewer + presence subscriber over its life), and\n 112|/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113|const LABEL_CAP: usize = 240;\n 114|\n...\n 138|\n*139|/// The state shared with the watchdog thread (no `Arc<BrokerConn>` cycle —\n 140|/// the watchdog must not keep the conn alive).\n 141|struct Inner {\n 142|    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143|    id: u64,\n*144|    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145|    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n 146|    label: Mutex<String>,\n 147|    /// Latch: the once-per-conn `write-start` lifecycle record fired.\n*148|    first_write_logged: AtomicBool,\n 149|    gate: Mutex<Gate>,\n 150|    gate_cv: Condvar,\n 151|    dog: Mutex<Dog>,\n...\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 175|    /// aborting this handle covers a parked read too. Valid for the whole\n 176|    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n 177|    /// `graveyard` until drop.\n...\n 183|\n*184|/// Recover a mutex guard even if a holder panicked — the state these guards\n 185|/// protect is a plain slot/flag pair, valid on every panic boundary.\n 186|fn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n 187|    match m.lock() {\n...\n 225|        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n*226|        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n 227|    }\n 228|\n 229|    /// Render the once-per-conn retirement record — the F-039 token split\n...\n 430|            label: Mutex::new(String::new()),\n*431|            first_write_logged: AtomicBool::new(false),\n 432|            gate: Mutex::new(Gate { half: Some(half) }),\n 433|            gate_cv: Condvar::new(),\n 434|            dog: Mutex::new(Dog {\n...\n 455|    /// The stable per-physical-conn id — what lifecycle records key on\n*456|    /// (`Arc::ptr_eq` identity does not survive a log line).\n 457|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 458|    pub(crate) fn id(&self) -> u64 {\n 459|        self.inner.id\n...\n 461|\n*462|    /// Accrete an attribution fact onto this conn's label (subscriber role,\n 463|    /// endpoint/session where known — e.g. `role=brain`,\n 464|    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n 465|    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n...\n 545|        // triplets with fresh ids).\n*546|        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n 547|            inner.lifecycle(\"write-start\", \"\");\n 548|        }\n 549|\n...\n 605|/// checked out) so every writer parks AT THE GATE until the pin drops — the\n*606|/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n 607|/// window (the P1c epoch-gate keystone unit relies on it).\n 608|#[cfg(test)]\n 609|pub(crate) struct TestGatePin {\n## consent.rs\n 12|//!   orchestrator should prompt.\n*13|//! - [`most_recently_active`] — resolve that target over the **v1 registry**\n 14|//!   (live perches + their `last_active_ms` recency stamp). This is the\n*15|//!   registry-resolution precursor CONTEXT names; the deferred PresenceChannel\n 16|//!   later generalizes it.\n 17|//!\n 18|//! What C2 deliberately does **not** build: the interactive prompt UX itself.\n...\n 28|/// most-recently-active live perch. Carries enough to address it (id + path) and\n*29|/// the recency that won it the selection (for logging / tie-break audit).\n 30|#[derive(Debug, Clone, PartialEq, Eq)]\n 31|pub struct ConsentTarget {\n 32|    /// The agent/perch id of the most-recently-active live session.\n...\n 71|///\n*72|/// This is the **v1 registry resolution** the consent flow rides — live perches +\n 73|/// their recency stamp, not the deferred PresenceChannel. The resolver core is\n 74|/// shared with the notif engine ([`crate::notif::most_recently_active_where`],\n 75|/// D8b — the subnet-scoped form); consent is the degenerate (unscoped) call.\n## crc_swap.rs\n 128|    // last copy). With the target present the `.old` is provably stale, so this is\n*129|    // safe; in the crash-recovery state the `.old` survives until a SUCCESSFUL commit\n 130|    // repopulates the target, and the NEXT run (target now present) sweeps it.\n 131|    // [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n 132|    for swap in plan {\n## daemon.rs\n 31|use crate::nethost::{NetConfig, NetHost};\n*32|use crate::seedmap::{self, SeedRegistry};\n 33|\n 34|/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\n 35|const START_TIMEOUT: Duration = Duration::from_secs(5);\n...\n 49|        // session's SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID — per-session\n*50|        // identity that is NEVER correct daemon state and would otherwise\n*51|        // leak into every spawn on the node. Scrub first, before any state\n 52|        // is touched or any child can be spawned.\n 53|        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n 54|        spt_runtime::scrub_identity_env();\n...\n 56|        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n*57|        crate::conn::init_log_anchor();\n 58|        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n 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...\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 80|                Err(e) => {\n 81|                    return Err(io::Error::other(format!(\n 82|                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n...\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...\n 120|\n*121|        let reg = Arc::new(SeedRegistry::new());\n 122|\n 123|        // Boot housekeeping (KNOWN-HAZARDS 1.3): sweep stale `index.lock`\n 124|        // wedges out of the git-touching runtime the daemon hosts — the\n*125|        // context store's seed repo + its agent/project worktrees (a crashed\n 126|        // git leaves a 0-byte lock that blocks every later commit forever).\n 127|        // Path composed via the layout authority without opening the full\n 128|        // ContextStore on the boot path; a lock-less (or store-less) home\n...\n 137|\n*138|        // Serve-state boot reset (M8 decision 6, REQ-SUBNET-5): the live\n*139|        // attachment state starts from the config's startup defaults — an\n 140|        // unsaved attach/detach flip deliberately does not survive a daemon\n 141|        // restart (`--save` is the persistence path).\n 142|        // [impl->REQ-SUBNET-5]\n...\n 195|                    });\n*196|                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n 197|                // the BRAIN child (restoration D2-1): they are pure IPC clients,\n 198|                // so they live with the restartable brain and respawn with it.\n 199|                // The broker keeps only the NetHost bring-up and the boot-race\n...\n 217|                    // just bound. Background thread — a saved endpoint that fails\n*218|                    // to come up logs loud and never blocks daemon start, the\n 219|                    // seed channel, or the other replays.\n 220|                    // [impl->REQ-ENDPOINT-AUTOSTART]\n 221|                    thread::spawn(|| {\n...\n 225|                    // docs are up whenever the daemon is. Loopback only; a bind\n*226|                    // failure (port squatted) logs loud and the daemon runs on —\n 227|                    // docs are a degradable surface, never a boot blocker.\n 228|                    // [impl->REQ-DOCS-LOCAL-SERVER]\n 229|                    {\n...\n 253|\n*254|        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n 255|        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n*256|        // delta-stream subscribes against it. Served on a background thread like\n 257|        // the broker. The per-session feed that *populates* the hub is driven by\n*258|        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n 259|        // sessions are spawned through the broker; a bare daemon serves the\n 260|        // channel and answers \"no live digest\" until a session is hosted.\n 261|        let digest_hub = Arc::new(DigestHub::new());\n...\n 282|\n*283|        // The per-shell shell-TUNNEL registry (M11-W3, REQ-SHELL-4) — the opaque\n## deelevate.rs\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...\n 129|        pub gid: u32,\n*130|        /// The invoker's username (`SUDO_USER`), for `USER`/`LOGNAME`.\n 131|        pub user: Option<String>,\n*132|        /// The invoker's home dir (passwd lookup), for `HOME` — the state\n 133|        /// universe anchor (`spt_home` resolves through `$HOME`).\n 134|        pub home: Option<PathBuf>,\n 135|    }\n...\n 274|    /// `sudo spt` decides and every later one is deterministic. `None` =\n*275|    /// not elevated, or no identity to drop to (genuine root login).\n 276|    // [impl->REQ-INSTALL-6]\n 277|    pub fn daemon_target_user() -> Option<SudoInvoker> {\n 278|        if unsafe { libc::geteuid() } != 0 {\n...\n 314|            \"spt: first elevated run — which account should the spt daemon (and its \\\n*315|             state) run under? [{default_name}]: \"\n 316|        );\n 317|        let _ = std::io::stderr().flush();\n 318|        let mut s = String::new();\n...\n 363|        /// Stamp the invoker's identity env onto a [`std::process::Command`]\n*364|        /// (HOME/USER/LOGNAME; SUDO_* removed so the child never re-triggers\n*365|        /// de-elevation logic). uid/gid ride `CommandExt::uid/gid` at the\n 366|        /// call site — std drops supplementary groups when a uid is set.\n 367|        pub fn stamp_env(&self, cmd: &mut std::process::Command) {\n 368|            cmd.env_remove(\"SUDO_UID\")\n...\n 374|            if let Some(user) = &self.user {\n*375|                cmd.env(\"USER\", user).env(\"LOGNAME\", user);\n 376|            }\n 377|        }\n 378|\n...\n 404|                std::env::set_var(\"USER\", user);\n*405|                std::env::set_var(\"LOGNAME\", user);\n 406|            }\n 407|            Ok(())\n 408|        }\n...\n 468|            token: isize,\n*469|            logon_flags: u32,\n 470|            app: *const u16,\n 471|            cmdline: *mut u16,\n 472|            flags: u32,\n...\n 772|                    token,\n*773|                    1, // LOGON_WITH_PROFILE: the desktop user's profile (usually loaded; cheap then)\n 774|                    std::ptr::null(),\n 775|                    cmdline_w.as_mut_ptr(),\n 776|                    flags,\n## digest.rs\n*1|//! The session-digest **log projection** — the daemon-side, on-demand projection\n*2|//! of an endpoint's live activity buffer from its normalized session logs\n*3|//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n 4|//!\n*5|//! ## On-demand projection, not a byte feed\n 6|//!\n 7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n*8|//! on every output chunk. The digest is now a **projection of the same normalized\n 9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n*10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n*11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n 12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n*13|//! read the logs.\n 14|//!\n 15|//! ## Sources (one per endpoint)\n 16|//!\n 17|//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or","truncated":true,"truncatedBy":"bytes","totalLines":1154,"totalBytes":62780,"outputLines":933,"outputBytes":51182,"lastLinePartial":false,"firstLineExceedsLimit":false},"meta":{"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":1154,"totalBytes":62780,"outputLines":933,"outputBytes":51182,"shownRange":{"start":1,"end":933},"nextOffset":934}}},"isError":false,"timestamp":1784271462352}}
{"type":"message","id":"b877e26a","parentId":"b491a5ee","timestamp":"2026-07-17T06:57:42.358Z","message":{"role":"toolResult","toolCallId":"call_5lWxaNTh64JxxrX8MU4mulf6|fc_038f28d3e93bbfd4016a59d26641888196895416dd5a9ee175","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## applyhost.rs\n 513|        let serve = Arc::clone(&broker);\n*514|        let handle = std::thread::spawn(move || {\n 515|            let _ = serve.serve();\n 516|        });\n 517|        ServedBroker {\n## autostart.rs\n 132|    fn replay_bounded(budget: Duration) {\n*133|        let (tx, rx) = mpsc::channel();\n*134|        let handle = std::thread::spawn(move || {\n 135|            replay_startup_endpoints(\"bogus-broker-socket-never-dialed\");\n 136|            let _ = tx.send(());\n 137|        });\n## brain.rs\n 25|use std::io;\n*26|use std::sync::mpsc::{self, Receiver, RecvTimeoutError};\n 27|use std::thread::{self, JoinHandle};\n 28|use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n 29|\n...\n 232|        let (mut recv, send) = stream.split();\n*233|        let (tx, frames) = mpsc::channel();\n 234|        let reader = thread::Builder::new()\n 235|            .name(\"pump-ipc-reader\".to_string())\n 236|            .spawn(move || {\n...\n 1843|    use std::sync::atomic::{AtomicU32, Ordering};\n*1844|    use std::sync::mpsc::{sync_channel, SyncSender};\n 1845|\n 1846|    // [unit->REQ-PUMP-DIAL-FASTFAIL] the peer-reply read budget is DECOUPLED from\n 1847|    // the 30s carrier deadline: a pump-mode reply-read caps at the 10s budget (so\n...\n 1919|        let brain_handle =\n*1920|            thread::spawn(move || Brain::cold_start(&connect_name, 1).expect(\"brain cold_start\"));\n 1921|\n 1922|        let mut server = listener.accept().expect(\"accept the brain\");\n 1923|        crate::transport::recv_hello(&mut server, Role::Brain).expect(\"stub recv_hello\");\n...\n 1931|        let (feed, frames) = sync_channel::<Envelope>(64);\n*1932|        let server_thread = thread::spawn(move || {\n 1933|            while let Ok(env) = frames.recv() {\n 1934|                if write_frame(&mut send, &env).is_err() {\n 1935|                    break;\n## brainproc.rs\n 1110|    let canonical_exe = std::env::current_exe().ok();\n*1111|    let handle = thread::spawn(move || {\n 1112|        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n 1113|        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n 1114|        // readiness-only (no old-gen control plane to drain).\n## broker.rs\n 36|use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\n*37|use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\n 38|use std::sync::{Arc, Mutex};\n 39|use std::thread::{self, JoinHandle};\n 40|use std::time::{Duration, Instant};\n...\n 947|        let writer_blocked = Arc::clone(&write_blocked_since);\n*948|        let writer = thread::spawn(move || {\n 949|            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n 950|        });\n 951|        self.controller = Some(ControllerSink {\n...\n 1137|        let writer =\n*1138|            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n 1139|        self.viewers.insert(\n 1140|            vid,\n 1141|            ViewerSink {\n...\n 1789|        let (tx, rx) = sync_channel::<InputRecord>(input_queue_depth());\n*1790|        let writer = thread::spawn(move || input_writer(session, rx));\n 1791|        Arc::new(InputWriter {\n 1792|            tx,\n 1793|            backpressure: AtomicBool::new(false),\n...\n 1991|            let sup_endpoint = endpoint_id.to_string();\n*1992|            thread::spawn(move || {\n 1993|                run_inject_worker_supervised(\n 1994|                    sup_child,\n 1995|                    sup_faulted,\n...\n 2794|            let me = Arc::clone(self);\n*2795|            thread::spawn(move || {\n 2796|                let _ = me.handle_conn(conn);\n 2797|            });\n 2798|        }\n...\n 2817|        let me = Arc::clone(self);\n*2818|        Ok(thread::spawn(move || {\n 2819|            let _ = me.handle_conn(conn);\n 2820|        }))\n 2821|    }\n...\n 2878|        // stays un-reapable as a live-but-wedged writer even under child output.\n*2879|        let writer = thread::spawn(move || for _ in rx.iter() {});\n 2880|        recover_log(&log).controller = Some(ControllerSink {\n 2881|            tx,\n 2882|            send,\n...\n 3434|        let waiter = Arc::clone(&session);\n*3435|        thread::spawn(move || {\n 3436|            if let Ok(status) = waiter.wait() {\n 3437|                let ev = ExitEvent {\n 3438|                    session_id: id,\n...\n 4756|        let mut client = client;\n*4757|        thread::spawn(move || {\n 4758|            let r = read_frame(&mut client).ok();\n 4759|            let _ = tx.send(r);\n 4760|        });\n...\n 4786|            // Empty initial batch (no size/replay needed for this assertion).\n*4787|            let writer = thread::spawn(move || viewer_writer(w_send, 7, Vec::new(), rx, w_evicted));\n 4788|\n 4789|            // The drain's eviction sequence: mark THEN drop the sink (its `tx`).\n 4790|            evicted.store(true, Ordering::Release);\n...\n 4812|            let w_send = Arc::clone(&send);\n*4813|            let writer = thread::spawn(move || viewer_writer(w_send, 7, Vec::new(), rx, w_evicted));\n 4814|\n 4815|            // A clean teardown: drop tx with the flag still false.\n 4816|            drop(tx);\n...\n 4861|                evicted,\n*4862|                _writer: thread::spawn(|| {}),\n 4863|            },\n 4864|        );\n 4865|\n...\n 4941|            write_blocked_since: Arc::new(Mutex::new(None)),\n*4942|            _writer: thread::spawn(|| {}),\n 4943|        });\n 4944|        // A VIEWER with a generous channel we CAN drain — to prove it stays fed.\n 4945|        let (vsend, _vclient, _vrecv) = controller_socket_pair();\n...\n 4953|                    evicted: Arc::new(AtomicBool::new(false)),\n*4954|                    _writer: thread::spawn(|| {}),\n 4955|                },\n 4956|            );\n 4957|\n...\n 5054|            endpoint: String::new(),\n*5055|            _writer: thread::spawn(|| {}),\n 5056|        };\n 5057|        (w, rx)\n 5058|    }\n...\n 5192|        let f = Arc::clone(&floor);\n*5193|        let _ = thread::spawn(move || {\n 5194|            let _g = f.lock().unwrap();\n 5195|            panic!(\"poison the inject floor\");\n 5196|        })\n...\n 5219|        let s = Arc::clone(&sessions);\n*5220|        let _ = thread::spawn(move || {\n 5221|            let _g = s.lock().unwrap();\n 5222|            panic!(\"poison the sessions map mid-attach\");\n 5223|        })\n...\n 5277|        let l = Arc::clone(&log);\n*5278|        let _ = thread::spawn(move || {\n 5279|            let _g = l.lock().unwrap();\n 5280|            panic!(\"poison the output log mid-append\");\n 5281|        })\n...\n 5950|        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n*5951|        let writer = thread::spawn(|| {});\n 5952|        for _ in 0..1000 {\n 5953|            if writer.is_finished() {\n 5954|                break;\n## daemon.rs\n 192|                    let serve_broker = Arc::clone(&broker);\n*193|                    thread::spawn(move || {\n 194|                        let _ = serve_broker.serve();\n 195|                    });\n 196|                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n...\n 212|                        let broker = Arc::clone(&broker);\n*213|                        thread::spawn(move || net_retry_attach(broker));\n 214|                    }\n 215|                    // Endpoint autostart replay (F-038 W5, REQ-ENDPOINT-AUTOSTART):\n 216|                    // bring saved startup defaults back up against the broker we\n...\n 220|                    // [impl->REQ-ENDPOINT-AUTOSTART]\n*221|                    thread::spawn(|| {\n 222|                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n 223|                    });\n 224|                    // The node-local docs server (ADR-0036 §4): broker-side so\n...\n 263|            let hub = Arc::clone(&digest_hub);\n*264|            thread::spawn(move || {\n 265|                let _ = digesthub::serve_digest_control(&digest_socket_name(), hub);\n 266|            });\n 267|        }\n...\n 277|            let hub = Arc::clone(&drive_hub);\n*278|            thread::spawn(move || {\n 279|                let _ = crate::drivehub::serve_drive_control(&drive_socket_name(), hub);\n 280|            });\n 281|        }\n...\n 294|            let broker = Arc::clone(broker);\n*295|            thread::spawn(move || {\n 296|                let _ = crate::tunnelhub::serve_tunnel_control(\n 297|                    &crate::endpoint::tunnel_socket_name(),\n 298|                    hub,\n...\n 1704|            let serve_name = name.clone();\n*1705|            thread::spawn(move || {\n 1706|                let _ = serve_seed_control(&serve_name, srv);\n 1707|            });\n 1708|            for _ in 0..400 {\n## digesthub.rs\n 28|use std::io::{self, Read, Write};\n*29|use std::sync::mpsc::{channel, Receiver, Sender};\n 30|use std::sync::{Arc, Mutex};\n 31|use std::thread;\n 32|\n...\n 179|        let hub = Arc::clone(&hub);\n*180|        thread::spawn(move || {\n 181|            let _ = handle_conn(conn, &hub);\n 182|        });\n 183|    }\n## dispatch.rs\n 497|            pool.fetch_add(1, Ordering::Relaxed);\n*498|            thread::spawn(move || {\n 499|                let started = Instant::now();\n 500|                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n 501|                pool.fetch_sub(1, Ordering::Relaxed);\n...\n 1201|) -> thread::JoinHandle<()> {\n*1202|    thread::spawn(move || {\n 1203|        let paths = DispatchPaths::production();\n 1204|        if let Err(e) =\n 1205|            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n## docshost.rs\n 252|pub fn start(docs_root: PathBuf, port: u16) -> io::Result<u16> {\n*253|    let (tx, rx) = std::sync::mpsc::channel::<io::Result<u16>>();\n 254|    std::thread::Builder::new()\n 255|        .name(\"spt-docs-server\".to_string())\n 256|        .spawn(move || {\n## drivehub.rs\n 185|        let hub = Arc::clone(&hub);\n*186|        thread::spawn(move || {\n 187|            let _ = handle_conn(conn, &hub);\n 188|        });\n 189|    }\n## effect.rs\n 832|        // \"effect entered\" signal + a \"release\" gate the blocked closure waits on.\n*833|        let (entered_tx, entered_rx) = mpsc::channel::<()>();\n*834|        let (release_tx, release_rx) = mpsc::channel::<()>();\n 835|\n 836|        // Thread 1: a PtyWrite whose effect blocks until we release it.\n 837|        let j1 = Arc::clone(&j);\n*838|        let blocker = std::thread::spawn(move || {\n 839|            j1.apply_once(k(1, 1), EffectKind::PtyWrite, || {\n 840|                entered_tx.send(()).expect(\"signal entered\");\n 841|                // Block inside the effect — the wedge a non-draining PTY induces.\n...\n 855|        let j2 = Arc::clone(&j);\n*856|        let (done_tx, done_rx) = mpsc::channel::<()>();\n*857|        let second = std::thread::spawn(move || {\n 858|            let o = j2\n 859|                .apply_once(k(1, 2), EffectKind::PtyWrite, || Ok(()))\n 860|                .expect(\"apply_once #2\");\n## grants.rs\n 346|                \"aa11\",\n*347|                Some(\"OsNotify\"),\n 348|                Some(target(\"doyle\"))\n 349|            ),\n 350|            GrantDecision::NeedsEscalation(Some(target(\"doyle\")))\n...\n 352|\n*353|        store.grant(CAP_SPAWN_SHELL, \"ling\", \"aa11\", Some(\"OsNotify\"));\n 354|        assert_eq!(\n 355|            decide(\n 356|                &store,\n...\n 359|                \"aa11\",\n*360|                Some(\"OsNotify\"),\n 361|                None\n 362|            ),\n 363|            GrantDecision::Allowed\n...\n 365|\n*366|        store.revoke(CAP_SPAWN_SHELL, \"ling\", \"aa11\", Some(\"OsNotify\"));\n 367|        assert_eq!(\n 368|            decide(\n 369|                &store,\n...\n 372|                \"aa11\",\n*373|                Some(\"OsNotify\"),\n 374|                None\n 375|            ),\n 376|            GrantDecision::NeedsEscalation(None)\n...\n 423|    fn escalation_ask_body_roundtrip() {\n*424|        for a in [ask(Some(\"OsNotify\")), ask(None)] {\n 425|            let parsed = EscalationAsk::parse_body(&a.body());\n 426|            assert_eq!(parsed.as_ref(), Some(&a));\n 427|        }\n...\n 440|    fn escalation_answers_apply() {\n*441|        let a = ask(Some(\"OsNotify\"));\n 442|\n 443|        let mut s = GrantStore::default();\n 444|        assert_eq!(\n...\n 460|        assert_eq!(\n*461|            decide(&s, CAP_SPAWN_SHELL, \"ling\", \"aa11\", Some(\"OsNotify\"), None),\n 462|            GrantDecision::Allowed,\n 463|            \"the written grant satisfies the gate\"\n 464|        );\n...\n 479|\n*480|        author_can_shutdown_grant(&mut s, \"OsNotify-0\", \"aa11\");\n 481|        assert_eq!(\n*482|            decide(&s, CAP_OWNER_SHUTDOWN, \"OsNotify-0\", \"aa11\", None, None),\n 483|            GrantDecision::Allowed,\n 484|            \"can_shutdown pre-consent satisfies the gate\"\n 485|        );\n...\n 510|        // Both idempotent.\n*511|        author_can_shutdown_grant(&mut s, \"OsNotify-0\", \"aa11\");\n 512|        author_wake_spawn_anywhere_grant(&mut s, \"ling\", \"aa11\");\n 513|        assert_eq!(s.rows.len(), 2);\n 514|    }\n...\n 546|\n*547|            let a = ask(Some(\"OsNotify\"));\n 548|            let (row, fired) = produce_escalation_notif(\n 549|                &a,\n 550|                &store,\n## livehost.rs\n 636|        let stop = Arc::clone(&stop);\n*637|        std::thread::spawn(move || {\n 638|            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});\n 639|        })\n 640|    };\n...\n 978|pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n*979|    std::thread::spawn(move || {\n 980|        let set = Arc::new(LiveSet::new());\n 981|        {\n 982|            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n...\n 1481|            let b2 = Arc::clone(&broker);\n*1482|            std::thread::spawn(move || {\n 1483|                let _ = b2.serve();\n 1484|            });\n 1485|            let mut brain = {\n## nethost.rs\n 155|    sub: SharedSend,\n*156|    tx: std::sync::mpsc::SyncSender<Envelope>,\n 157|    /// Writer thread exited (failure or clean drain) — the seat is fully gone\n 158|    /// and a replacement may install (the no-reinstall-until-gone rule,\n 159|    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\n...\n 189|    ) -> SubscriberSeat {\n*190|        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\n 191|        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\n 192|        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\n 193|        let writer_sub = Arc::clone(&sub);\n...\n 195|        let writer_poisoned = Arc::clone(&poisoned);\n*196|        std::thread::spawn(move || {\n 197|            let poison = |halt_reason: &str| {\n 198|                writer_poisoned.store(true, Ordering::Release);\n 199|                if let Some(l) = &lease {\n...\n 237|            Ok(()) => Ok(()),\n*238|            Err(std::sync::mpsc::TrySendError::Full(_)) => Err(SeatFail::Overflow),\n*239|            Err(std::sync::mpsc::TrySendError::Disconnected(_)) => Err(SeatFail::Poisoned),\n 240|        }\n 241|    }\n 242|\n...\n 791|    /// Unused by ordinary (non-retentive) streams.\n*792|    room: Arc<tokio::sync::Notify>,\n 793|    /// Retired = invisible to dispatch eligibility (ADR-0038 A), NOT torn down:\n 794|    /// retire must never sever the transport — the worker's reply send is\n 795|    /// fire-and-forget, so a retire that dropped this entry could overtake the\n...\n 953|    let hex_close = remote_id_hex.clone();\n*954|    tokio::spawn(async move {\n 955|        conn_close.closed().await;\n 956|        sh_close.conns.lock().unwrap().remove(&id);\n 957|        // Retirement (ADR-0038 A): a dead conn's stream rows are transport-dead\n...\n 979|    let hex = remote_id_hex.clone();\n*980|    tokio::spawn(async move {\n 981|        while let Ok((send, recv)) = conn.accept_bi().await {\n 982|            register_stream(\n 983|                &sh,\n...\n 1009|    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n*1010|    let room = Arc::new(tokio::sync::Notify::new());\n 1011|    shared.streams.lock().unwrap().insert(\n 1012|        id,\n 1013|        Arc::new(StreamEntry {\n...\n 1035|    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n*1036|    tokio::spawn(async move {\n 1037|        match recv {\n 1038|            RecvHalf::Quic(mut recv) => loop {\n 1039|                await_ring_room(&log, &room).await;\n...\n 1070|// [impl->REQ-SHELL-4]\n*1071|async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n 1072|    loop {\n 1073|        {\n 1074|            let l = log.lock().unwrap();\n...\n 1228|                    serde_json::to_value(accept_ep.addr()).unwrap_or(serde_json::Value::Null);\n*1229|                tokio::spawn(async move {\n 1230|                    let Ok(connecting) = incoming.accept() else {\n 1231|                        return;\n 1232|                    };\n...\n 2258|            let sent = Arc::clone(&sent);\n*2259|            std::thread::spawn(move || {\n 2260|                for f in &frames {\n 2261|                    host.send_stream(owner, f, false).expect(\"send\");\n 2262|                    sent.fetch_add(1, AtOrd::Relaxed);\n## pairhost.rs\n 204|                let real_addr = endpoint.addr();\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## projwriter.rs\n 622|pub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n*623|    std::thread::spawn(move || {\n 624|        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\n 625|        // untouched): rigs that need a writer-silent daemon set it — the\n 626|        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\n## psyrelay.rs\n 121|                },\n*122|                PsycheIntent::Notify { .. } => {\n 123|                    // The D8c swap: the notify leg IS a notif producer. The\n 124|                    // engine authors the typed envelope (re-stamped\n 125|                    // from=<psyche_id>, daemon-minted notif_id, daemon-supplied\n## seedmap.rs\n 141|        let name = name.to_string();\n*142|        thread::spawn(move || {\n 143|            let _ = handle_conn(conn, &reg, &stop, &name);\n 144|        });\n 145|    }\n...\n 278|    let name = name.to_string();\n*279|    let (tx, rx) = std::sync::mpsc::channel();\n*280|    thread::spawn(move || {\n 281|        let _ = tx.send(ping(&name));\n 282|    });\n 283|    match rx.recv_timeout(bound) {\n...\n 306|    let n = name.to_string();\n*307|    let (tx, rx) = std::sync::mpsc::channel();\n*308|    thread::spawn(move || {\n 309|        let _ = tx.send(connect(&n));\n 310|    });\n 311|    match rx.recv_timeout(bound) {\n...\n 444|        let serve_name = name.clone();\n*445|        std::thread::spawn(move || {\n 446|            let _ = serve_seed_control(&serve_name, srv);\n 447|        });\n 448|        for _ in 0..400 {\n## shellwake.rs\n 526|                let params = *params;\n*527|                std::thread::spawn(move || {\n 528|                    watcher_run(&owlery, &owner, &shell_id, &tokens, &params, &stop, || {\n 529|                        resolve_wake(&owlery, &owner, &shell_id, &adapter_name, &shell)\n 530|                    });\n...\n 553|pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n*554|    std::thread::spawn(move || {\n 555|        let set = Arc::new(WakeSet::new());\n 556|        let params = WakeParams::default();\n 557|        while !stop.load(Ordering::SeqCst) {\n\n## pump/\n### mod.rs\n 1250|) -> thread::JoinHandle<()> {\n*1251|    thread::spawn(move || {\n 1252|        let paths = PumpPaths::production();\n 1253|        supervise_pump(&stop, SUPERVISE_BACKOFF_BASE, || {\n 1254|            run_peer_pump(\n\nShowing files 1-20 of 64+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":87,"fileCount":20,"files":["crates/spt-daemon/src/applyhost.rs","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/daemon.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/drivehub.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/grants.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/pairhost.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/psyrelay.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/shellwake.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":1},{"path":"crates/spt-daemon/src/autostart.rs","count":2},{"path":"crates/spt-daemon/src/brain.rs","count":5},{"path":"crates/spt-daemon/src/brainproc.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/daemon.rs","count":7},{"path":"crates/spt-daemon/src/digesthub.rs","count":2},{"path":"crates/spt-daemon/src/dispatch.rs","count":2},{"path":"crates/spt-daemon/src/docshost.rs","count":1},{"path":"crates/spt-daemon/src/drivehub.rs","count":1},{"path":"crates/spt-daemon/src/effect.rs","count":5},{"path":"crates/spt-daemon/src/grants.rs","count":12},{"path":"crates/spt-daemon/src/livehost.rs","count":3},{"path":"crates/spt-daemon/src/nethost.rs","count":13},{"path":"crates/spt-daemon/src/pairhost.rs","count":1},{"path":"crates/spt-daemon/src/projwriter.rs","count":1},{"path":"crates/spt-daemon/src/psyrelay.rs","count":1},{"path":"crates/spt-daemon/src/pump/mod.rs","count":1},{"path":"crates/spt-daemon/src/seedmap.rs","count":6},{"path":"crates/spt-daemon/src/shellwake.rs","count":2}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## applyhost.rs\n 513│        let serve = Arc::clone(&broker);\n*514│        let handle = std::thread::spawn(move || {\n 515│            let _ = serve.serve();\n 516│        });\n 517│        ServedBroker {\n## autostart.rs\n 132│    fn replay_bounded(budget: Duration) {\n*133│        let (tx, rx) = mpsc::channel();\n*134│        let handle = std::thread::spawn(move || {\n 135│            replay_startup_endpoints(\"bogus-broker-socket-never-dialed\");\n 136│            let _ = tx.send(());\n 137│        });\n## brain.rs\n   25│use std::io;\n  *26│use std::sync::mpsc::{self, Receiver, RecvTimeoutError};\n   27│use std::thread::{self, JoinHandle};\n   28│use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n   29│\n     │...\n  232│        let (mut recv, send) = stream.split();\n *233│        let (tx, frames) = mpsc::channel();\n  234│        let reader = thread::Builder::new()\n  235│            .name(\"pump-ipc-reader\".to_string())\n  236│            .spawn(move || {\n     │...\n 1843│    use std::sync::atomic::{AtomicU32, Ordering};\n*1844│    use std::sync::mpsc::{sync_channel, SyncSender};\n 1845│\n 1846│    // [unit->REQ-PUMP-DIAL-FASTFAIL] the peer-reply read budget is DECOUPLED from\n 1847│    // the 30s carrier deadline: a pump-mode reply-read caps at the 10s budget (so\n     │...\n 1919│        let brain_handle =\n*1920│            thread::spawn(move || Brain::cold_start(&connect_name, 1).expect(\"brain cold_start\"));\n 1921│\n 1922│        let mut server = listener.accept().expect(\"accept the brain\");\n 1923│        crate::transport::recv_hello(&mut server, Role::Brain).expect(\"stub recv_hello\");\n     │...\n 1931│        let (feed, frames) = sync_channel::<Envelope>(64);\n*1932│        let server_thread = thread::spawn(move || {\n 1933│            while let Ok(env) = frames.recv() {\n 1934│                if write_frame(&mut send, &env).is_err() {\n 1935│                    break;\n## brainproc.rs\n 1110│    let canonical_exe = std::env::current_exe().ok();\n*1111│    let handle = thread::spawn(move || {\n 1112│        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n 1113│        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n 1114│        // readiness-only (no old-gen control plane to drain).\n## broker.rs\n   36│use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\n  *37│use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\n   38│use std::sync::{Arc, Mutex};\n   39│use std::thread::{self, JoinHandle};\n   40│use std::time::{Duration, Instant};\n     │...\n  947│        let writer_blocked = Arc::clone(&write_blocked_since);\n *948│        let writer = thread::spawn(move || {\n  949│            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n  950│        });\n  951│        self.controller = Some(ControllerSink {\n     │...\n 1137│        let writer =\n*1138│            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));\n 1139│        self.viewers.insert(\n 1140│            vid,\n 1141│            ViewerSink {\n     │...\n 1789│        let (tx, rx) = sync_channel::<InputRecord>(input_queue_depth());\n*1790│        let writer = thread::spawn(move || input_writer(session, rx));\n 1791│        Arc::new(InputWriter {\n 1792│            tx,\n 1793│            backpressure: AtomicBool::new(false),\n     │...\n 1991│            let sup_endpoint = endpoint_id.to_string();\n*1992│            thread::spawn(move || {\n 1993│                run_inject_worker_supervised(\n 1994│                    sup_child,\n 1995│                    sup_faulted,\n     │...\n 2794│            let me = Arc::clone(self);\n*2795│            thread::spawn(move || {\n 2796│                let _ = me.handle_conn(conn);\n 2797│            });\n 2798│        }\n     │...\n 2817│        let me = Arc::clone(self);\n*2818│        Ok(thread::spawn(move || {\n 2819│            let _ = me.handle_conn(conn);\n 2820│        }))\n 2821│    }\n     │...\n 2878│        // stays un-reapable as a live-but-wedged writer even under child output.\n*2879│        let writer = thread::spawn(move || for _ in rx.iter() {});\n 2880│        recover_log(&log).controller = Some(ControllerSink {\n 2881│            tx,\n 2882│            send,\n     │...\n 3434│        let waiter = Arc::clone(&session);\n*3435│        thread::spawn(move || {\n 3436│            if let Ok(status) = waiter.wait() {\n 3437│                let ev = ExitEvent {\n 3438│                    session_id: id,\n     │...\n 4756│        let mut client = client;\n*4757│        thread::spawn(move || {\n 4758│            let r = read_frame(&mut client).ok();\n 4759│            let _ = tx.send(r);\n 4760│        });\n     │...\n 4786│            // Empty initial batch (no size/replay needed for this assertion).\n*4787│            let writer = thread::spawn(move || viewer_writer(w_send, 7, Vec::new(), rx, w_evicted));\n 4788│\n 4789│            // The drain's eviction sequence: mark THEN drop the sink (its `tx`).\n 4790│            evicted.store(true, Ordering::Release);\n     │...\n 4812│            let w_send = Arc::clone(&send);\n*4813│            let writer = thread::spawn(move || viewer_writer(w_send, 7, Vec::new(), rx, w_evicted));\n 4814│\n 4815│            // A clean teardown: drop tx with the flag still false.\n 4816│            drop(tx);\n     │...\n 4861│                evicted,\n*4862│                _writer: thread::spawn(|| {}),\n 4863│            },\n 4864│        );\n 4865│\n     │...\n 4941│            write_blocked_since: Arc::new(Mutex::new(None)),\n*4942│            _writer: thread::spawn(|| {}),\n 4943│        });\n 4944│        // A VIEWER with a generous channel we CAN drain — to prove it stays fed.\n 4945│        let (vsend, _vclient, _vrecv) = controller_socket_pair();\n     │...\n 4953│                    evicted: Arc::new(AtomicBool::new(false)),\n*4954│                    _writer: thread::spawn(|| {}),\n 4955│                },\n 4956│            );\n 4957│\n     │...\n 5054│            endpoint: String::new(),\n*5055│            _writer: thread::spawn(|| {}),\n 5056│        };\n 5057│        (w, rx)\n 5058│    }\n     │...\n 5192│        let f = Arc::clone(&floor);\n*5193│        let _ = thread::spawn(move || {\n 5194│            let _g = f.lock().unwrap();\n 5195│            panic!(\"poison the inject floor\");\n 5196│        })\n     │...\n 5219│        let s = Arc::clone(&sessions);\n*5220│        let _ = thread::spawn(move || {\n 5221│            let _g = s.lock().unwrap();\n 5222│            panic!(\"poison the sessions map mid-attach\");\n 5223│        })\n     │...\n 5277│        let l = Arc::clone(&log);\n*5278│        let _ = thread::spawn(move || {\n 5279│            let _g = l.lock().unwrap();\n 5280│            panic!(\"poison the output log mid-append\");\n 5281│        })\n     │...\n 5950│        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n*5951│        let writer = thread::spawn(|| {});\n 5952│        for _ in 0..1000 {\n 5953│            if writer.is_finished() {\n 5954│                break;\n## daemon.rs\n  192│                    let serve_broker = Arc::clone(&broker);\n *193│                    thread::spawn(move || {\n  194│                        let _ = serve_broker.serve();\n  195│                    });\n  196│                // Inbound net dispatch + outbound peer pump (D9-1) now run in\n     │...\n  212│                        let broker = Arc::clone(&broker);\n *213│                        thread::spawn(move || net_retry_attach(broker));\n  214│                    }\n  215│                    // Endpoint autostart replay (F-038 W5, REQ-ENDPOINT-AUTOSTART):\n  216│                    // bring saved startup defaults back up against the broker we\n     │...\n  220│                    // [impl->REQ-ENDPOINT-AUTOSTART]\n *221│                    thread::spawn(|| {\n  222│                        crate::autostart::replay_startup_endpoints(&broker_socket_name());\n  223│                    });\n  224│                    // The node-local docs server (ADR-0036 §4): broker-side so\n     │...\n  263│            let hub = Arc::clone(&digest_hub);\n *264│            thread::spawn(move || {\n  265│                let _ = digesthub::serve_digest_control(&digest_socket_name(), hub);\n  266│            });\n  267│        }\n     │...\n  277│            let hub = Arc::clone(&drive_hub);\n *278│            thread::spawn(move || {\n  279│                let _ = crate::drivehub::serve_drive_control(&drive_socket_name(), hub);\n  280│            });\n  281│        }\n     │...\n  294│            let broker = Arc::clone(broker);\n *295│            thread::spawn(move || {\n  296│                let _ = crate::tunnelhub::serve_tunnel_control(\n  297│                    &crate::endpoint::tunnel_socket_name(),\n  298│                    hub,\n     │...\n 1704│            let serve_name = name.clone();\n*1705│            thread::spawn(move || {\n 1706│                let _ = serve_seed_control(&serve_name, srv);\n 1707│            });\n 1708│            for _ in 0..400 {\n## digesthub.rs\n  28│use std::io::{self, Read, Write};\n *29│use std::sync::mpsc::{channel, Receiver, Sender};\n  30│use std::sync::{Arc, Mutex};\n  31│use std::thread;\n  32│\n    │...\n 179│        let hub = Arc::clone(&hub);\n*180│        thread::spawn(move || {\n 181│            let _ = handle_conn(conn, &hub);\n 182│        });\n 183│    }\n## dispatch.rs\n  497│            pool.fetch_add(1, Ordering::Relaxed);\n *498│            thread::spawn(move || {\n  499│                let started = Instant::now();\n  500│                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n  501│                pool.fetch_sub(1, Ordering::Relaxed);\n     │...\n 1201│) -> thread::JoinHandle<()> {\n*1202│    thread::spawn(move || {\n 1203│        let paths = DispatchPaths::production();\n 1204│        if let Err(e) =\n 1205│            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n## docshost.rs\n 252│pub fn start(docs_root: PathBuf, port: u16) -> io::Result<u16> {\n*253│    let (tx, rx) = std::sync::mpsc::channel::<io::Result<u16>>();\n 254│    std::thread::Builder::new()\n 255│        .name(\"spt-docs-server\".to_string())\n 256│        .spawn(move || {\n## drivehub.rs\n 185│        let hub = Arc::clone(&hub);\n*186│        thread::spawn(move || {\n 187│            let _ = handle_conn(conn, &hub);\n 188│        });\n 189│    }\n## effect.rs\n 832│        // \"effect entered\" signal + a \"release\" gate the blocked closure waits on.\n*833│        let (entered_tx, entered_rx) = mpsc::channel::<()>();\n*834│        let (release_tx, release_rx) = mpsc::channel::<()>();\n 835│\n 836│        // Thread 1: a PtyWrite whose effect blocks until we release it.\n 837│        let j1 = Arc::clone(&j);\n*838│        let blocker = std::thread::spawn(move || {\n 839│            j1.apply_once(k(1, 1), EffectKind::PtyWrite, || {\n 840│                entered_tx.send(()).expect(\"signal entered\");\n 841│                // Block inside the effect — the wedge a non-draining PTY induces.\n    │...\n 855│        let j2 = Arc::clone(&j);\n*856│        let (done_tx, done_rx) = mpsc::channel::<()>();\n*857│        let second = std::thread::spawn(move || {\n 858│            let o = j2\n 859│                .apply_once(k(1, 2), EffectKind::PtyWrite, || Ok(()))\n 860│                .expect(\"apply_once #2\");\n## grants.rs\n 346│                \"aa11\",\n*347│                Some(\"OsNotify\"),\n 348│                Some(target(\"doyle\"))\n 349│            ),\n 350│            GrantDecision::NeedsEscalation(Some(target(\"doyle\")))\n    │...\n 352│\n*353│        store.grant(CAP_SPAWN_SHELL, \"ling\", \"aa11\", Some(\"OsNotify\"));\n 354│        assert_eq!(\n 355│            decide(\n 356│                &store,\n    │...\n 359│                \"aa11\",\n*360│                Some(\"OsNotify\"),\n 361│                None\n 362│            ),\n 363│            GrantDecision::Allowed\n    │...\n 365│\n*366│        store.revoke(CAP_SPAWN_SHELL, \"ling\", \"aa11\", Some(\"OsNotify\"));\n 367│        assert_eq!(\n 368│            decide(\n 369│                &store,\n    │...\n 372│                \"aa11\",\n*373│                Some(\"OsNotify\"),\n 374│                None\n 375│            ),\n 376│            GrantDecision::NeedsEscalation(None)\n    │...\n 423│    fn escalation_ask_body_roundtrip() {\n*424│        for a in [ask(Some(\"OsNotify\")), ask(None)] {\n 425│            let parsed = EscalationAsk::parse_body(&a.body());\n 426│            assert_eq!(parsed.as_ref(), Some(&a));\n 427│        }\n    │...\n 440│    fn escalation_answers_apply() {\n*441│        let a = ask(Some(\"OsNotify\"));\n 442│\n 443│        let mut s = GrantStore::default();\n 444│        assert_eq!(\n    │...\n 460│        assert_eq!(\n*461│            decide(&s, CAP_SPAWN_SHELL, \"ling\", \"aa11\", Some(\"OsNotify\"), None),\n 462│            GrantDecision::Allowed,\n 463│            \"the written grant satisfies the gate\"\n 464│        );\n    │...\n 479│\n*480│        author_can_shutdown_grant(&mut s, \"OsNotify-0\", \"aa11\");\n 481│        assert_eq!(\n*482│            decide(&s, CAP_OWNER_SHUTDOWN, \"OsNotify-0\", \"aa11\", None, None),\n 483│            GrantDecision::Allowed,\n 484│            \"can_shutdown pre-consent satisfies the gate\"\n 485│        );\n    │...\n 510│        // Both idempotent.\n*511│        author_can_shutdown_grant(&mut s, \"OsNotify-0\", \"aa11\");\n 512│        author_wake_spawn_anywhere_grant(&mut s, \"ling\", \"aa11\");\n 513│        assert_eq!(s.rows.len(), 2);\n 514│    }\n    │...\n 546│\n*547│            let a = ask(Some(\"OsNotify\"));\n 548│            let (row, fired) = produce_escalation_notif(\n 549│                &a,\n 550│                &store,\n## livehost.rs\n  636│        let stop = Arc::clone(&stop);\n *637│        std::thread::spawn(move || {\n  638│            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});\n  639│        })\n  640│    };\n     │...\n  978│pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n *979│    std::thread::spawn(move || {\n  980│        let set = Arc::new(LiveSet::new());\n  981│        {\n  982│            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n     │...\n 1481│            let b2 = Arc::clone(&broker);\n*1482│            std::thread::spawn(move || {\n 1483│                let _ = b2.serve();\n 1484│            });\n 1485│            let mut brain = {\n## nethost.rs\n  155│    sub: SharedSend,\n *156│    tx: std::sync::mpsc::SyncSender<Envelope>,\n  157│    /// Writer thread exited (failure or clean drain) — the seat is fully gone\n  158│    /// and a replacement may install (the no-reinstall-until-gone rule,\n  159│    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\n     │...\n  189│    ) -> SubscriberSeat {\n *190│        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\n  191│        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\n  192│        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\n  193│        let writer_sub = Arc::clone(&sub);\n     │...\n  195│        let writer_poisoned = Arc::clone(&poisoned);\n *196│        std::thread::spawn(move || {\n  197│            let poison = |halt_reason: &str| {\n  198│                writer_poisoned.store(true, Ordering::Release);\n  199│                if let Some(l) = &lease {\n     │...\n  237│            Ok(()) => Ok(()),\n *238│            Err(std::sync::mpsc::TrySendError::Full(_)) => Err(SeatFail::Overflow),\n *239│            Err(std::sync::mpsc::TrySendError::Disconnected(_)) => Err(SeatFail::Poisoned),\n  240│        }\n  241│    }\n  242│\n     │...\n  791│    /// Unused by ordinary (non-retentive) streams.\n *792│    room: Arc<tokio::sync::Notify>,\n  793│    /// Retired = invisible to dispatch eligibility (ADR-0038 A), NOT torn down:\n  794│    /// retire must never sever the transport — the worker's reply send is\n  795│    /// fire-and-forget, so a retire that dropped this entry could overtake the\n     │...\n  953│    let hex_close = remote_id_hex.clone();\n *954│    tokio::spawn(async move {\n  955│        conn_close.closed().await;\n  956│        sh_close.conns.lock().unwrap().remove(&id);\n  957│        // Retirement (ADR-0038 A): a dead conn's stream rows are transport-dead\n     │...\n  979│    let hex = remote_id_hex.clone();\n *980│    tokio::spawn(async move {\n  981│        while let Ok((send, recv)) = conn.accept_bi().await {\n  982│            register_stream(\n  983│                &sh,\n     │...\n 1009│    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n*1010│    let room = Arc::new(tokio::sync::Notify::new());\n 1011│    shared.streams.lock().unwrap().insert(\n 1012│        id,\n 1013│        Arc::new(StreamEntry {\n     │...\n 1035│    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n*1036│    tokio::spawn(async move {\n 1037│        match recv {\n 1038│            RecvHalf::Quic(mut recv) => loop {\n 1039│                await_ring_room(&log, &room).await;\n     │...\n 1070│// [impl->REQ-SHELL-4]\n*1071│async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n 1072│    loop {\n 1073│        {\n 1074│            let l = log.lock().unwrap();\n     │...\n 1228│                    serde_json::to_value(accept_ep.addr()).unwrap_or(serde_json::Value::Null);\n*1229│                tokio::spawn(async move {\n 1230│                    let Ok(connecting) = incoming.accept() else {\n 1231│                        return;\n 1232│                    };\n     │...\n 2258│            let sent = Arc::clone(&sent);\n*2259│            std::thread::spawn(move || {\n 2260│                for f in &frames {\n 2261│                    host.send_stream(owner, f, false).expect(\"send\");\n 2262│                    sent.fetch_add(1, AtOrd::Relaxed);\n## pairhost.rs\n 204│                let real_addr = endpoint.addr();\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## projwriter.rs\n 622│pub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n*623│    std::thread::spawn(move || {\n 624│        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\n 625│        // untouched): rigs that need a writer-silent daemon set it — the\n 626│        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\n## psyrelay.rs\n 121│                },\n*122│                PsycheIntent::Notify { .. } => {\n 123│                    // The D8c swap: the notify leg IS a notif producer. The\n 124│                    // engine authors the typed envelope (re-stamped\n 125│                    // from=<psyche_id>, daemon-minted notif_id, daemon-supplied\n## seedmap.rs\n 141│        let name = name.to_string();\n*142│        thread::spawn(move || {\n 143│            let _ = handle_conn(conn, &reg, &stop, &name);\n 144│        });\n 145│    }\n    │...\n 278│    let name = name.to_string();\n*279│    let (tx, rx) = std::sync::mpsc::channel();\n*280│    thread::spawn(move || {\n 281│        let _ = tx.send(ping(&name));\n 282│    });\n 283│    match rx.recv_timeout(bound) {\n    │...\n 306│    let n = name.to_string();\n*307│    let (tx, rx) = std::sync::mpsc::channel();\n*308│    thread::spawn(move || {\n 309│        let _ = tx.send(connect(&n));\n 310│    });\n 311│    match rx.recv_timeout(bound) {\n    │...\n 444│        let serve_name = name.clone();\n*445│        std::thread::spawn(move || {\n 446│            let _ = serve_seed_control(&serve_name, srv);\n 447│        });\n 448│        for _ in 0..400 {\n## shellwake.rs\n 526│                let params = *params;\n*527│                std::thread::spawn(move || {\n 528│                    watcher_run(&owlery, &owner, &shell_id, &tokens, &params, &stop, || {\n 529│                        resolve_wake(&owlery, &owner, &shell_id, &adapter_name, &shell)\n 530│                    });\n    │...\n 553│pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n*554│    std::thread::spawn(move || {\n 555│        let set = Arc::new(WakeSet::new());\n 556│        let params = WakeParams::default();\n 557│        while !stop.load(Ordering::SeqCst) {\n\n## pump/\n### mod.rs\n 1250│) -> thread::JoinHandle<()> {\n*1251│    thread::spawn(move || {\n 1252│        let paths = PumpPaths::production();\n 1253│        supervise_pump(&stop, SUPERVISE_BACKOFF_BASE, || {\n 1254│            run_peer_pump("},"isError":false,"timestamp":1784271462358}}
{"type":"message","id":"97eee97e","parentId":"b877e26a","timestamp":"2026-07-17T06:57:42.367Z","message":{"role":"toolResult","toolCallId":"call_Mvb47KlTjKvnWPmoc4wD5YER|fc_038f28d3e93bbfd4016a59d26641808196a95b8eceecd712d9","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## adapter_update.rs\n 389|            let src = tmp.path().join(format!(\"{name}-src\"));\n*390|            std::fs::create_dir_all(&src).unwrap();\n*391|            std::fs::write(src.join(\"manifest.toml\"), body).unwrap();\n 392|            src\n 393|        };\n 394|        let plain = seed(\n## applyhost.rs\n 261|    let aside = swap_aside_path(exe_path, version);\n*262|    let _ = std::fs::remove_file(&aside); // stale prior apply of this version\n*263|    std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;\n 264|    if let Err(e) = land_artifact(exe_path, artifact) {\n*265|        let _ = std::fs::rename(&aside, exe_path); // roll back — never half-swapped\n 266|        return Err(format!(\"land new binary (rolled back): {e}\"));\n 267|    }\n 268|    // The binary IS swapped from here on — but DO NOT claim `applied` yet\n...\n 444|        use std::os::unix::fs::PermissionsExt;\n*445|        std::fs::set_permissions(exe_path, std::fs::Permissions::from_mode(0o755))?;\n 446|    }\n 447|    Ok(())\n 448|}\n...\n 625|            .collect();\n*626|        std::fs::write(\n 627|            &keys,\n 628|            serde_json::json!({\"keys\": {\"k\": pub_hex}, \"channel\": \"stable\"}).to_string(),\n 629|        )\n...\n 687|            .collect();\n*688|        std::fs::write(\n 689|            &keys,\n 690|            serde_json::json!({\"keys\": {\"k\": pub_hex}, \"channel\": \"stable\"}).to_string(),\n 691|        )\n...\n 723|            .collect();\n*724|        std::fs::write(\n 725|            &keys,\n 726|            serde_json::json!({\"keys\": {\"k\": pub_hex}, \"channel\": \"stable\"}).to_string(),\n 727|        )\n...\n 743|        let exe = dir.path().join(\"spt-binary\");\n*744|        std::fs::write(&exe, b\"OLD\").unwrap();\n 745|\n 746|        let cache = ReleaseCache::open(&dir.path().join(\"releases\"));\n 747|        let (signed, keys) = sign_single(dir.path(), 7, b\"CROSS-OS BYTES\");\n...\n 756|        );\n*757|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 758|\n 759|        // 2. Single stamped for the OTHER OS refuses too. \"Other\" = any\n 760|        //    registered platform that is not this host — derived from the\n...\n 775|        );\n*776|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 777|\n 778|        // 3. Stamped for THIS platform: the single applies (the legitimate\n 779|        //    origin-fetched-local path).\n...\n 787|        );\n*788|        assert_eq!(std::fs::read(&exe).unwrap(), b\"CROSS-OS BYTES\");\n 789|    }\n 790|\n 791|    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] [unit->REQ-UPD-3] the planned\n...\n 889|        let exe = dir.path().join(\"spt-binary\");\n*890|        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n 891|        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n 892|\n 893|        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"apply ok\");\n...\n 898|        assert_eq!(\n*899|            std::fs::read(&exe).unwrap(),\n 900|            b\"NEW BINARY BYTES\",\n 901|            \"artifact landed\"\n 902|        );\n 903|        assert_eq!(\n*904|            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n 905|            b\"OLD BINARY\",\n 906|            \"the outgoing binary stepped aside\"\n 907|        );\n...\n 927|        let exe = dir.path().join(\"spt-binary\");\n*928|        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n 929|        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n 930|\n 931|        // No served_broker — deliberately. A daemonless apply must not need one.\n...\n 937|        assert_eq!(\n*938|            std::fs::read(&exe).unwrap(),\n 939|            b\"NEW BINARY BYTES\",\n 940|            \"artifact landed without any broker handoff\"\n 941|        );\n 942|        assert_eq!(\n*943|            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n 944|            b\"OLD BINARY\",\n 945|            \"the outgoing binary stepped aside\"\n 946|        );\n...\n 966|        let exe = dir.path().join(\"spt-binary\");\n*967|        std::fs::write(&exe, b\"OLD\").unwrap();\n 968|\n 969|        // Nothing staged.\n 970|        let empty = ReleaseCache::open(&dir.path().join(\"releases\"));\n...\n 984|        );\n*985|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 986|\n 987|        // Already-applied: idempotent no-op, never re-renames an already-swapped exe.\n 988|        // A still-open trial that already landed this version's bytes marks it applied.\n...\n 1002|        );\n*1003|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1004|    }\n 1005|\n 1006|    // [unit->REQ-UPD-6] apply-time re-verification handles platform-targeted\n## brainproc.rs\n 382|    let exe = std::env::current_exe().ok()?;\n*383|    let bytes = std::fs::read(exe).ok()?;\n 384|    Some(crate::release::sha256_hex(&bytes))\n 385|}\n 386|\n...\n 418|pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n*419|    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n 420|}\n 421|\n 422|/// The supervisor's next backoff after a brain death: a healthy run (≥\n...\n 541|    fn clear_ready(&self) {\n*542|        let _ = std::fs::remove_file(brain_ready_path());\n 543|    }\n 544|    fn record_promoted(&self, version: u64) {\n 545|        let cache = self.cache();\n...\n 563|    fn ready_exe_hash(&self) -> Option<String> {\n*564|        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n 565|        parse_ready_exe_hash(&body)\n 566|    }\n 567|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n...\n 1391|        assert_eq!(ready_generation_at(&path), None, \"absent file → not-ready\");\n*1392|        std::fs::write(&path, serde_json::json!({\"pid\": 1, \"generation\": 42}).to_string())\n 1393|            .unwrap();\n 1394|        assert_eq!(ready_generation_at(&path), Some(42));\n 1395|    }\n## broker.rs\n 2028|fn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n*2029|    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n 2030|    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n 2031|    let decl = manifest.message_idle_translation_binary.as_ref()?;\n 2032|    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n...\n 6244|                        [message-idle-translation-binary]\\npath = \\\"some/bin\\\"\\n\";\n*6245|        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6246|        assert_eq!(\n 6247|            read_translation_command(dir.path()),\n 6248|            Some(vec![\"some/bin\".to_string()]),\n...\n 6258|                         command = \\\"{adapter_dir}/claude-spt translate\\\"\\n\";\n*6259|        std::fs::write(dir2.path().join(\"manifest.toml\"), manifest2).unwrap();\n 6260|        let argv = read_translation_command(dir2.path()).expect(\"command resolves\");\n 6261|        assert_eq!(argv.len(), 2, \"program + one arg: {argv:?}\");\n 6262|        assert!(\n...\n 6281|                        min_spt_core_version=\\\"1\\\"\\n\";\n*6282|        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6283|        assert_eq!(\n 6284|            read_translation_command(dir.path()),\n 6285|            None,\n## config.rs\n 241|    pub fn load_from(path: &Path) -> Self {\n*242|        let Ok(raw) = std::fs::read_to_string(path) else {\n 243|            return Self::default();\n 244|        };\n 245|        let file: DaemonConfigFile = match serde_json::from_str(&raw) {\n...\n 328|            .map_err(|e| std::io::Error::other(e.to_string()))?;\n*329|        std::fs::write(path, json)\n 330|    }\n 331|\n 332|    /// Upsert a startup default at the canonical path (`endpoint run --save`,\n...\n 369|        let path = dir.path().join(\"daemon.json\");\n*370|        std::fs::write(&path, r#\"{\"pulse_period_ms\": 250}\"#).unwrap();\n 371|\n 372|        let cfg = DaemonConfig::load_from(&path);\n 373|        assert_eq!(cfg.pulse_period, Duration::from_millis(250));\n...\n 385|        let path = dir.path().join(\"daemon.json\");\n*386|        std::fs::write(&path, \"not json {{{\").unwrap();\n 387|        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());\n 388|    }\n 389|\n...\n 435|        let path = dir.path().join(\"daemon.json\");\n*436|        std::fs::write(&path, r#\"{\"detached_subnets\": [\"work\"]}\"#).unwrap();\n 437|        assert_eq!(\n 438|            DaemonConfig::load_from(&path).detached_subnets,\n 439|            vec![\"work\".to_string()]\n...\n 458|        let path = dir.path().join(\"daemon.json\");\n*459|        std::fs::write(&path, r#\"{\"auto_suspend_after_ms\": 300000}\"#).unwrap();\n 460|        assert_eq!(\n 461|            DaemonConfig::load_from(&path).auto_suspend_after_ms,\n 462|            Some(300_000)\n...\n 479|        let path = dir.path().join(\"daemon.json\");\n*480|        std::fs::write(&path, r#\"{\"full_auto_update\": true}\"#).unwrap();\n 481|        assert!(\n 482|            DaemonConfig::load_from(&path).full_auto_update,\n 483|            \"explicit opt-in honored\"\n...\n 499|        let path = dir.path().join(\"daemon.json\");\n*500|        std::fs::write(&path, r#\"{\"relay\": \"disabled\"}\"#).unwrap();\n 501|        assert_eq!(DaemonConfig::load_from(&path).relay, RelayChoice::Disabled);\n 502|\n 503|        // An unrecognized future value degrades to the default, never wedges.\n*504|        std::fs::write(&path, r#\"{\"relay\": \"quantum-mesh\"}\"#).unwrap();\n 505|        assert_eq!(DaemonConfig::load_from(&path).relay, RelayChoice::N0);\n 506|\n 507|        let cfg = DaemonConfig {\n...\n 518|        let path = dir.path().join(\"daemon.json\");\n*519|        std::fs::write(&path, r#\"{\"crash_grace_ms\": 500}\"#).unwrap();\n 520|\n 521|        let cfg = DaemonConfig::load_from(&path);\n 522|        assert_eq!(cfg.crash_grace, Duration::from_millis(500));\n...\n 576|        // key) loads as empty — forward/back-compat, never a parse failure.\n*577|        std::fs::write(&path, r#\"{\"pulse_period_ms\": 5000}\"#).unwrap();\n 578|        assert!(\n 579|            DaemonConfig::load_from(&path).startup_endpoints.is_empty(),\n 580|            \"a file without the key back-loads as empty, not an error\"\n## consent.rs\n 92|        let p = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);\n*93|        std::fs::create_dir_all(&p).unwrap();\n 94|        write_info(&p, &InfoJson::new(id, \"t\", 4242, \"sid\", \"live_agent\")).unwrap();\n 95|        set_status(&p, status).unwrap();\n 96|        if let Some(ms) = last_active {\n## crc_swap.rs\n 52|) -> std::io::Result<()> {\n*53|    for entry in std::fs::read_dir(dir)? {\n 54|        let entry = entry?;\n 55|        let path = entry.path();\n 56|        if path.is_dir() {\n...\n 64|        let target = install_dir.join(&rel);\n*65|        let differs = match std::fs::read(&target) {\n*66|            Ok(existing) => sha256_hex(&std::fs::read(&path)?) != sha256_hex(&existing),\n 67|            // Absent (or unreadable) target ⇒ a new/changed file to place.\n 68|            Err(_) => true,\n 69|        };\n...\n 108|pub fn apply_crc_swap(plan: &[PlannedSwap]) -> std::io::Result<()> {\n*109|    apply_crc_swap_with(plan, &|from, to| std::fs::rename(from, to))\n 110|}\n 111|\n 112|/// [`apply_crc_swap`] over an injectable `rename` so the never-strand rollback is\n...\n 115|/// commit failure any more — the seam supplies one instead. Production passes\n*116|/// `std::fs::rename`.\n 117|// [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n 118|fn apply_crc_swap_with(\n 119|    plan: &[PlannedSwap],\n...\n 140|        if let Some(parent) = swap.target.parent() {\n*141|            std::fs::create_dir_all(parent)?;\n 142|        }\n 143|        let dst = new_path(&swap.target);\n*144|        std::fs::copy(&swap.staged, &dst)\n 145|            .map_err(|e| swap_err(e, \"stage new\", &swap.staged, &dst))?;\n 146|    }\n 147|\n...\n 183|        if let Some(old) = displaced {\n*184|            let _ = std::fs::remove_file(old);\n 185|        }\n 186|    }\n 187|    Ok(())\n...\n 226|    let dashed = format!(\"{base}-\");\n*227|    let Ok(entries) = std::fs::read_dir(dir) else {\n 228|        return;\n 229|    };\n 230|    for entry in entries.flatten() {\n...\n 232|            if name == base || name.starts_with(&dashed) {\n*233|                let _ = std::fs::remove_file(entry.path());\n 234|            }\n 235|        }\n 236|    }\n...\n 273|    for swap in plan {\n*274|        let _ = std::fs::remove_file(new_path(&swap.target));\n 275|    }\n 276|}\n 277|\n...\n 280|    use super::*;\n*281|    use std::fs;\n 282|    use std::path::Path;\n 283|    use tempfile::tempdir;\n 284|\n...\n 303|        fn walk(dir: &Path, found: &mut bool) {\n*304|            for entry in fs::read_dir(dir).unwrap() {\n 305|                let path = entry.unwrap().path();\n 306|                if path.is_dir() {\n 307|                    walk(&path, found);\n...\n 405|\n*406|        assert_eq!(fs::read(i.join(\"changed\")).unwrap(), b\"NEW\", \"changed got new bytes\");\n 407|        assert_eq!(\n*408|            fs::read(i.join(&fresh_rel)).unwrap(),\n 409|            b\"FRESH\",\n 410|            \"fresh file materialized with its nested parent dir created\"\n 411|        );\n 412|        assert_eq!(\n*413|            fs::read(i.join(\"identical\")).unwrap(),\n 414|            b\"STABLE\",\n 415|            \"identical file byte-unchanged (never touched)\"\n 416|        );\n...\n 476|            }\n*477|            std::fs::rename(from, to)\n 478|        };\n 479|\n 480|        let result = apply_crc_swap_with(&plan, &rename);\n...\n 483|        assert_eq!(\n*484|            fs::read(&first_target).unwrap(),\n 485|            b\"BEFORE\",\n 486|            \"rollback restored the first target's ORIGINAL bytes (not the staged AFTER)\"\n 487|        );\n...\n 489|        assert_eq!(\n*490|            fs::read(&second_target).unwrap(),\n 491|            b\"second-original\",\n 492|            \"the failing swap's target is restored — never half-applied\"\n 493|        );\n...\n 580|        assert_eq!(\n*581|            fs::read(i.join(\"bin\")).unwrap(),\n 582|            b\"NEW-IMAGE-BYTES\",\n 583|            \"the target received the new bytes (the original was displaced, not replaced-over)\"\n 584|        );\n## daemon.rs\n 349|        // is the socket ping, never this file, so a stale value is harmless.\n*350|        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n 351|\n 352|        // The seed-control channel is the foreground loop. Binding it is what\n 353|        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n...\n 959|    if let Ok(marker) = std::env::var(\"SPT_LAUNCH_RUNG_MARKER\") {\n*960|        let _ = std::fs::write(&marker, via);\n 961|    }\n 962|    if matches!(rung, LaunchRung::InJob) {\n 963|        eprintln!(\n...\n 1527|                }\n*1528|                let _ = std::fs::write(\n 1529|                    &diag,\n 1530|                    format!(\"launcher_in_job={launcher_in} gc_in_any_job={gc_in}\"),\n 1531|                );\n 1532|            }\n*1533|            std::fs::write(&pidfile, gc.to_string()).expect(\"launcher: write gc pid\");\n 1534|            std::thread::sleep(Duration::from_secs(300));\n 1535|            std::process::exit(0);\n 1536|        }\n...\n 1540|            std::env::temp_dir().join(format!(\"spt_breakaway_gc_{}.pid\", std::process::id()));\n*1541|        let _ = std::fs::remove_file(&pidfile);\n 1542|\n 1543|        let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };\n 1544|        assert!(job != 0, \"create job\");\n...\n 1586|                unsafe { CloseHandle(job) };\n*1587|                let _ = std::fs::remove_file(&pidfile);\n 1588|                eprintln!(\n 1589|                    \"SKIP breakaway_spawn_escapes_a_kill_on_close_job: runner ancestor \\\n 1590|                     job forbids breakaway (ACCESS_DENIED) — cannot isolate a single job\"\n...\n 1606|            while Instant::now() < deadline {\n*1607|                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608|                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609|                        if p != 0 {\n 1610|                            found = Some(p);\n...\n 1639|        unsafe { CloseHandle(job) };\n*1640|        let _ = std::fs::remove_file(&pidfile);\n 1641|        let diagpath = std::env::temp_dir().join(format!(\"spt_breakaway_diag_{}.log\", std::process::id()));\n*1642|        let diag = std::fs::read_to_string(&diagpath).unwrap_or_default();\n*1643|        let _ = std::fs::remove_file(&diagpath);\n 1644|\n 1645|        // Second skip path (sibling of the ACCESS_DENIED skip above): on some\n 1646|        // self-hosted runners the breakaway spawn SUCCEEDS (no ACCESS_DENIED) yet\n## deadline.rs\n 117|    fn load(path: &Path) -> Option<Self> {\n*118|        let raw = std::fs::read_to_string(path).ok()?;\n 119|        serde_json::from_str(&raw).ok()\n 120|    }\n 121|\n...\n 125|            if !parent.as_os_str().is_empty() {\n*126|                std::fs::create_dir_all(parent)?;\n 127|            }\n 128|        }\n 129|        let raw = serde_json::to_string(self).map_err(std::io::Error::other)?;\n*130|        std::fs::write(path, raw)\n 131|    }\n 132|\n 133|    /// The next fire at or after `now_ms`, derived functionally — **NO write**:\n...\n 334|        with_home(|_| {\n*335|            std::fs::write(anchor_path(\"pulse\"), b\"{ not valid json\").unwrap();\n 336|            // A garbled file is treated as absent → Update falls back to fresh.\n 337|            let upd = DeadlineAnchor::open(\"pulse\", 100, StartReason::Update, 8_000).unwrap();\n 338|            assert_eq!(upd.anchor_ms, 8_000);\n## deelevate.rs\n 197|    pub(super) fn elected_user_from(path: &std::path::Path) -> Option<SudoInvoker> {\n*198|        let name = std::fs::read_to_string(path).ok()?;\n 199|        user_by_name(name.trim())\n 200|    }\n 201|\n...\n 258|        if let Some(parent) = path.parent() {\n*259|            let _ = std::fs::create_dir_all(parent);\n 260|        }\n*261|        match std::fs::write(&path, format!(\"{name}\\n\")) {\n 262|            Ok(()) => eprintln!(\n 263|                \"ELECTED_DEFAULT_USER:{name} (recorded in {})\",\n 264|                path.display()\n...\n 905|        let file = dir.path().join(\"default-user\");\n*906|        std::fs::write(&file, \"no-such-user-zzz\\n\").unwrap();\n 907|        assert_eq!(unix::elected_user_from(&file), None);\n 908|        // Root never elects.\n*909|        std::fs::write(&file, \"root\\n\").unwrap();\n 910|        assert_eq!(unix::elected_user_from(&file), None);\n 911|        // The current (non-root) user resolves to its own ids. Skipped when\n 912|        // the test itself runs as root (containered CI) — root may not be\n...\n 917|                if !name.is_empty() {\n*918|                    std::fs::write(&file, format!(\"{name}\\n\")).unwrap();\n 919|                    if let Some(t) = unix::elected_user_from(&file) {\n 920|                        assert_eq!(t.uid, uid, \"elected uid = the named account's\");\n 921|                        assert!(t.home.is_some(), \"home resolved for the universe anchor\");\n## digest.rs\n 417|fn log_less_activity(id: &str, perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*418|    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 419|        return Vec::new();\n 420|    };\n 421|    let mut items = Vec::new();\n...\n 462|fn read_context_items(perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*463|    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 464|        return Vec::new();\n 465|    };\n 466|    s.lines()\n...\n 600|            let perch = perch::resolve_perch_path(\"solo\", ParentHint::Infer);\n*601|            std::fs::create_dir_all(&perch).unwrap();\n 602|            // No adapter recorded ⇒ the digest.log path.\n 603|            let rec =\n 604|                spt_store::info::InfoJson::new(\"solo\", \"0\", std::process::id(), \"sid\", \"live_agent\");\n...\n 780|            // Two per-session log files the extractor reads via {session_id}.\n*781|            std::fs::write(\n 782|                home.join(\"sess-A.jsonl\"),\n 783|                \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"before clear\\\"}\\n\",\n 784|            )\n 785|            .unwrap();\n*786|            std::fs::write(\n 787|                home.join(\"sess-B.jsonl\"),\n 788|                \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"after clear\\\"}\\n\",\n 789|            )\n...\n 799|            let asrc = home.join(\"srcs\").join(\"ccspan\");\n*800|            std::fs::create_dir_all(&asrc).unwrap();\n*801|            std::fs::write(asrc.join(\"manifest.toml\"), manifest).unwrap();\n 802|            spt_runtime::registry::register(&perch::adapters_dir(), &asrc, 1).unwrap();\n 803|\n 804|            // The endpoint records the adapter; the ledger holds both sessions.\n 805|            let perch = perch::resolve_perch_path(\"span\", ParentHint::Infer);\n*806|            std::fs::create_dir_all(&perch).unwrap();\n 807|            let mut rec =\n 808|                spt_store::info::InfoJson::new(\"span\", \"0\", std::process::id(), \"B\", \"live_agent\");\n 809|            rec.adapter = Some(\"ccspan\".to_string());\n...\n 859|            let perch = perch::resolve_perch_path(\"merge\", ParentHint::Infer);\n*860|            std::fs::create_dir_all(&perch).unwrap();\n 861|            let rec = spt_store::info::InfoJson::new(\n 862|                \"merge\",\n 863|                \"0\",\n## docshost.rs\n 144|/// Resolve one request against the docs root. Filesystem gate two: after the\n*145|/// pure sanitizer admits the relative path, the canonicalized target must\n*146|/// still live under the canonicalized root — a symlink inside the bundle\n 147|/// cannot escape it.\n 148|// [impl->REQ-DOCS-LOCAL-SERVER]\n 149|fn serve_path(docs_root: &Path, uri_path: &str, head_only: bool) -> Response<Full<Bytes>> {\n...\n 173|    let target = docs_root.join(&rel);\n*174|    let (Ok(canon_root), Ok(canon_target)) = (docs_root.canonicalize(), target.canonicalize())\n 175|    else {\n 176|        return respond(\n 177|            StatusCode::NOT_FOUND,\n...\n 188|    }\n*189|    match std::fs::read(&canon_target) {\n 190|        Ok(bytes) => respond(StatusCode::OK, content_type_for(&rel), bytes),\n 191|        Err(e) => respond(\n 192|            StatusCode::INTERNAL_SERVER_ERROR,\n## effect.rs\n 69|use std::collections::HashSet;\n*70|use std::fs::{File, OpenOptions};\n*71|use std::io::{self, BufRead, BufReader, Write};\n 72|use std::path::{Path, PathBuf};\n 73|use std::sync::Mutex;\n 74|\n...\n 341|        // (`default_journal_path`), which may not have been created yet on a fresh\n*342|        // machine — `OpenOptions::create` makes the file, never its directory, so a\n 343|        // missing parent surfaces as a NotFound at bind (the broker-bind CI failure).\n 344|        if let Some(parent) = path.parent() {\n 345|            if !parent.as_os_str().is_empty() {\n*346|                std::fs::create_dir_all(parent)?;\n 347|            }\n 348|        }\n 349|        let (applied, pending) = recover(&path)?;\n*350|        let file = OpenOptions::new().create(true).append(true).open(&path)?;\n 351|        Ok(EffectJournal {\n 352|            path,\n 353|            inner: Mutex::new(Inner {\n...\n 516|    let mut pending: HashSet<EffectKey> = HashSet::new();\n*517|    let file = match File::open(path) {\n 518|        Ok(f) => f,\n 519|        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok((applied, pending)),\n 520|        Err(e) => return Err(e),\n 521|    };\n*522|    for line in BufReader::new(file).lines() {\n 523|        let line = line?;\n 524|        let mut parts = line.split_whitespace();\n 525|        match parts.next() {\n...\n 619|    // under `spt_home()`, absent on a fresh machine (the broker-bind CI failure:\n*620|    // `OpenOptions::create` makes the file, never its directory).\n 621|    #[test]\n 622|    fn open_creates_missing_parent_dirs() {\n 623|        let dir = tempfile::tempdir().unwrap();\n...\n 930|        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n*931|        let text = std::fs::read_to_string(j.path()).expect(\"read journal file\");\n 932|        // New line shape carries the minter tag: \"PENDING 1 cli 2 net-send\".\n 933|        let net_token = format!(\"{} {} {}\", net_key.class, net_key.minter.as_tag(), net_key.op);\n 934|        let pty_token = format!(\"{} {} {}\", pty_key.class, pty_key.minter.as_tag(), pty_key.op);\n...\n 1035|        let old_path = dir.path().join(\"old.log\");\n*1036|        std::fs::write(&old_path, \"PENDING 7 3 net-dial\\nDONE 7 3\\n\").unwrap();\n 1037|        let j_old = EffectJournal::open(&old_path).unwrap();\n 1038|        let legacy_key = EffectKey {\n 1039|            class: 7,\n...\n 1063|        let new_path = dir.path().join(\"new.log\");\n*1064|        std::fs::write(&new_path, \"PENDING 7 rc 3 net-dial\\nDONE 7 rc 3\\n\").unwrap();\n 1065|        let j_new = EffectJournal::open(&new_path).unwrap();\n 1066|        let rc_key = EffectKey {\n 1067|            class: 7,\n...\n 1078|        let mixed_path = dir.path().join(\"mixed.log\");\n*1079|        std::fs::write(\n 1080|            &mixed_path,\n 1081|            \"PENDING 7 3 net-dial\\nDONE 7 3\\nPENDING 7 cli 3 net-dial\\nDONE 7 cli 3\\n\",\n 1082|        )\n## endpoint.rs\n 98|pub fn read_pump_heartbeat() -> Option<u64> {\n*99|    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n 100|    serde_json::from_str::<u64>(s.trim()).ok()\n 101|}\n 102|\n...\n 108|    let path = crate::pump::health::PumpHealth::path_beside(&pump_heartbeat_path());\n*109|    std::fs::metadata(&path).ok()?;\n 110|    Some(crate::pump::health::PumpHealth::load_from(&path))\n 111|}\n## grants.rs\n 533|                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);\n*534|                std::fs::create_dir_all(&p).unwrap();\n 535|                write_info(&p, &InfoJson::new(id, \"t\", 4242, \"sid\", \"live_agent\")).unwrap();\n 536|                set_status(&p, STATUS_ONLINE).unwrap();\n 537|                set_last_active(&p, ms).unwrap();\n## harnesshost.rs\n 413|        let shipped = dir.path().join(bare);\n*414|        std::fs::write(&shipped, b\"stub\").unwrap();\n 415|        let dir_str = dir.path().to_string_lossy().into_owned();\n 416|\n 417|        let m = spt_runtime::Manifest::from_toml_str(&format!(\n## inject.rs\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 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## lifecycle.rs\n 96|fn write_psyche_context_file(psyche_perch: &Path, content: &str) -> std::io::Result<PathBuf> {\n*97|    std::fs::create_dir_all(psyche_perch)?;\n 98|    let path = psyche_perch.join(PSYCHE_CONTEXT_FILE);\n*99|    std::fs::write(&path, content.as_bytes())?;\n 100|    Ok(path)\n 101|}\n 102|\n...\n 465|        let dir = self.install_dir.as_ref()?;\n*466|        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n 467|        Some(crate::release::sha256_hex(&bytes))\n 468|    }\n 469|\n...\n 486|        };\n*487|        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n 488|            return;\n 489|        };\n 490|        let hash = crate::release::sha256_hex(&bytes);\n...\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 1273|                    }\n...\n 1411|        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*1412|        std::fs::create_dir_all(&perch_path).unwrap();\n 1413|        let mut info = InfoJson::new(id, \"epoch-ms:0\", parent_pid, \"sid-1\", \"ready\");\n 1414|        info.parent_pid = Some(parent_pid);\n 1415|        write_info(&perch_path, &info).unwrap();\n...\n 1417|        if let Some(p) = ready.parent() {\n*1418|            std::fs::create_dir_all(p).unwrap();\n 1419|        }\n*1420|        std::fs::write(&ready, \"\").unwrap();\n 1421|        perch_path\n 1422|    }\n 1423|\n...\n 1432|        let p = perch::resolve_perch_path(id, ParentHint::Infer);\n*1433|        std::fs::create_dir_all(&p).unwrap();\n 1434|        let mut rec = InfoJson::new(id, \"t\", std::process::id(), \"sid-1\", \"live_agent\");\n 1435|        rec.read_env.insert(var.to_string(), val.to_string());\n 1436|        write_info(&p, &rec).unwrap();\n...\n 1446|            let script = home.join(\"echo_env.bat\");\n*1447|            std::fs::write(\n 1448|                &script,\n 1449|                format!(\"@echo %CLAUDE_TEST_ROOT%>\\\"{}\\\"\\r\\n\", marker.display()),\n 1450|            )\n...\n 1456|            let script = home.join(\"echo_env.sh\");\n*1457|            std::fs::write(\n 1458|                &script,\n 1459|                format!(\"echo \\\"$CLAUDE_TEST_ROOT\\\" > \\\"{}\\\"\\n\", marker.display()),\n 1460|            )\n...\n 1484|            let commune = home.join(\"commune\");\n*1485|            std::fs::create_dir_all(&commune).unwrap();\n 1486|            let dir = commune.to_string_lossy().replace('\\\\', \"/\");\n 1487|            let toml = format!(\n 1488|                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n...\n 1503|\n*1504|            let got = std::fs::read_to_string(&marker)\n 1505|                .expect(\"the psyche child must have spawned + written the marker\");\n 1506|            assert_eq!(\n 1507|                got.trim(),\n...\n 1547|\n*1548|            std::fs::write(drops.path().join(\"doyle-commune.md\"), \"brief\").unwrap();\n 1549|            let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 1550|            assert_eq!(report.ingested.len(), 1, \"the drop was folded in\");\n 1551|            assert!(!report.echo_fired, \"no echo gate armed\");\n...\n 1613|            let perch_path = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n*1614|            std::fs::create_dir_all(&perch_path).unwrap();\n 1615|            write_info(\n 1616|                &perch_path,\n 1617|                &InfoJson::new(\"doyle\", \"t\", std::process::id(), \"sid-1\", \"live_agent\"),\n...\n 1662|            let perch_path = perch::resolve_perch_path(\"hallbf\", ParentHint::Infer);\n*1663|            std::fs::create_dir_all(&perch_path).unwrap();\n 1664|            write_info(\n 1665|                &perch_path,\n 1666|                &InfoJson::new(\"hallbf\", \"t\", std::process::id(), \"sid-1\", \"live_agent\"),\n...\n 1775|            let perch_path = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n*1776|            std::fs::create_dir_all(&perch_path).unwrap();\n 1777|\n 1778|            // Homed parent → {subnet} is its home-subnet label.\n 1779|            let mut rec = InfoJson::new(\"doyle\", \"t\", std::process::id(), \"sid-1\", \"live_agent\");\n...\n 1816|        assert_eq!(\n*1817|            std::fs::metadata(&cont).unwrap().len(),\n 1818|            0,\n 1819|            \"a continue turn writes the context file TRULY 0-byte (perri PIN 1: no sentinel)\"\n 1820|        );\n...\n 1823|        let fresh = write_psyche_context_file(&perch, first_turn_psyche_context(Some(\"MIND\"))).unwrap();\n*1824|        assert_eq!(std::fs::read_to_string(&fresh).unwrap(), \"MIND\");\n*1825|        assert!(std::fs::metadata(&fresh).unwrap().len() > 0);\n 1826|\n 1827|        // FRESH with a zero-context agent: the NON-EMPTY <fresh-psyche/> marker (never\n 1828|        // 0-byte — else it would masquerade as continue). Overwrite-in-place: same path.\n## linkhost.rs\n 731|        let src = perch::spt_home().join(\"srcs\").join(name);\n*732|        std::fs::create_dir_all(&src).unwrap();\n*733|        std::fs::write(\n 734|            src.join(\"manifest.toml\"),\n 735|            format!(\n 736|                \"[adapter]\\nname = \\\"{name}\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\\n...\n 822|                let p = owlery.join(gw);\n*823|                std::fs::create_dir_all(&p).unwrap();\n 824|                spt_store::info::write_info(\n 825|                    &p,\n 826|                    &spt_store::info::InfoJson::new(\n...\n 884|        let src = perch::spt_home().join(\"srcs\").join(name);\n*885|        std::fs::create_dir_all(&src).unwrap();\n*886|        std::fs::write(\n 887|            src.join(\"manifest.toml\"),\n 888|            format!(\n 889|                \"[adapter]\\nname = \\\"{name}\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\\n...\n 1075|            let src = perch::spt_home().join(\"srcs\").join(\"Gated\");\n*1076|            std::fs::create_dir_all(&src).unwrap();\n 1077|            #[cfg(windows)]\n 1078|            let spawn = \"cmd /C exit 0\";\n 1079|            #[cfg(unix)]\n 1080|            let spawn = \"true\";\n*1081|            std::fs::write(\n 1082|                src.join(\"manifest.toml\"),\n 1083|                format!(\n 1084|                    \"[adapter]\\nname = \\\"Gated\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\\n\n## bin/\n### xlate_choreo_fixture.rs\n 36|\n*37|use std::fs::OpenOptions;\n 38|use std::io::{self, BufRead, Write};\n 39|\n 40|/// The fixed, recognizable payload token the choreography types into the PTY.\n...\n 62|        if let Some(path) = &stdin_log {\n*63|            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n 64|                let _ = writeln!(f, \"{trimmed}\");\n 65|            }\n 66|        }\n\nShowing files 1-20 of 73+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":144,"fileCount":20,"files":["crates/spt-daemon/src/adapter_update.rs","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/consent.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/grants.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/linkhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/adapter_update.rs","count":2},{"path":"crates/spt-daemon/src/applyhost.rs","count":20},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":2},{"path":"crates/spt-daemon/src/brainproc.rs","count":5},{"path":"crates/spt-daemon/src/broker.rs","count":4},{"path":"crates/spt-daemon/src/config.rs","count":11},{"path":"crates/spt-daemon/src/consent.rs","count":1},{"path":"crates/spt-daemon/src/crc_swap.rs","count":20},{"path":"crates/spt-daemon/src/daemon.rs","count":10},{"path":"crates/spt-daemon/src/deadline.rs","count":4},{"path":"crates/spt-daemon/src/deelevate.rs","count":6},{"path":"crates/spt-daemon/src/digest.rs","count":9},{"path":"crates/spt-daemon/src/docshost.rs","count":4},{"path":"crates/spt-daemon/src/effect.rs","count":12},{"path":"crates/spt-daemon/src/endpoint.rs","count":2},{"path":"crates/spt-daemon/src/grants.rs","count":1},{"path":"crates/spt-daemon/src/harnesshost.rs","count":1},{"path":"crates/spt-daemon/src/inject.rs","count":3},{"path":"crates/spt-daemon/src/lifecycle.rs","count":20},{"path":"crates/spt-daemon/src/linkhost.rs","count":7}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## adapter_update.rs\n 389│            let src = tmp.path().join(format!(\"{name}-src\"));\n*390│            std::fs::create_dir_all(&src).unwrap();\n*391│            std::fs::write(src.join(\"manifest.toml\"), body).unwrap();\n 392│            src\n 393│        };\n 394│        let plain = seed(\n## applyhost.rs\n  261│    let aside = swap_aside_path(exe_path, version);\n *262│    let _ = std::fs::remove_file(&aside); // stale prior apply of this version\n *263│    std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;\n  264│    if let Err(e) = land_artifact(exe_path, artifact) {\n *265│        let _ = std::fs::rename(&aside, exe_path); // roll back — never half-swapped\n  266│        return Err(format!(\"land new binary (rolled back): {e}\"));\n  267│    }\n  268│    // The binary IS swapped from here on — but DO NOT claim `applied` yet\n     │...\n  444│        use std::os::unix::fs::PermissionsExt;\n *445│        std::fs::set_permissions(exe_path, std::fs::Permissions::from_mode(0o755))?;\n  446│    }\n  447│    Ok(())\n  448│}\n     │...\n  625│            .collect();\n *626│        std::fs::write(\n  627│            &keys,\n  628│            serde_json::json!({\"keys\": {\"k\": pub_hex}, \"channel\": \"stable\"}).to_string(),\n  629│        )\n     │...\n  687│            .collect();\n *688│        std::fs::write(\n  689│            &keys,\n  690│            serde_json::json!({\"keys\": {\"k\": pub_hex}, \"channel\": \"stable\"}).to_string(),\n  691│        )\n     │...\n  723│            .collect();\n *724│        std::fs::write(\n  725│            &keys,\n  726│            serde_json::json!({\"keys\": {\"k\": pub_hex}, \"channel\": \"stable\"}).to_string(),\n  727│        )\n     │...\n  743│        let exe = dir.path().join(\"spt-binary\");\n *744│        std::fs::write(&exe, b\"OLD\").unwrap();\n  745│\n  746│        let cache = ReleaseCache::open(&dir.path().join(\"releases\"));\n  747│        let (signed, keys) = sign_single(dir.path(), 7, b\"CROSS-OS BYTES\");\n     │...\n  756│        );\n *757│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n  758│\n  759│        // 2. Single stamped for the OTHER OS refuses too. \"Other\" = any\n  760│        //    registered platform that is not this host — derived from the\n     │...\n  775│        );\n *776│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n  777│\n  778│        // 3. Stamped for THIS platform: the single applies (the legitimate\n  779│        //    origin-fetched-local path).\n     │...\n  787│        );\n *788│        assert_eq!(std::fs::read(&exe).unwrap(), b\"CROSS-OS BYTES\");\n  789│    }\n  790│\n  791│    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] [unit->REQ-UPD-3] the planned\n     │...\n  889│        let exe = dir.path().join(\"spt-binary\");\n *890│        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n  891│        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n  892│\n  893│        let out = apply_staged(&cache, &keys, &exe, &name).expect(\"apply ok\");\n     │...\n  898│        assert_eq!(\n *899│            std::fs::read(&exe).unwrap(),\n  900│            b\"NEW BINARY BYTES\",\n  901│            \"artifact landed\"\n  902│        );\n  903│        assert_eq!(\n *904│            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n  905│            b\"OLD BINARY\",\n  906│            \"the outgoing binary stepped aside\"\n  907│        );\n     │...\n  927│        let exe = dir.path().join(\"spt-binary\");\n *928│        std::fs::write(&exe, b\"OLD BINARY\").unwrap();\n  929│        let (cache, keys) = stage(dir.path(), 7, b\"NEW BINARY BYTES\", BROKER_RESOURCE_ABI);\n  930│\n  931│        // No served_broker — deliberately. A daemonless apply must not need one.\n     │...\n  937│        assert_eq!(\n *938│            std::fs::read(&exe).unwrap(),\n  939│            b\"NEW BINARY BYTES\",\n  940│            \"artifact landed without any broker handoff\"\n  941│        );\n  942│        assert_eq!(\n *943│            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n  944│            b\"OLD BINARY\",\n  945│            \"the outgoing binary stepped aside\"\n  946│        );\n     │...\n  966│        let exe = dir.path().join(\"spt-binary\");\n *967│        std::fs::write(&exe, b\"OLD\").unwrap();\n  968│\n  969│        // Nothing staged.\n  970│        let empty = ReleaseCache::open(&dir.path().join(\"releases\"));\n     │...\n  984│        );\n *985│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n  986│\n  987│        // Already-applied: idempotent no-op, never re-renames an already-swapped exe.\n  988│        // A still-open trial that already landed this version's bytes marks it applied.\n     │...\n 1002│        );\n*1003│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1004│    }\n 1005│\n 1006│    // [unit->REQ-UPD-6] apply-time re-verification handles platform-targeted\n## brainproc.rs\n  382│    let exe = std::env::current_exe().ok()?;\n *383│    let bytes = std::fs::read(exe).ok()?;\n  384│    Some(crate::release::sha256_hex(&bytes))\n  385│}\n  386│\n     │...\n  418│pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n *419│    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n  420│}\n  421│\n  422│/// The supervisor's next backoff after a brain death: a healthy run (≥\n     │...\n  541│    fn clear_ready(&self) {\n *542│        let _ = std::fs::remove_file(brain_ready_path());\n  543│    }\n  544│    fn record_promoted(&self, version: u64) {\n  545│        let cache = self.cache();\n     │...\n  563│    fn ready_exe_hash(&self) -> Option<String> {\n *564│        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n  565│        parse_ready_exe_hash(&body)\n  566│    }\n  567│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n     │...\n 1391│        assert_eq!(ready_generation_at(&path), None, \"absent file → not-ready\");\n*1392│        std::fs::write(&path, serde_json::json!({\"pid\": 1, \"generation\": 42}).to_string())\n 1393│            .unwrap();\n 1394│        assert_eq!(ready_generation_at(&path), Some(42));\n 1395│    }\n## broker.rs\n 2028│fn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n*2029│    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n 2030│    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n 2031│    let decl = manifest.message_idle_translation_binary.as_ref()?;\n 2032│    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n     │...\n 6244│                        [message-idle-translation-binary]\\npath = \\\"some/bin\\\"\\n\";\n*6245│        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6246│        assert_eq!(\n 6247│            read_translation_command(dir.path()),\n 6248│            Some(vec![\"some/bin\".to_string()]),\n     │...\n 6258│                         command = \\\"{adapter_dir}/claude-spt translate\\\"\\n\";\n*6259│        std::fs::write(dir2.path().join(\"manifest.toml\"), manifest2).unwrap();\n 6260│        let argv = read_translation_command(dir2.path()).expect(\"command resolves\");\n 6261│        assert_eq!(argv.len(), 2, \"program + one arg: {argv:?}\");\n 6262│        assert!(\n     │...\n 6281│                        min_spt_core_version=\\\"1\\\"\\n\";\n*6282│        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6283│        assert_eq!(\n 6284│            read_translation_command(dir.path()),\n 6285│            None,\n## config.rs\n 241│    pub fn load_from(path: &Path) -> Self {\n*242│        let Ok(raw) = std::fs::read_to_string(path) else {\n 243│            return Self::default();\n 244│        };\n 245│        let file: DaemonConfigFile = match serde_json::from_str(&raw) {\n    │...\n 328│            .map_err(|e| std::io::Error::other(e.to_string()))?;\n*329│        std::fs::write(path, json)\n 330│    }\n 331│\n 332│    /// Upsert a startup default at the canonical path (`endpoint run --save`,\n    │...\n 369│        let path = dir.path().join(\"daemon.json\");\n*370│        std::fs::write(&path, r#\"{\"pulse_period_ms\": 250}\"#).unwrap();\n 371│\n 372│        let cfg = DaemonConfig::load_from(&path);\n 373│        assert_eq!(cfg.pulse_period, Duration::from_millis(250));\n    │...\n 385│        let path = dir.path().join(\"daemon.json\");\n*386│        std::fs::write(&path, \"not json {{{\").unwrap();\n 387│        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());\n 388│    }\n 389│\n    │...\n 435│        let path = dir.path().join(\"daemon.json\");\n*436│        std::fs::write(&path, r#\"{\"detached_subnets\": [\"work\"]}\"#).unwrap();\n 437│        assert_eq!(\n 438│            DaemonConfig::load_from(&path).detached_subnets,\n 439│            vec![\"work\".to_string()]\n    │...\n 458│        let path = dir.path().join(\"daemon.json\");\n*459│        std::fs::write(&path, r#\"{\"auto_suspend_after_ms\": 300000}\"#).unwrap();\n 460│        assert_eq!(\n 461│            DaemonConfig::load_from(&path).auto_suspend_after_ms,\n 462│            Some(300_000)\n    │...\n 479│        let path = dir.path().join(\"daemon.json\");\n*480│        std::fs::write(&path, r#\"{\"full_auto_update\": true}\"#).unwrap();\n 481│        assert!(\n 482│            DaemonConfig::load_from(&path).full_auto_update,\n 483│            \"explicit opt-in honored\"\n    │...\n 499│        let path = dir.path().join(\"daemon.json\");\n*500│        std::fs::write(&path, r#\"{\"relay\": \"disabled\"}\"#).unwrap();\n 501│        assert_eq!(DaemonConfig::load_from(&path).relay, RelayChoice::Disabled);\n 502│\n 503│        // An unrecognized future value degrades to the default, never wedges.\n*504│        std::fs::write(&path, r#\"{\"relay\": \"quantum-mesh\"}\"#).unwrap();\n 505│        assert_eq!(DaemonConfig::load_from(&path).relay, RelayChoice::N0);\n 506│\n 507│        let cfg = DaemonConfig {\n    │...\n 518│        let path = dir.path().join(\"daemon.json\");\n*519│        std::fs::write(&path, r#\"{\"crash_grace_ms\": 500}\"#).unwrap();\n 520│\n 521│        let cfg = DaemonConfig::load_from(&path);\n 522│        assert_eq!(cfg.crash_grace, Duration::from_millis(500));\n    │...\n 576│        // key) loads as empty — forward/back-compat, never a parse failure.\n*577│        std::fs::write(&path, r#\"{\"pulse_period_ms\": 5000}\"#).unwrap();\n 578│        assert!(\n 579│            DaemonConfig::load_from(&path).startup_endpoints.is_empty(),\n 580│            \"a file without the key back-loads as empty, not an error\"\n## consent.rs\n 92│        let p = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);\n*93│        std::fs::create_dir_all(&p).unwrap();\n 94│        write_info(&p, &InfoJson::new(id, \"t\", 4242, \"sid\", \"live_agent\")).unwrap();\n 95│        set_status(&p, status).unwrap();\n 96│        if let Some(ms) = last_active {\n## crc_swap.rs\n  52│) -> std::io::Result<()> {\n *53│    for entry in std::fs::read_dir(dir)? {\n  54│        let entry = entry?;\n  55│        let path = entry.path();\n  56│        if path.is_dir() {\n    │...\n  64│        let target = install_dir.join(&rel);\n *65│        let differs = match std::fs::read(&target) {\n *66│            Ok(existing) => sha256_hex(&std::fs::read(&path)?) != sha256_hex(&existing),\n  67│            // Absent (or unreadable) target ⇒ a new/changed file to place.\n  68│            Err(_) => true,\n  69│        };\n    │...\n 108│pub fn apply_crc_swap(plan: &[PlannedSwap]) -> std::io::Result<()> {\n*109│    apply_crc_swap_with(plan, &|from, to| std::fs::rename(from, to))\n 110│}\n 111│\n 112│/// [`apply_crc_swap`] over an injectable `rename` so the never-strand rollback is\n    │...\n 115│/// commit failure any more — the seam supplies one instead. Production passes\n*116│/// `std::fs::rename`.\n 117│// [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n 118│fn apply_crc_swap_with(\n 119│    plan: &[PlannedSwap],\n    │...\n 140│        if let Some(parent) = swap.target.parent() {\n*141│            std::fs::create_dir_all(parent)?;\n 142│        }\n 143│        let dst = new_path(&swap.target);\n*144│        std::fs::copy(&swap.staged, &dst)\n 145│            .map_err(|e| swap_err(e, \"stage new\", &swap.staged, &dst))?;\n 146│    }\n 147│\n    │...\n 183│        if let Some(old) = displaced {\n*184│            let _ = std::fs::remove_file(old);\n 185│        }\n 186│    }\n 187│    Ok(())\n    │...\n 226│    let dashed = format!(\"{base}-\");\n*227│    let Ok(entries) = std::fs::read_dir(dir) else {\n 228│        return;\n 229│    };\n 230│    for entry in entries.flatten() {\n    │...\n 232│            if name == base || name.starts_with(&dashed) {\n*233│                let _ = std::fs::remove_file(entry.path());\n 234│            }\n 235│        }\n 236│    }\n    │...\n 273│    for swap in plan {\n*274│        let _ = std::fs::remove_file(new_path(&swap.target));\n 275│    }\n 276│}\n 277│\n    │...\n 280│    use super::*;\n*281│    use std::fs;\n 282│    use std::path::Path;\n 283│    use tempfile::tempdir;\n 284│\n    │...\n 303│        fn walk(dir: &Path, found: &mut bool) {\n*304│            for entry in fs::read_dir(dir).unwrap() {\n 305│                let path = entry.unwrap().path();\n 306│                if path.is_dir() {\n 307│                    walk(&path, found);\n    │...\n 405│\n*406│        assert_eq!(fs::read(i.join(\"changed\")).unwrap(), b\"NEW\", \"changed got new bytes\");\n 407│        assert_eq!(\n*408│            fs::read(i.join(&fresh_rel)).unwrap(),\n 409│            b\"FRESH\",\n 410│            \"fresh file materialized with its nested parent dir created\"\n 411│        );\n 412│        assert_eq!(\n*413│            fs::read(i.join(\"identical\")).unwrap(),\n 414│            b\"STABLE\",\n 415│            \"identical file byte-unchanged (never touched)\"\n 416│        );\n    │...\n 476│            }\n*477│            std::fs::rename(from, to)\n 478│        };\n 479│\n 480│        let result = apply_crc_swap_with(&plan, &rename);\n    │...\n 483│        assert_eq!(\n*484│            fs::read(&first_target).unwrap(),\n 485│            b\"BEFORE\",\n 486│            \"rollback restored the first target's ORIGINAL bytes (not the staged AFTER)\"\n 487│        );\n    │...\n 489│        assert_eq!(\n*490│            fs::read(&second_target).unwrap(),\n 491│            b\"second-original\",\n 492│            \"the failing swap's target is restored — never half-applied\"\n 493│        );\n    │...\n 580│        assert_eq!(\n*581│            fs::read(i.join(\"bin\")).unwrap(),\n 582│            b\"NEW-IMAGE-BYTES\",\n 583│            \"the target received the new bytes (the original was displaced, not replaced-over)\"\n 584│        );\n## daemon.rs\n  349│        // is the socket ping, never this file, so a stale value is harmless.\n *350│        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n  351│\n  352│        // The seed-control channel is the foreground loop. Binding it is what\n  353│        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n     │...\n  959│    if let Ok(marker) = std::env::var(\"SPT_LAUNCH_RUNG_MARKER\") {\n *960│        let _ = std::fs::write(&marker, via);\n  961│    }\n  962│    if matches!(rung, LaunchRung::InJob) {\n  963│        eprintln!(\n     │...\n 1527│                }\n*1528│                let _ = std::fs::write(\n 1529│                    &diag,\n 1530│                    format!(\"launcher_in_job={launcher_in} gc_in_any_job={gc_in}\"),\n 1531│                );\n 1532│            }\n*1533│            std::fs::write(&pidfile, gc.to_string()).expect(\"launcher: write gc pid\");\n 1534│            std::thread::sleep(Duration::from_secs(300));\n 1535│            std::process::exit(0);\n 1536│        }\n     │...\n 1540│            std::env::temp_dir().join(format!(\"spt_breakaway_gc_{}.pid\", std::process::id()));\n*1541│        let _ = std::fs::remove_file(&pidfile);\n 1542│\n 1543│        let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };\n 1544│        assert!(job != 0, \"create job\");\n     │...\n 1586│                unsafe { CloseHandle(job) };\n*1587│                let _ = std::fs::remove_file(&pidfile);\n 1588│                eprintln!(\n 1589│                    \"SKIP breakaway_spawn_escapes_a_kill_on_close_job: runner ancestor \\\n 1590│                     job forbids breakaway (ACCESS_DENIED) — cannot isolate a single job\"\n     │...\n 1606│            while Instant::now() < deadline {\n*1607│                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608│                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609│                        if p != 0 {\n 1610│                            found = Some(p);\n     │...\n 1639│        unsafe { CloseHandle(job) };\n*1640│        let _ = std::fs::remove_file(&pidfile);\n 1641│        let diagpath = std::env::temp_dir().join(format!(\"spt_breakaway_diag_{}.log\", std::process::id()));\n*1642│        let diag = std::fs::read_to_string(&diagpath).unwrap_or_default();\n*1643│        let _ = std::fs::remove_file(&diagpath);\n 1644│\n 1645│        // Second skip path (sibling of the ACCESS_DENIED skip above): on some\n 1646│        // self-hosted runners the breakaway spawn SUCCEEDS (no ACCESS_DENIED) yet\n## deadline.rs\n 117│    fn load(path: &Path) -> Option<Self> {\n*118│        let raw = std::fs::read_to_string(path).ok()?;\n 119│        serde_json::from_str(&raw).ok()\n 120│    }\n 121│\n    │...\n 125│            if !parent.as_os_str().is_empty() {\n*126│                std::fs::create_dir_all(parent)?;\n 127│            }\n 128│        }\n 129│        let raw = serde_json::to_string(self).map_err(std::io::Error::other)?;\n*130│        std::fs::write(path, raw)\n 131│    }\n 132│\n 133│    /// The next fire at or after `now_ms`, derived functionally — **NO write**:\n    │...\n 334│        with_home(|_| {\n*335│            std::fs::write(anchor_path(\"pulse\"), b\"{ not valid json\").unwrap();\n 336│            // A garbled file is treated as absent → Update falls back to fresh.\n 337│            let upd = DeadlineAnchor::open(\"pulse\", 100, StartReason::Update, 8_000).unwrap();\n 338│            assert_eq!(upd.anchor_ms, 8_000);\n## deelevate.rs\n 197│    pub(super) fn elected_user_from(path: &std::path::Path) -> Option<SudoInvoker> {\n*198│        let name = std::fs::read_to_string(path).ok()?;\n 199│        user_by_name(name.trim())\n 200│    }\n 201│\n    │...\n 258│        if let Some(parent) = path.parent() {\n*259│            let _ = std::fs::create_dir_all(parent);\n 260│        }\n*261│        match std::fs::write(&path, format!(\"{name}\\n\")) {\n 262│            Ok(()) => eprintln!(\n 263│                \"ELECTED_DEFAULT_USER:{name} (recorded in {})\",\n 264│                path.display()\n    │...\n 905│        let file = dir.path().join(\"default-user\");\n*906│        std::fs::write(&file, \"no-such-user-zzz\\n\").unwrap();\n 907│        assert_eq!(unix::elected_user_from(&file), None);\n 908│        // Root never elects.\n*909│        std::fs::write(&file, \"root\\n\").unwrap();\n 910│        assert_eq!(unix::elected_user_from(&file), None);\n 911│        // The current (non-root) user resolves to its own ids. Skipped when\n 912│        // the test itself runs as root (containered CI) — root may not be\n    │...\n 917│                if !name.is_empty() {\n*918│                    std::fs::write(&file, format!(\"{name}\\n\")).unwrap();\n 919│                    if let Some(t) = unix::elected_user_from(&file) {\n 920│                        assert_eq!(t.uid, uid, \"elected uid = the named account's\");\n 921│                        assert!(t.home.is_some(), \"home resolved for the universe anchor\");\n## digest.rs\n 417│fn log_less_activity(id: &str, perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*418│    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 419│        return Vec::new();\n 420│    };\n 421│    let mut items = Vec::new();\n    │...\n 462│fn read_context_items(perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*463│    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 464│        return Vec::new();\n 465│    };\n 466│    s.lines()\n    │...\n 600│            let perch = perch::resolve_perch_path(\"solo\", ParentHint::Infer);\n*601│            std::fs::create_dir_all(&perch).unwrap();\n 602│            // No adapter recorded ⇒ the digest.log path.\n 603│            let rec =\n 604│                spt_store::info::InfoJson::new(\"solo\", \"0\", std::process::id(), \"sid\", \"live_agent\");\n    │...\n 780│            // Two per-session log files the extractor reads via {session_id}.\n*781│            std::fs::write(\n 782│                home.join(\"sess-A.jsonl\"),\n 783│                \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"before clear\\\"}\\n\",\n 784│            )\n 785│            .unwrap();\n*786│            std::fs::write(\n 787│                home.join(\"sess-B.jsonl\"),\n 788│                \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"after clear\\\"}\\n\",\n 789│            )\n    │...\n 799│            let asrc = home.join(\"srcs\").join(\"ccspan\");\n*800│            std::fs::create_dir_all(&asrc).unwrap();\n*801│            std::fs::write(asrc.join(\"manifest.toml\"), manifest).unwrap();\n 802│            spt_runtime::registry::register(&perch::adapters_dir(), &asrc, 1).unwrap();\n 803│\n 804│            // The endpoint records the adapter; the ledger holds both sessions.\n 805│            let perch = perch::resolve_perch_path(\"span\", ParentHint::Infer);\n*806│            std::fs::create_dir_all(&perch).unwrap();\n 807│            let mut rec =\n 808│                spt_store::info::InfoJson::new(\"span\", \"0\", std::process::id(), \"B\", \"live_agent\");\n 809│            rec.adapter = Some(\"ccspan\".to_string());\n    │...\n 859│            let perch = perch::resolve_perch_path(\"merge\", ParentHint::Infer);\n*860│            std::fs::create_dir_all(&perch).unwrap();\n 861│            let rec = spt_store::info::InfoJson::new(\n 862│                \"merge\",\n 863│                \"0\",\n## docshost.rs\n 144│/// Resolve one request against the docs root. Filesystem gate two: after the\n*145│/// pure sanitizer admits the relative path, the canonicalized target must\n*146│/// still live under the canonicalized root — a symlink inside the bundle\n 147│/// cannot escape it.\n 148│// [impl->REQ-DOCS-LOCAL-SERVER]\n 149│fn serve_path(docs_root: &Path, uri_path: &str, head_only: bool) -> Response<Full<Bytes>> {\n    │...\n 173│    let target = docs_root.join(&rel);\n*174│    let (Ok(canon_root), Ok(canon_target)) = (docs_root.canonicalize(), target.canonicalize())\n 175│    else {\n 176│        return respond(\n 177│            StatusCode::NOT_FOUND,\n    │...\n 188│    }\n*189│    match std::fs::read(&canon_target) {\n 190│        Ok(bytes) => respond(StatusCode::OK, content_type_for(&rel), bytes),\n 191│        Err(e) => respond(\n 192│            StatusCode::INTERNAL_SERVER_ERROR,\n## effect.rs\n   69│use std::collections::HashSet;\n  *70│use std::fs::{File, OpenOptions};\n  *71│use std::io::{self, BufRead, BufReader, Write};\n   72│use std::path::{Path, PathBuf};\n   73│use std::sync::Mutex;\n   74│\n     │...\n  341│        // (`default_journal_path`), which may not have been created yet on a fresh\n *342│        // machine — `OpenOptions::create` makes the file, never its directory, so a\n  343│        // missing parent surfaces as a NotFound at bind (the broker-bind CI failure).\n  344│        if let Some(parent) = path.parent() {\n  345│            if !parent.as_os_str().is_empty() {\n *346│                std::fs::create_dir_all(parent)?;\n  347│            }\n  348│        }\n  349│        let (applied, pending) = recover(&path)?;\n *350│        let file = OpenOptions::new().create(true).append(true).open(&path)?;\n  351│        Ok(EffectJournal {\n  352│            path,\n  353│            inner: Mutex::new(Inner {\n     │...\n  516│    let mut pending: HashSet<EffectKey> = HashSet::new();\n *517│    let file = match File::open(path) {\n  518│        Ok(f) => f,\n  519│        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok((applied, pending)),\n  520│        Err(e) => return Err(e),\n  521│    };\n *522│    for line in BufReader::new(file).lines() {\n  523│        let line = line?;\n  524│        let mut parts = line.split_whitespace();\n  525│        match parts.next() {\n     │...\n  619│    // under `spt_home()`, absent on a fresh machine (the broker-bind CI failure:\n *620│    // `OpenOptions::create` makes the file, never its directory).\n  621│    #[test]\n  622│    fn open_creates_missing_parent_dirs() {\n  623│        let dir = tempfile::tempdir().unwrap();\n     │...\n  930│        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n *931│        let text = std::fs::read_to_string(j.path()).expect(\"read journal file\");\n  932│        // New line shape carries the minter tag: \"PENDING 1 cli 2 net-send\".\n  933│        let net_token = format!(\"{} {} {}\", net_key.class, net_key.minter.as_tag(), net_key.op);\n  934│        let pty_token = format!(\"{} {} {}\", pty_key.class, pty_key.minter.as_tag(), pty_key.op);\n     │...\n 1035│        let old_path = dir.path().join(\"old.log\");\n*1036│        std::fs::write(&old_path, \"PENDING 7 3 net-dial\\nDONE 7 3\\n\").unwrap();\n 1037│        let j_old = EffectJournal::open(&old_path).unwrap();\n 1038│        let legacy_key = EffectKey {\n 1039│            class: 7,\n     │...\n 1063│        let new_path = dir.path().join(\"new.log\");\n*1064│        std::fs::write(&new_path, \"PENDING 7 rc 3 net-dial\\nDONE 7 rc 3\\n\").unwrap();\n 1065│        let j_new = EffectJournal::open(&new_path).unwrap();\n 1066│        let rc_key = EffectKey {\n 1067│            class: 7,\n     │...\n 1078│        let mixed_path = dir.path().join(\"mixed.log\");\n*1079│        std::fs::write(\n 1080│            &mixed_path,\n 1081│            \"PENDING 7 3 net-dial\\nDONE 7 3\\nPENDING 7 cli 3 net-dial\\nDONE 7 cli 3\\n\",\n 1082│        )\n## endpoint.rs\n  98│pub fn read_pump_heartbeat() -> Option<u64> {\n *99│    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n 100│    serde_json::from_str::<u64>(s.trim()).ok()\n 101│}\n 102│\n    │...\n 108│    let path = crate::pump::health::PumpHealth::path_beside(&pump_heartbeat_path());\n*109│    std::fs::metadata(&path).ok()?;\n 110│    Some(crate::pump::health::PumpHealth::load_from(&path))\n 111│}\n## grants.rs\n 533│                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);\n*534│                std::fs::create_dir_all(&p).unwrap();\n 535│                write_info(&p, &InfoJson::new(id, \"t\", 4242, \"sid\", \"live_agent\")).unwrap();\n 536│                set_status(&p, STATUS_ONLINE).unwrap();\n 537│                set_last_active(&p, ms).unwrap();\n## harnesshost.rs\n 413│        let shipped = dir.path().join(bare);\n*414│        std::fs::write(&shipped, b\"stub\").unwrap();\n 415│        let dir_str = dir.path().to_string_lossy().into_owned();\n 416│\n 417│        let m = spt_runtime::Manifest::from_toml_str(&format!(\n## inject.rs\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 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## lifecycle.rs\n   96│fn write_psyche_context_file(psyche_perch: &Path, content: &str) -> std::io::Result<PathBuf> {\n  *97│    std::fs::create_dir_all(psyche_perch)?;\n   98│    let path = psyche_perch.join(PSYCHE_CONTEXT_FILE);\n  *99│    std::fs::write(&path, content.as_bytes())?;\n  100│    Ok(path)\n  101│}\n  102│\n     │...\n  465│        let dir = self.install_dir.as_ref()?;\n *466│        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n  467│        Some(crate::release::sha256_hex(&bytes))\n  468│    }\n  469│\n     │...\n  486│        };\n *487│        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n  488│            return;\n  489│        };\n  490│        let hash = crate::release::sha256_hex(&bytes);\n     │...\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 1273│                    }\n     │...\n 1411│        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*1412│        std::fs::create_dir_all(&perch_path).unwrap();\n 1413│        let mut info = InfoJson::new(id, \"epoch-ms:0\", parent_pid, \"sid-1\", \"ready\");\n 1414│        info.parent_pid = Some(parent_pid);\n 1415│        write_info(&perch_path, &info).unwrap();\n     │...\n 1417│        if let Some(p) = ready.parent() {\n*1418│            std::fs::create_dir_all(p).unwrap();\n 1419│        }\n*1420│        std::fs::write(&ready, \"\").unwrap();\n 1421│        perch_path\n 1422│    }\n 1423│\n     │...\n 1432│        let p = perch::resolve_perch_path(id, ParentHint::Infer);\n*1433│        std::fs::create_dir_all(&p).unwrap();\n 1434│        let mut rec = InfoJson::new(id, \"t\", std::process::id(), \"sid-1\", \"live_agent\");\n 1435│        rec.read_env.insert(var.to_string(), val.to_string());\n 1436│        write_info(&p, &rec).unwrap();\n     │...\n 1446│            let script = home.join(\"echo_env.bat\");\n*1447│            std::fs::write(\n 1448│                &script,\n 1449│                format!(\"@echo %CLAUDE_TEST_ROOT%>\\\"{}\\\"\\r\\n\", marker.display()),\n 1450│            )\n     │...\n 1456│            let script = home.join(\"echo_env.sh\");\n*1457│            std::fs::write(\n 1458│                &script,\n 1459│                format!(\"echo \\\"$CLAUDE_TEST_ROOT\\\" > \\\"{}\\\"\\n\", marker.display()),\n 1460│            )\n     │...\n 1484│            let commune = home.join(\"commune\");\n*1485│            std::fs::create_dir_all(&commune).unwrap();\n 1486│            let dir = commune.to_string_lossy().replace('\\\\', \"/\");\n 1487│            let toml = format!(\n 1488│                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n     │...\n 1503│\n*1504│            let got = std::fs::read_to_string(&marker)\n 1505│                .expect(\"the psyche child must have spawned + written the marker\");\n 1506│            assert_eq!(\n 1507│                got.trim(),\n     │...\n 1547│\n*1548│            std::fs::write(drops.path().join(\"doyle-commune.md\"), \"brief\").unwrap();\n 1549│            let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 1550│            assert_eq!(report.ingested.len(), 1, \"the drop was folded in\");\n 1551│            assert!(!report.echo_fired, \"no echo gate armed\");\n     │...\n 1613│            let perch_path = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n*1614│            std::fs::create_dir_all(&perch_path).unwrap();\n 1615│            write_info(\n 1616│                &perch_path,\n 1617│                &InfoJson::new(\"doyle\", \"t\", std::process::id(), \"sid-1\", \"live_agent\"),\n     │...\n 1662│            let perch_path = perch::resolve_perch_path(\"hallbf\", ParentHint::Infer);\n*1663│            std::fs::create_dir_all(&perch_path).unwrap();\n 1664│            write_info(\n 1665│                &perch_path,\n 1666│                &InfoJson::new(\"hallbf\", \"t\", std::process::id(), \"sid-1\", \"live_agent\"),\n     │...\n 1775│            let perch_path = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n*1776│            std::fs::create_dir_all(&perch_path).unwrap();\n 1777│\n 1778│            // Homed parent → {subnet} is its home-subnet label.\n 1779│            let mut rec = InfoJson::new(\"doyle\", \"t\", std::process::id(), \"sid-1\", \"live_agent\");\n     │...\n 1816│        assert_eq!(\n*1817│            std::fs::metadata(&cont).unwrap().len(),\n 1818│            0,\n 1819│            \"a continue turn writes the context file TRULY 0-byte (perri PIN 1: no sentinel)\"\n 1820│        );\n     │...\n 1823│        let fresh = write_psyche_context_file(&perch, first_turn_psyche_context(Some(\"MIND\"))).unwrap();\n*1824│        assert_eq!(std::fs::read_to_string(&fresh).unwrap(), \"MIND\");\n*1825│        assert!(std::fs::metadata(&fresh).unwrap().len() > 0);\n 1826│\n 1827│        // FRESH with a zero-context agent: the NON-EMPTY <fresh-psyche/> marker (never\n 1828│        // 0-byte — else it would masquerade as continue). Overwrite-in-place: same path.\n## linkhost.rs\n  731│        let src = perch::spt_home().join(\"srcs\").join(name);\n *732│        std::fs::create_dir_all(&src).unwrap();\n *733│        std::fs::write(\n  734│            src.join(\"manifest.toml\"),\n  735│            format!(\n  736│                \"[adapter]\\nname = \\\"{name}\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\\n     │...\n  822│                let p = owlery.join(gw);\n *823│                std::fs::create_dir_all(&p).unwrap();\n  824│                spt_store::info::write_info(\n  825│                    &p,\n  826│                    &spt_store::info::InfoJson::new(\n     │...\n  884│        let src = perch::spt_home().join(\"srcs\").join(name);\n *885│        std::fs::create_dir_all(&src).unwrap();\n *886│        std::fs::write(\n  887│            src.join(\"manifest.toml\"),\n  888│            format!(\n  889│                \"[adapter]\\nname = \\\"{name}\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\\n     │...\n 1075│            let src = perch::spt_home().join(\"srcs\").join(\"Gated\");\n*1076│            std::fs::create_dir_all(&src).unwrap();\n 1077│            #[cfg(windows)]\n 1078│            let spawn = \"cmd /C exit 0\";\n 1079│            #[cfg(unix)]\n 1080│            let spawn = \"true\";\n*1081│            std::fs::write(\n 1082│                src.join(\"manifest.toml\"),\n 1083│                format!(\n 1084│                    \"[adapter]\\nname = \\\"Gated\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\\n\n## bin/\n### xlate_choreo_fixture.rs\n 36│\n*37│use std::fs::OpenOptions;\n 38│use std::io::{self, BufRead, Write};\n 39│\n 40│/// The fixed, recognizable payload token the choreography types into the PTY.\n   │...\n 62│        if let Some(path) = &stdin_log {\n*63│            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n 64│                let _ = writeln!(f, \"{trimmed}\");\n 65│            }\n 66│        }"},"isError":false,"timestamp":1784271462367}}
{"type":"message","id":"14d3be0c","parentId":"97eee97e","timestamp":"2026-07-17T06:57:42.462Z","message":{"role":"toolResult","toolCallId":"call_rPv8MsUYGn52opcykJ8Yg1Ps|fc_038f28d3e93bbfd4016a59d266417881969dd637c898e6c264","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n## Cargo.toml\n 5|publish = false\n*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) — the `spt` binary is a thin CLI over it. B0 lands the crate + the version...\n 7|\n 8|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n 9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n\n## src/\n### applyhost.rs\n 34|//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n*35|//!    subscriber while the live brain kept running the OLD code.\n 36|//!\n 37|//! New code runs **immediately** — the supervised respawn lands on the new\n 38|//! executable, no manual bounce, no logon. Readiness-gated promotion +\n...\n 134|    // failed readiness on this node and auto-rolled-back is quarantined — never\n*135|    // re-applied, or the self-heal becomes a self-harm loop (apply → fail → roll\n 136|    // back → re-apply → …). The `RolledBack` record is the quarantine marker; it\n 137|    // clears naturally when a newer version stages (N+1 ≠ the quarantined N). An\n 138|    // operator override (`--force`) to re-apply a genuinely-good version whose\n...\n 270|    // reached `ready`. Writing `applied` here is the optimism the design indicts\n*271|    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n 272|    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n 273|    // carrying the last-known-good `rollback_binary` (the aside we just created)\n 274|    // so the broker's supervisor, a different process from this CLI, can promote\n...\n 331|    // level. This replaces the old in-process `apply_brain_only` handoff (the\n*332|    // regression: it re-attached a CLI-side subscriber while the live brain kept\n 333|    // running the OLD code). `classify` already gated this as brain-only, so the\n 334|    // broker never moves; only the brain restarts.\n 335|    //\n...\n 352|/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n*353|/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n 354|/// broker and raise the same supervisor brain-restart signal the post-swap\n 355|/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n 356|/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n...\n 362|/// (accepted, not acted on).\n*363|// [impl->REQ-DAEMON-REFRESH]\n 364|pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365|    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366|    brain\n...\n 461|        }\n*462|        std::thread::sleep(Duration::from_millis(10));\n 463|    }\n 464|    Err(last)\n 465|}\n...\n 560|            }\n*561|            std::thread::sleep(Duration::from_millis(10));\n 562|        }\n 563|        panic!(\"brain cold-start could not connect\");\n 564|    }\n...\n 822|\n*823|    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n 824|    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n 825|    // staged release, NO release cache, NO binary swap. The verb has no staging\n 826|    // preconditions: the signal raises and `honored` reports the supervisor\n...\n 832|\n*833|        // No supervisor wired: the refresh round-trips and reports not-honored.\n 834|        assert!(\n*835|            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n 836|            \"no supervisor wired must report honored=false\"\n 837|        );\n 838|\n*839|        // Supervisor wired: refresh raises the same planned-restart signal the\n 840|        // apply path rides — with nothing staged anywhere.\n 841|        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n 842|        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 843|        assert!(\n*844|            refresh_brain(&name).expect(\"refresh round-trips\"),\n 845|            \"a wired supervisor must report honored=true\"\n 846|        );\n 847|        assert!(\n 848|            signal.take(),\n*849|            \"refresh must raise the planned-restart signal the supervisor consumes\"\n 850|        );\n 851|    }\n 852|\n...\n 1055|    // [unit->REQ-UPD-6] Broker-touching debug/update-set payloads are accepted\n*1056|    // as staged candidates, but apply refuses them typed while the broker-held\n 1057|    // runtime is live. The first broker-touching flow is refusal-only; it does\n 1058|    // not auto-suspend or cycle hosted work.\n 1059|    #[test]\n...\n 1139|    // is refused typed at the apply gate — never re-applied (no self-heal →\n*1140|    // self-harm loop), seat untouched. The guard is version-keyed, so a NEWER\n 1141|    // staged version is not quarantined (the marker clears naturally at N+1).\n 1142|    #[test]\n 1143|    fn apply_staged_refuses_a_quarantined_version() {\n### attach.rs\n 18|//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n*19|//! and *discards* everything interleaved — inside a full-duplex pump loop that\n 20|//! eats the operator's `Input` records (both sides starve; found the hard way\n 21|//! in `tests/attach.rs`). Idempotency holds without it because each direction\n 22|//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n...\n 57|/// viewport ending detaches it (active → dormant; the transition echo is\n*58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n 59|/// [`mark_driven_by`]: a session served without a perch has no resting\n 60|/// record to move.\n 61|// [impl->REQ-INST-3]\n...\n 65|\n*66|/// Why a serve loop returned.\n 67|#[derive(Debug, Clone, PartialEq, Eq)]\n 68|pub enum AttachServeOutcome {\n 69|    /// The access gate refused the requesting origin, OR a `Control` attach hit\n...\n 85|\n*86|/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n 87|/// arrives — gates wake (controller-only) and PTY input.\n 88|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 89|enum ServeRole {\n...\n 99|/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n*100|/// it, the operator sees an intermittent LIVE burst each interval (forward\n*101|/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\n 102|const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n 103|\n 104|/// The CONTROLLER fell behind the live ring further than the ring holds — the\n...\n 106|/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n*107|/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n 108|/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n 109|/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n 110|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n...\n 172|    let mut swallow_resume_ack = false;\n*173|    loop {\n 174|        match brain.read_event() {\n 175|            // Our own re-subscribe's Subscribed reply: swallow it (role is already\n 176|            // resolved) so it cannot mask a repeated gap as forward progress.\n...\n 283|/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n*284|/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n 285|/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n*286|/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n*287|/// round-trip inside the event loop would swallow interleaved attach records\n*288|/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n 289|/// the wire `session_id`.\n 290|///\n 291|/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\n...\n 321|    // forwards rode `brain` too, that stuck handler could not read them →\n*322|    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n 323|    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n 324|    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n 325|    // two directions on independent single-threaded handlers cannot block each\n...\n 329|    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n*330|    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n 331|    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n 332|    // the controller onto that async path would remove the inline-replay block at\n 333|    // the source, but it touches the authoritative cursor-advancing path.)\n...\n 360|    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\n*361|    // BEFORE the event loop, through the exact wire-Request path — a rolled\n 362|    // ring can no longer replay it. Non-Request seeds are ignored (the opener\n 363|    // of an attach stream is always its Request).\n 364|    if let Some(AttachRecord::Request {\n...\n 391|\n*392|    loop {\n 393|        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n 394|        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n 395|        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n...\n 443|                            // The dispatcher already resolved endpoint_id→session\n*444|                            // (passed as remote_session) BEFORE this loop; ignore\n*445|                            // the wire copy here (a mid-loop re-resolve would swallow\n 446|                            // interleaved attach records — see remote_session docs).\n 447|                            endpoint_id: _,\n 448|                            gen: req_gen,\n...\n 513|\n*514|            // ── Broker → loop: the resolved controller/viewer role. ─────────\n 515|            BrokerEvent::Subscribed { outcome, .. } => {\n 516|                use crate::msg::SubscribeOutcome as O;\n 517|                match outcome {\n...\n 553|                // No dormant edge: the endpoint stays driven by the taker (whose\n*554|                // own serve loop already fired the wake).\n 555|                return Ok(AttachServeOutcome::Displaced);\n 556|            }\n 557|\n### autostart.rs\n 22|/// replay thread starts alongside the broker's serve thread, so the very first\n*23|/// connect can race the accept loop up. Bounded and short: a genuine launch\n 24|/// failure (dead adapter binary, bad template) fails again instantly and we\n 25|/// move on after the last attempt.\n 26|const LAUNCH_ATTEMPTS: u32 = 3;\n...\n 104|                    if attempt < LAUNCH_ATTEMPTS {\n*105|                        std::thread::sleep(LAUNCH_RETRY_DELAY);\n 106|                    }\n 107|                }\n 108|            }\n...\n 190|            // Must return promptly — the skip happens before the bogus socket is\n*191|            // ever dialed, so no retry loop, no launch, no hang.\n 192|            replay_bounded(Duration::from_secs(10));\n 193|        });\n 194|    }\n### brain.rs\n 196|/// single-threaded; a brain-IPC read that blocks forever on a black-holed peer\n*197|/// wedges the whole loop (the 2.2h hfenduleam stall). The deadline must be\n 198|/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n 199|/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n 200|/// → deprecated `PIPE_NOWAIT`, which corrupts mid-stream — proven by the mesh\n...\n 237|                eprintln!(\"PUMP_IPC_READER: spawned\");\n*238|                loop {\n 239|                    let framed = read_frame(&mut recv);\n 240|                    let was_err = framed.is_err();\n 241|                    // Receiver gone (Brain dropped) → stop reading.\n...\n 298|    /// brain-IPC read waits forever, which is correct everywhere EXCEPT the\n*299|    /// single-threaded pump, where a black-holed peer would wedge the whole loop\n 300|    /// (KNOWN-HAZARDS — a blocked brain-IPC read must never wedge the pump). The\n 301|    /// two facts are coupled at construction: `Some` ⇔ [`BrainConn::Split`].\n 302|    io_timeout: Option<Duration>,\n...\n 455|    ///\n*456|    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\n 457|    /// calls [`Brain::read_event`] on every interleaved frame, so on a *resume-mode*\n 458|    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\n 459|    /// **different** session read here is cursor-processed (its cursor snaps; the\n...\n 470|        )?;\n*471|        loop {\n 472|            match self.read_event()? {\n 473|                BrokerEvent::Spawned { session_id, pid } => {\n 474|                    self.session_id = Some(session_id);\n...\n 528|    /// brain↔broker conn without returning to read. With no ack the broker's\n*529|    /// per-conn handler never writes back while servicing that flood, so the return\n 530|    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the\n 531|    /// broker still dedups by `(session, op_id)` at its applied-set.\n 532|    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n...\n 583|    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n*584|        loop {\n 585|            if contains(buf, needle) {\n 586|                return Ok(self.next_seq.saturating_sub(1));\n 587|            }\n...\n 607|    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n*608|    /// entry (a whole `loop { read_event }` shares it, so unrelated events\n 609|    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n 610|    /// pump mode (blocking, unbounded).\n 611|    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n...\n 636|    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n*637|        loop {\n 638|            let env = self.read_frame_until(deadline)?;\n 639|            match env.kind.as_str() {\n 640|                KIND_OUTPUT => {\n...\n 918|    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]\n*919|    /// loop) reads to learn whether it became controller / viewer / was refused.\n 920|    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the\n 921|    /// generation is untouched).\n 922|    ///\n...\n 986|    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n*987|    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n 988|    /// recovers POST-eviction (re-subscribe from the floor). VIEWER-only →\n 989|    /// B2-SAFE (a viewer never advances `delivered_through` / is not authoritative\n 990|    /// — the CONTROLLER keeps the strict reject-gap for exactly-once resume). The\n...\n 1118|        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;\n*1119|        loop {\n 1120|            match self.read_event()? {\n 1121|                BrokerEvent::NetStatus(s) => return Ok(s),\n 1122|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1139|        self.send(KIND_BROKER_IMAGE, serde_json::Value::Null)?;\n*1140|        loop {\n 1141|            let env = self.read_frame_until(None)?;\n 1142|            match env.kind.as_str() {\n 1143|                KIND_BROKER_IMAGE_REPLY => {\n...\n 1165|        self.send(KIND_STALL_EVICTS, serde_json::Value::Null)?;\n*1166|        loop {\n 1167|            let env = self.read_frame_until(None)?;\n 1168|            match env.kind.as_str() {\n 1169|                KIND_STALL_EVICTS_REPLY => {\n...\n 1192|        )?;\n*1193|        loop {\n 1194|            let env = self.read_frame_until(None)?;\n 1195|            match env.kind.as_str() {\n 1196|                KIND_APPLIED => return Ok(()),\n...\n 1228|        )?;\n*1229|        loop {\n 1230|            match self.read_event()? {\n 1231|                BrokerEvent::PairJoined(r) => return Ok(r),\n 1232|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1253|        )?;\n*1254|        loop {\n 1255|            match self.read_event()? {\n 1256|                BrokerEvent::MetMember(m) => return Ok(MetOutcome::Met(m)),\n 1257|                BrokerEvent::PairJoined(r) => return Ok(MetOutcome::Failed(r)),\n...\n 1277|        )?;\n*1278|        loop {\n 1279|            match self.read_event()? {\n 1280|                BrokerEvent::PairJoined(r) => return Ok(r),\n 1281|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1307|        let deadline = self.call_deadline();\n*1308|        loop {\n 1309|            match self.read_event_until(deadline)? {\n 1310|                BrokerEvent::NetDialed(d) => return Ok(d),\n 1311|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1337|        let deadline = self.call_deadline();\n*1338|        loop {\n 1339|            match self.read_event_until(deadline)? {\n 1340|                BrokerEvent::NetDialSubmitted => return Ok(()),\n 1341|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n### brainproc.rs\n 41|/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n*42|/// never a hot respawn loop.\n 43|const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n 44|/// A run at least this long counts as healthy and resets the backoff floor.\n 45|/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n...\n 55|const TRIAL_PREREADY_BUDGET: u32 = 3;\n*56|/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n*57|const TICK: Duration = Duration::from_millis(200);\n 58|\n 59|/// How long the brain child retries connecting to the broker socket before\n 60|/// giving up (the broker may still be binding — the child is freshly spawned).\n...\n 107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n*108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n 110|/// between the broker serve path and the supervisor — both run in the broker\n 111|/// process. This is the seam that makes a routine `apply` actually run new code,\n...\n 171|    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n*172|    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n 173|    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n 174|    // *counter* is rehydrated, never the wall-clock stamp.\n 175|    eprintln!(\n...\n 223|    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n*224|    // online/offline exclusivity — a reconcile loop that runs each offline\n 225|    // instance's `wake_command` as a supervised watcher child. Relocated from\n 226|    // the broker process to the brain child: shellwake is brain-owned logic\n 227|    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n...\n 238|    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n*239|    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\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...\n 258|    let mut consumers_started = false;\n*259|    loop {\n*260|        thread::sleep(BRAIN_HEARTBEAT);\n 261|        // `net-status` is both the net-capability probe and the broker-liveness\n 262|        // probe: an `Err` is the broker gone (or a future planned\n 263|        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n...\n 279|        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n*280|        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n 281|        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n 282|        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283|        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n...\n 289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n*290|        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291|        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292|        // net_status probe above already owns broker-liveness). Self-applies: it drives\n 293|        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n...\n 334|\n*335|/// Connect to the broker, retrying briefly while it finishes binding.\n 336|fn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n 337|    let mut last: Option<io::Error> = None;\n 338|    for _ in 0..tries {\n...\n 342|                last = Some(e);\n*343|                thread::sleep(Duration::from_millis(10));\n 344|            }\n 345|        }\n 346|    }\n...\n 438|/// deterministically with a scripted ready/no-ready + a capturing notif sink,\n*439|/// while production binds the real `spt_home` + relcache + notif sender\n 440|/// ([`ProductionTrialEnv`]). Every method is **best-effort / fail-safe**: a\n 441|/// missing record reads as \"no pending trial\", an absent ready file as\n 442|/// \"not ready\" — the broker's supervisor thread never panics (a panic there\n...\n 486|/// ready\" and every effect is a no-op. Used where there is no readiness gate to\n*487|/// drive — the supervisor then behaves exactly as the pre-D6 respawn loop.\n 488|pub struct NoTrialEnv;\n 489|\n 490|impl TrialEnv for NoTrialEnv {\n...\n 507|        // No readiness gate to drive here → no old-gen control plane to drain; the\n*508|        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n 509|        // no-op effects).\n 510|        true\n 511|    }\n...\n 594|/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n*595|/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n 596|/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n 597|/// any future broker-side live-notif is cross-process serialization (a file\n 598|/// lock), not relaxing this rule.\n...\n 624|\n*625|/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n 626|/// promptly mid-backoff (saturating — never an underflow panic).\n 627|fn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n 628|    let mut slept = Duration::ZERO;\n*629|    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630|        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n### broker.rs\n 23|//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n*24|//!   live. The brain dedups by `seq`. Output produced *while no brain was\n 25|//!   attached* sits in the ring and is replayed on the next subscribe — so a\n 26|//!   brain restart loses nothing.\n 27|//!\n...\n 167|/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n*168|/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n*169|/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n 170|/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 171|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 172|const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n...\n 197|const WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n*198|/// Poll slice while waiting on a racing wake's session to appear.\n 199|const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 200|\n 201|/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n...\n 280|/// doyle non-negotiable = W1's \"never park forever\"): the inject worker holds the\n*281|/// floor — buffering operator input — only while a binary's response to one\n 282|/// `{type:event}` is in flight. If no `{commit}` arrives within this window the\n 283|/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no\n 284|/// respawn → idle inbound then SPOOLS, never raw-injected), and never re-opens. A wedged or\n...\n 337|/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n*338|/// psyche crash-loop backoff's bounded give-up.\n 339|const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n 340|\n 341|/// The effective respawn budget. Production is the const above; the int gate tunes\n...\n 367|/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n*368|/// returns in a tick or two, well under this.\n 369|const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n*370|/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n 371|const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372|/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373|/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n...\n 512|    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n*513|    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n 514|    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n 515|    /// floor). Seeded `now` at [`OutputLog::become_controller`].\n 516|    last_ok: Instant,\n...\n 542|/// an isolated bounded queue + writer thread, evicted on overflow). Every\n*543|/// controller send and viewer `try_send` happens while the enclosing\n 544|/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\n 545|struct OutputLog {\n 546|    session_id: u64,\n...\n 628|/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\n*629|/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\n 630|// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n 631|fn stamp_divergence(\n 632|    rec_driven_by: Option<&str>,\n...\n 649|/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\n*650|/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\n 651|/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\n 652|/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\n 653|// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n...\n 728|    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n*729|    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n 730|    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n*731|    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n 732|    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n 733|    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n 734|    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n...\n 752|    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*753|    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n 754|    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 755|    fn clamp_or_reset(&mut self) -> bool {\n 756|        let within_cap = self.ring.len() <= self.cap_chunks;\n...\n 804|        self.ring.push_back((seq, bytes.to_vec()));\n*805|        while self.ring.len() > self.cap_chunks {\n 806|            self.ring.pop_front();\n 807|        }\n 808|        let frame = output_envelope(self.session_id, seq, bytes);\n...\n 830|        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n*831|        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n 832|        // slow controller falls behind the ring) unless it has been Full for the\n 833|        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n 834|        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n...\n 889|    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n*890|    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n 891|    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n 892|    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n 893|    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n...\n 897|    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64, attach_gen: u64) {\n*898|        // Drop the prior controller sink first (its writer's live loop ends when\n 899|        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n 900|        // for the old controller can't unseat this one and (b) a prior writer\n 901|        // still flushing its initial batch returns before its next frame.\n...\n 975|    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n*976|    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n 977|    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n### codec.rs\n 9|//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n*10|//! contract, while [`crate::transport`] owns the OS-specific connection.\n 11|// [impl->REQ-DAEMON-2]\n 12|\n 13|use std::io::{self, Read, Write};\n### config.rs\n 5|//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n*6|//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n 7|//! period is **durable and configurable**, sourced from a small JSON file under\n 8|//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n 9|//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n*10|//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n 11|//! from config, never a constant.\n 12|// [impl->REQ-DAEMON-1]\n 13|\n...\n 30|/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n*31|/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n 32|/// teardown for long. Configurable per machine via `crash_grace_ms`.\n 33|pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n 34|\n...\n 36|/// registry/notif feeds to trusted peers and pulls context syncs from them.\n*37|/// Conservative defaults — every loop is a thin call over an idempotent\n*38|/// driver, so a missed tick costs only latency, never correctness.\n 39|pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40|pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41|pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n...\n 45|/// authored by a node not heard within this window are evicted on the next\n*46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n 47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n...\n 132|pub struct DaemonConfig {\n*133|    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).\n 134|    pub pulse_period: Duration,\n 135|    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136|    pub protection_window_ms: u64,\n...\n 156|    /// node not heard within this window are evicted on the next registry\n*157|    /// pump tick — a dead node identity must not poison bare-id resolution\n 158|    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n 159|    pub registry_evict_after: Duration,\n 160|    /// Notif feed push cadence (D9-1).\n 161|    pub notif_pump_period: Duration,\n*162|    /// Context sync pull cadence — the anti-entropy loop (D9-1).\n 163|    pub sync_pull_period: Duration,\n 164|    /// Update check cadence (D9-1; the pull rides the same trusted conns).\n 165|    pub update_check_period: Duration,\n### conn.rs\n 32|//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n*33|//! completion APC makes the wedged `SleepEx` loop return with\n 34|//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n 35|//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n 36|//! the same abort (the stuck op it waited behind is the wedge).\n...\n 46|//! synchronous return IS the completion handshake); (4) the retired half is\n*47|//! parked in a graveyard slot — never dropped while the watchdog might still\n 48|//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n 49|//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n 50|//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n...\n 132|struct Dog {\n*133|    /// `Some((op_id, deadline))` while a write is in flight.\n 134|    inflight: Option<(u64, Instant)>,\n 135|    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136|    shutdown: bool,\n...\n 337|\n*338|    /// The watchdog body: park until a write is in flight, then sleep to its\n 339|    /// absolute deadline. If the op is still in flight at the deadline,\n 340|    /// poison + cancel, re-canceling periodically until the op reports\n 341|    /// completion (the writer clears `inflight` the instant `write_frame`\n...\n 344|        let mut d = recover(&self.dog);\n*345|        loop {\n 346|            // Park until armed (or shutdown).\n*347|            let (op, deadline) = loop {\n 348|                if d.shutdown {\n 349|                    return;\n 350|                }\n...\n 358|            };\n*359|            // Sleep toward the deadline while THIS op stays in flight.\n*360|            let fired = loop {\n 361|                if d.shutdown {\n 362|                    return;\n 363|                }\n...\n 391|            d = recover(&self.dog);\n*392|            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n 393|                let (g, _) = self\n 394|                    .dog_cv\n 395|                    .wait_timeout(d, Duration::from_millis(100))\n...\n 511|        let mut g = recover(&inner.gate);\n*512|        loop {\n 513|            if inner.poisoned.load(Ordering::Acquire) {\n 514|                return Err(poisoned_err());\n 515|            }\n...\n 564|        // released. Clear the arm BEFORE deciding health, so the watchdog\n*565|        // never severs while the op is formally in flight.\n 566|        {\n 567|            let mut d = recover(&inner.dog);\n 568|            d.inflight = None;\n...\n 763|        // A first write can land in the OS buffer before the peer-death is\n*764|        // observable; loop until the failure surfaces (bounded).\n 765|        for _ in 0..200 {\n 766|            match conn.write(&env) {\n 767|                Err(e) => {\n### crc_swap.rs\n 9|//! hash differs (so identical files — and their still-running binaries — are\n*10|//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n 11|//! so a mid-swap failure never strands the install half-new (doyle W3 gate,\n 12|//! binding partial-failure refinement).\n 13|//!\n...\n 85|/// 1. **Stage** — copy every change to `<target>.new` (creating parent dirs).\n*86|/// 2. **Commit** — one tight loop: for each change, DISPLACE the existing original\n 87|///    to a FRESH `<target>.old[-N]` (if present) then rename `<target>.new` →\n 88|///    `<target>`.\n*89|/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n 90|///\n 91|/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n 92|/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n...\n 100|///\n*101|/// On ANY failure in the commit loop, the already-committed renames are **rolled\n 102|/// back** (restore each displaced original over `<target>`) and the error —\n 103|/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104|/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n...\n 113|/// unit-testable deterministically: the displace fix removes every rename-OVER-an-\n*114|/// existing-file (fresh names only), so no filesystem fixture can force a mid-loop\n 115|/// commit failure any more — the seam supplies one instead. Production passes\n 116|/// `std::fs::rename`.\n 117|// [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n...\n 147|\n*148|    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n 149|    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150|    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151|    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n...\n 178|\n*179|    // Phase 3 — whole loop committed: drop the displaced originals (best-effort; a\n 180|    // still-mapped one stays but is uniquely named → the next run's Phase-0 GC\n 181|    // retries it, never a brick).\n 182|    for (_, displaced) in &committed {\n...\n 422|\n*423|    /// Never-strand invariant: a mid-loop rename failure rolls back the renames\n 424|    /// that already committed. Because the C1 displace fix removes every\n 425|    /// rename-OVER-an-existing-file (fresh `.old[-N]` names only), no filesystem\n*426|    /// fixture can force a mid-loop commit failure any more — so we drive\n 427|    /// [`apply_crc_swap_with`] with an INJECTED rename that fails on the SECOND\n 428|    /// target's `.new` commit, AFTER swap 1 has landed. After the Err, the first\n 429|    /// target must hold its ORIGINAL bytes (rollback restored it, NOT the staged\n...\n 445|        // Swap 2: an ordinary existing target file whose `.new` commit rename the\n*446|        // injected seam forces to fail — landing the error mid-loop, after swap 1.\n 447|        let second_staged = s.join(\"second.src\");\n 448|        write(&second_staged, b\"second-bytes\");\n 449|        let second_target = i.join(\"second\");\n...\n 466|        // into place (`rename(second.new, second)`), which fails — the deterministic,\n*467|        // cross-platform mid-loop failure the displace fix otherwise makes impossible.\n 468|        let second_new = new_path(&second_target);\n 469|        let second_target_c = second_target.clone();\n 470|        let rename = move |from: &Path, to: &Path| -> std::io::Result<()> {\n...\n 481|\n*482|        assert!(result.is_err(), \"the injected mid-loop commit failure propagates\");\n 483|        assert_eq!(\n 484|            fs::read(&first_target).unwrap(),\n 485|            b\"BEFORE\",\n...\n 549|        #[cfg(target_os = \"linux\")]\n*550|        let image = std::path::PathBuf::from(\"/bin/sleep\");\n 551|        if !image.exists() {\n 552|            return; // no reference image on this host — inconclusive, not a fail\n 553|        }\n...\n 566|        // Give the OS a beat to map the image.\n*567|        std::thread::sleep(std::time::Duration::from_millis(150));\n 568|\n 569|        let plan = plan_crc_swap(s, i).unwrap();\n 570|        let result = apply_crc_swap(&plan);\n...\n 661|            stranded_old.exists() && fs::read(&stranded_old).unwrap() == b\"LAST-GOOD-V1\",\n*662|            \"the last-good `.old` is NOT GC'd while the target was missing — survives \\\n 663|             to be swept next run (never a no-binary double-fault window)\"\n 664|        );\n 665|    }\n### daemon.rs\n 35|const START_TIMEOUT: Duration = Duration::from_secs(5);\n*36|/// Poll cadence while waiting for the spawned daemon to come up.\n 37|const START_POLL: Duration = Duration::from_millis(25);\n 38|\n 39|/// The per-machine broker/brain daemon process.\n...\n 43|    /// Run the daemon: bind the broker (served on a background thread) + the\n*44|    /// seed-control channel (the foreground loop + liveness anchor) and serve\n 45|    /// until the process is killed. The body of `spt daemon`.\n 46|    pub fn run() -> io::Result<()> {\n 47|        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n...\n 272|        // nothing — the slot is ephemeral control, a restart correctly drops it).\n*273|        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n 274|        // [impl->REQ-SHELL-3]\n 275|        let drive_hub = Arc::new(crate::drivehub::DriveHub::new());\n 276|        {\n...\n 351|\n*352|        // The seed-control channel is the foreground loop. Binding it is what\n 353|        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n 354|        // to start fails this bind and exits, leaving the winner serving. It\n 355|        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n...\n 357|\n*358|        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n 359|        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n 360|        // does not respawn the brain we are about to kill) THEN reap the brain\n 361|        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n...\n 441|    let mut backoff = NET_RETRY_FIRST;\n*442|    loop {\n*443|        thread::sleep(backoff);\n 444|        if let Some(host) = try_start_net() {\n 445|            if broker.attach_net(host) {\n 446|                eprintln!(\"NET_ATTACHED: net endpoint bound on retry — brain starts the consumers\");\n...\n 470|/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the\n*471|/// daemon's foreground loop returns and the process exits cleanly.\n 472|// [impl->REQ-CLI-2]\n 473|pub fn request_stop() -> io::Result<()> {\n 474|    seedmap::request_stop(&seed_socket_name())\n...\n 482|    let mut waited = Duration::ZERO;\n*483|    while waited < START_TIMEOUT {\n 484|        if is_running() {\n 485|            return Ok(());\n 486|        }\n*487|        thread::sleep(START_POLL);\n 488|        waited += START_POLL;\n 489|    }\n 490|    Err(io::Error::new(\n...\n 501|/// daemon that would fight the service for the broker socket (the kitsubito\n*502|/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n 503|// [impl->REQ-DAEMON-8]\n 504|pub fn ensure_running() -> io::Result<()> {\n 505|    if is_running() {\n...\n 929|/// the real per-rung launchers and log which rung won. The caller still confirms\n*930|/// readiness via the existing `brain.ready` poll — the returned pid is for\n 931|/// logging/return, not readiness (WMI/schtasks pids are the launcher's view).\n 932|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 933|#[cfg(windows)]\n...\n 1185|/// is recoverable from schtasks → returns a sentinel; readiness is the caller's\n*1186|/// `brain.ready` poll. This is the FALLBACK rung (WMI is primary); its `/tr`\n 1187|/// quoting is best-effort.\n 1188|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1189|#[cfg(windows)]\n...\n 1225|    }\n*1226|    // No pid from schtasks; sentinel — readiness is the brain.ready poll.\n 1227|    Ok(0)\n 1228|}\n 1229|\n...\n 1299|    // and caps at NET_RETRY_CAP — so a long boot-race wait settles to a steady\n*1300|    // 30s poll, never an ever-growing or zero-length spin.\n 1301|    #[test]\n 1302|    fn net_retry_backoff_doubles_then_caps() {\n 1303|        assert_eq!(net_retry_backoff(NET_RETRY_FIRST), Duration::from_secs(2));\n...\n 1533|            std::fs::write(&pidfile, gc.to_string()).expect(\"launcher: write gc pid\");\n*1534|            std::thread::sleep(Duration::from_secs(300));\n 1535|            std::process::exit(0);\n 1536|        }\n 1537|\n...\n 1605|            let mut found = None;\n*1606|            while Instant::now() < deadline {\n 1607|                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608|                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609|                        if p != 0 {\n...\n 1614|                }\n*1615|                std::thread::sleep(Duration::from_millis(20));\n 1616|            }\n 1617|            found.expect(\"launcher reports its grandchild pid\")\n 1618|        };\n...\n 1625|        for _ in 0..25 {\n*1626|            std::thread::sleep(Duration::from_millis(40));\n 1627|            if !is_process_alive(gc) {\n 1628|                still_alive = false;\n 1629|                break;\n...\n 1711|                }\n*1712|                thread::sleep(Duration::from_millis(5));\n 1713|            }\n 1714|            assert!(is_running(), \"the seed channel answers → daemon is up\");\n 1715|            // ensure_running short-circuits (no real process spawn).\n### deadline.rs\n*1|//! Durable absolute-deadline loop timing — ADR-0018 Q4 / restoration D5.\n 2|//!\n*3|//! Phase-significant periodic timing (the pulse loop) and one-shot deadlines\n 4|//! (alarms) must survive a brain swap: the supervisor respawns a fresh brain\n 5|//! (`Brain::cold_start`) and the outgoing brain is gone before the new one starts\n 6|//! (D3-3), so continuity cannot ride a brain→brain frame — it must come from the\n...\n 10|//!\n*11|//! - [`DeadlineAnchor`] — a **periodic** `(anchor, interval)` grid. Persisted\n 12|//!   ONCE per fresh/crash start; every fire is derived functionally\n 13|//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n 14|//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n 15|//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n*16|//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n 17|//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n 18|//!   IPC round-trip.\n 19|//!\n...\n 31|//! (`pump`) need none — they already stagger from everything-due-now,\n*32|//! catch-up/idempotent/restart-safe; converting them would re-add the per-loop\n 33|//! writes Q4 minimizes.\n 34|\n 35|use std::path::{Path, PathBuf};\n...\n 41|\n*42|/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n 43|///\n 44|/// Keyed, not a singleton (ADR-0018 D5 amendment 6): a singleton would\n 45|/// cross-clobber the moment the live-agent adapter's **per-agent** pulse drivers\n...\n 65|impl DeadlineAnchor {\n*66|    /// Resolve a non-zero interval (doyle minor — ONE degrade rule, unit-tested):\n 67|    /// a zero `interval_ms` degrades to the config default pulse period (a `const`\n 68|    /// that is always `> 0`), so the derive can never divide by zero. The `else\n 69|    /// 1ms` floor is belt-and-braces should that default ever be made zero.\n...\n 81|    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n*82|    ///   only the interval refreshes from live config. If no file exists (updating\n 83|    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n 84|    ///   — never fail an update on a missing file.\n 85|    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n...\n 100|        let anchor = match (reason, Self::load(&path)) {\n*101|            // Update keeps the persisted phase; the interval refreshes to config.\n 102|            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103|                anchor_ms: prev.anchor_ms,\n 104|                interval_ms,\n...\n 133|    /// The next fire at or after `now_ms`, derived functionally — **NO write**:\n*134|    /// `anchor + interval · ⌈max(0, now − anchor) / interval⌉`.\n 135|    ///\n 136|    /// On an exact grid instant this returns `now` (fire-now); a long jump\n 137|    /// (`now ≫ anchor`) snaps to the next grid point, collapsing missed ticks to\n...\n 148|\n*149|    /// The grid point **strictly after** `fired_ms` — the loop's post-fire\n 150|    /// advance (doyle minor — structural double-fire guard). Deriving the next\n 151|    /// deadline from the FIRED grid point rather than a re-sampled `now` means a\n*152|    /// fast tick re-sampling within the same millisecond cannot re-yield the same\n*153|    /// instant and double-fire. The loop body must advance via this, not via a\n*154|    /// fresh `now` alone, making the guard an invariant of the loop, not just an\n 155|    /// assertion in a test.\n 156|    pub fn next_after(&self, fired_ms: u64) -> u64 {\n 157|        self.next_fire(fired_ms.saturating_add(1))\n...\n 160|    /// The next deadline after firing at `fired_ms`, observed at `now_ms` — the\n*161|    /// loop's post-fire advance (doyle amendment 7). Combines BOTH invariants the\n*162|    /// loop needs in one pure expression so neither can be lost:\n 163|    ///\n 164|    /// - **strictly after the fired grid point** (the double-fire guard: an\n*165|    ///   instant tick where `now == fired` still steps exactly one grid);\n 166|    /// - **skips past `now`** so a long stall — a `pulse_tick` that outran the\n*167|    ///   interval, or the real killer, a laptop suspend/resume with the daemon\n 168|    ///   resident — collapses to **exactly one** catch-up fire instead of N rapid\n 169|    ///   back-to-back fires. (`next_after(deadline)` alone steps a single grid per\n 170|    ///   fire, so a deadline N grids behind `now` would fire N times in a tight\n*171|    ///   loop — the exact storm the loop-entry `next_fire(now)` collapse promises\n 172|    ///   to avoid; this composes with that collapse so a suspend/resume fires once\n 173|    ///   total.)\n 174|    pub fn advance_after_fire(&self, fired_ms: u64, now_ms: u64) -> u64 {\n...\n 226|        assert_eq!(a.next_fire(1_300), 1_300);\n*227|        // The post-fire advance must move STRICTLY forward by one interval, so a\n 228|        // re-sample at the same instant cannot double-fire (structural guard).\n 229|        assert_eq!(a.next_after(1_300), 1_400);\n 230|        assert_eq!(a.next_after(1_000), 1_100);\n### deelevate.rs\n 81|/// (still carrying `SPT_HOME`) sees no daemon and prints `DAEMON_NOT_RUNNING`\n*82|/// while the orphaned daemon serves the wrong home.\n 83|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n 84|pub fn apply_env_overrides(block: &[u16], overrides: &[(String, String)]) -> Vec<u16> {\n 85|    const NUL: u16 = 0;\n...\n 213|        let mut buf = vec![0u8; buf_len as usize];\n*214|        loop {\n 215|            let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };\n 216|            let mut result: *mut libc::passwd = std::ptr::null_mut();\n 217|            let rc = unsafe {\n...\n 332|        let mut buf = vec![0u8; buf_len as usize];\n*333|        loop {\n 334|            let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };\n 335|            let mut result: *mut libc::passwd = std::ptr::null_mut();\n 336|            let rc = unsafe {\n...\n 716|        let mut n = 0usize;\n*717|        while !(*p.add(n) == 0 && *p.add(n + 1) == 0) {\n 718|            n += 1;\n 719|        }\n\n[Showing lines 1-958 of 1175 (49.9KB limit). Use :959 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"crates/spt-daemon","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":222,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/codec.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/drivehub.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":1},{"path":"crates/spt-daemon/src/applyhost.rs","count":16},{"path":"crates/spt-daemon/src/attach.rs","count":20},{"path":"crates/spt-daemon/src/autostart.rs","count":3},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":2},{"path":"crates/spt-daemon/src/brain.rs","count":20},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/codec.rs","count":1},{"path":"crates/spt-daemon/src/config.rs","count":9},{"path":"crates/spt-daemon/src/conn.rs","count":12},{"path":"crates/spt-daemon/src/crc_swap.rs","count":15},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/deadline.rs","count":20},{"path":"crates/spt-daemon/src/deelevate.rs","count":4},{"path":"crates/spt-daemon/src/digest.rs","count":2},{"path":"crates/spt-daemon/src/digesthub.rs","count":6},{"path":"crates/spt-daemon/src/dispatch.rs","count":20},{"path":"crates/spt-daemon/src/docshost.rs","count":2},{"path":"crates/spt-daemon/src/drivehub.rs","count":9}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n## Cargo.toml\n 5│publish = false\n*6│description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) — the `spt` binary is a thin CLI over it. B0 lands the crate + the version...\n 7│\n 8│# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n 9│# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n\n## src/\n### applyhost.rs\n   34│//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n  *35│//!    subscriber while the live brain kept running the OLD code.\n   36│//!\n   37│//! New code runs **immediately** — the supervised respawn lands on the new\n   38│//! executable, no manual bounce, no logon. Readiness-gated promotion +\n     │...\n  134│    // failed readiness on this node and auto-rolled-back is quarantined — never\n *135│    // re-applied, or the self-heal becomes a self-harm loop (apply → fail → roll\n  136│    // back → re-apply → …). The `RolledBack` record is the quarantine marker; it\n  137│    // clears naturally when a newer version stages (N+1 ≠ the quarantined N). An\n  138│    // operator override (`--force`) to re-apply a genuinely-good version whose\n     │...\n  270│    // reached `ready`. Writing `applied` here is the optimism the design indicts\n *271│    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n  272│    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n  273│    // carrying the last-known-good `rollback_binary` (the aside we just created)\n  274│    // so the broker's supervisor, a different process from this CLI, can promote\n     │...\n  331│    // level. This replaces the old in-process `apply_brain_only` handoff (the\n *332│    // regression: it re-attached a CLI-side subscriber while the live brain kept\n  333│    // running the OLD code). `classify` already gated this as brain-only, so the\n  334│    // broker never moves; only the brain restarts.\n  335│    //\n     │...\n  352│/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n *353│/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n  354│/// broker and raise the same supervisor brain-restart signal the post-swap\n  355│/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n  356│/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n     │...\n  362│/// (accepted, not acted on).\n *363│// [impl->REQ-DAEMON-REFRESH]\n  364│pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n  365│    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n  366│    brain\n     │...\n  461│        }\n *462│        std::thread::sleep(Duration::from_millis(10));\n  463│    }\n  464│    Err(last)\n  465│}\n     │...\n  560│            }\n *561│            std::thread::sleep(Duration::from_millis(10));\n  562│        }\n  563│        panic!(\"brain cold-start could not connect\");\n  564│    }\n     │...\n  822│\n *823│    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n  824│    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n  825│    // staged release, NO release cache, NO binary swap. The verb has no staging\n  826│    // preconditions: the signal raises and `honored` reports the supervisor\n     │...\n  832│\n *833│        // No supervisor wired: the refresh round-trips and reports not-honored.\n  834│        assert!(\n *835│            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n  836│            \"no supervisor wired must report honored=false\"\n  837│        );\n  838│\n *839│        // Supervisor wired: refresh raises the same planned-restart signal the\n  840│        // apply path rides — with nothing staged anywhere.\n  841│        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n  842│        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n  843│        assert!(\n *844│            refresh_brain(&name).expect(\"refresh round-trips\"),\n  845│            \"a wired supervisor must report honored=true\"\n  846│        );\n  847│        assert!(\n  848│            signal.take(),\n *849│            \"refresh must raise the planned-restart signal the supervisor consumes\"\n  850│        );\n  851│    }\n  852│\n     │...\n 1055│    // [unit->REQ-UPD-6] Broker-touching debug/update-set payloads are accepted\n*1056│    // as staged candidates, but apply refuses them typed while the broker-held\n 1057│    // runtime is live. The first broker-touching flow is refusal-only; it does\n 1058│    // not auto-suspend or cycle hosted work.\n 1059│    #[test]\n     │...\n 1139│    // is refused typed at the apply gate — never re-applied (no self-heal →\n*1140│    // self-harm loop), seat untouched. The guard is version-keyed, so a NEWER\n 1141│    // staged version is not quarantined (the marker clears naturally at N+1).\n 1142│    #[test]\n 1143│    fn apply_staged_refuses_a_quarantined_version() {\n### attach.rs\n  18│//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n *19│//! and *discards* everything interleaved — inside a full-duplex pump loop that\n  20│//! eats the operator's `Input` records (both sides starve; found the hard way\n  21│//! in `tests/attach.rs`). Idempotency holds without it because each direction\n  22│//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n    │...\n  57│/// viewport ending detaches it (active → dormant; the transition echo is\n *58│/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n  59│/// [`mark_driven_by`]: a session served without a perch has no resting\n  60│/// record to move.\n  61│// [impl->REQ-INST-3]\n    │...\n  65│\n *66│/// Why a serve loop returned.\n  67│#[derive(Debug, Clone, PartialEq, Eq)]\n  68│pub enum AttachServeOutcome {\n  69│    /// The access gate refused the requesting origin, OR a `Control` attach hit\n    │...\n  85│\n *86│/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n  87│/// arrives — gates wake (controller-only) and PTY input.\n  88│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n  89│enum ServeRole {\n    │...\n  99│/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n*100│/// it, the operator sees an intermittent LIVE burst each interval (forward\n*101│/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\n 102│const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n 103│\n 104│/// The CONTROLLER fell behind the live ring further than the ring holds — the\n    │...\n 106│/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n*107│/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n 108│/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n 109│/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n 110│// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n    │...\n 172│    let mut swallow_resume_ack = false;\n*173│    loop {\n 174│        match brain.read_event() {\n 175│            // Our own re-subscribe's Subscribed reply: swallow it (role is already\n 176│            // resolved) so it cannot mask a repeated gap as forward progress.\n    │...\n 283│/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n*284│/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n 285│/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n*286│/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n*287│/// round-trip inside the event loop would swallow interleaved attach records\n*288│/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n 289│/// the wire `session_id`.\n 290│///\n 291│/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\n    │...\n 321│    // forwards rode `brain` too, that stuck handler could not read them →\n*322│    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n 323│    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n 324│    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n 325│    // two directions on independent single-threaded handlers cannot block each\n    │...\n 329│    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n*330│    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n 331│    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n 332│    // the controller onto that async path would remove the inline-replay block at\n 333│    // the source, but it touches the authoritative cursor-advancing path.)\n    │...\n 360│    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\n*361│    // BEFORE the event loop, through the exact wire-Request path — a rolled\n 362│    // ring can no longer replay it. Non-Request seeds are ignored (the opener\n 363│    // of an attach stream is always its Request).\n 364│    if let Some(AttachRecord::Request {\n    │...\n 391│\n*392│    loop {\n 393│        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n 394│        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n 395│        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n    │...\n 443│                            // The dispatcher already resolved endpoint_id→session\n*444│                            // (passed as remote_session) BEFORE this loop; ignore\n*445│                            // the wire copy here (a mid-loop re-resolve would swallow\n 446│                            // interleaved attach records — see remote_session docs).\n 447│                            endpoint_id: _,\n 448│                            gen: req_gen,\n    │...\n 513│\n*514│            // ── Broker → loop: the resolved controller/viewer role. ─────────\n 515│            BrokerEvent::Subscribed { outcome, .. } => {\n 516│                use crate::msg::SubscribeOutcome as O;\n 517│                match outcome {\n    │...\n 553│                // No dormant edge: the endpoint stays driven by the taker (whose\n*554│                // own serve loop already fired the wake).\n 555│                return Ok(AttachServeOutcome::Displaced);\n 556│            }\n 557│\n### autostart.rs\n  22│/// replay thread starts alongside the broker's serve thread, so the very first\n *23│/// connect can race the accept loop up. Bounded and short: a genuine launch\n  24│/// failure (dead adapter binary, bad template) fails again instantly and we\n  25│/// move on after the last attempt.\n  26│const LAUNCH_ATTEMPTS: u32 = 3;\n    │...\n 104│                    if attempt < LAUNCH_ATTEMPTS {\n*105│                        std::thread::sleep(LAUNCH_RETRY_DELAY);\n 106│                    }\n 107│                }\n 108│            }\n    │...\n 190│            // Must return promptly — the skip happens before the bogus socket is\n*191│            // ever dialed, so no retry loop, no launch, no hang.\n 192│            replay_bounded(Duration::from_secs(10));\n 193│        });\n 194│    }\n### brain.rs\n  196│/// single-threaded; a brain-IPC read that blocks forever on a black-holed peer\n *197│/// wedges the whole loop (the 2.2h hfenduleam stall). The deadline must be\n  198│/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n  199│/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n  200│/// → deprecated `PIPE_NOWAIT`, which corrupts mid-stream — proven by the mesh\n     │...\n  237│                eprintln!(\"PUMP_IPC_READER: spawned\");\n *238│                loop {\n  239│                    let framed = read_frame(&mut recv);\n  240│                    let was_err = framed.is_err();\n  241│                    // Receiver gone (Brain dropped) → stop reading.\n     │...\n  298│    /// brain-IPC read waits forever, which is correct everywhere EXCEPT the\n *299│    /// single-threaded pump, where a black-holed peer would wedge the whole loop\n  300│    /// (KNOWN-HAZARDS — a blocked brain-IPC read must never wedge the pump). The\n  301│    /// two facts are coupled at construction: `Some` ⇔ [`BrainConn::Split`].\n  302│    io_timeout: Option<Duration>,\n     │...\n  455│    ///\n *456│    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\n  457│    /// calls [`Brain::read_event`] on every interleaved frame, so on a *resume-mode*\n  458│    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\n  459│    /// **different** session read here is cursor-processed (its cursor snaps; the\n     │...\n  470│        )?;\n *471│        loop {\n  472│            match self.read_event()? {\n  473│                BrokerEvent::Spawned { session_id, pid } => {\n  474│                    self.session_id = Some(session_id);\n     │...\n  528│    /// brain↔broker conn without returning to read. With no ack the broker's\n *529│    /// per-conn handler never writes back while servicing that flood, so the return\n  530│    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the\n  531│    /// broker still dedups by `(session, op_id)` at its applied-set.\n  532│    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n     │...\n  583│    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n *584│        loop {\n  585│            if contains(buf, needle) {\n  586│                return Ok(self.next_seq.saturating_sub(1));\n  587│            }\n     │...\n  607│    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n *608│    /// entry (a whole `loop { read_event }` shares it, so unrelated events\n  609│    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n  610│    /// pump mode (blocking, unbounded).\n  611│    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n     │...\n  636│    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n *637│        loop {\n  638│            let env = self.read_frame_until(deadline)?;\n  639│            match env.kind.as_str() {\n  640│                KIND_OUTPUT => {\n     │...\n  918│    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]\n *919│    /// loop) reads to learn whether it became controller / viewer / was refused.\n  920│    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the\n  921│    /// generation is untouched).\n  922│    ///\n     │...\n  986│    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n *987│    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n  988│    /// recovers POST-eviction (re-subscribe from the floor). VIEWER-only →\n  989│    /// B2-SAFE (a viewer never advances `delivered_through` / is not authoritative\n  990│    /// — the CONTROLLER keeps the strict reject-gap for exactly-once resume). The\n     │...\n 1118│        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;\n*1119│        loop {\n 1120│            match self.read_event()? {\n 1121│                BrokerEvent::NetStatus(s) => return Ok(s),\n 1122│                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n     │...\n 1139│        self.send(KIND_BROKER_IMAGE, serde_json::Value::Null)?;\n*1140│        loop {\n 1141│            let env = self.read_frame_until(None)?;\n 1142│            match env.kind.as_str() {\n 1143│                KIND_BROKER_IMAGE_REPLY => {\n     │...\n 1165│        self.send(KIND_STALL_EVICTS, serde_json::Value::Null)?;\n*1166│        loop {\n 1167│            let env = self.read_frame_until(None)?;\n 1168│            match env.kind.as_str() {\n 1169│                KIND_STALL_EVICTS_REPLY => {\n     │...\n 1192│        )?;\n*1193│        loop {\n 1194│            let env = self.read_frame_until(None)?;\n 1195│            match env.kind.as_str() {\n 1196│                KIND_APPLIED => return Ok(()),\n     │...\n 1228│        )?;\n*1229│        loop {\n 1230│            match self.read_event()? {\n 1231│                BrokerEvent::PairJoined(r) => return Ok(r),\n 1232│                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n     │...\n 1253│        )?;\n*1254│        loop {\n 1255│            match self.read_event()? {\n 1256│                BrokerEvent::MetMember(m) => return Ok(MetOutcome::Met(m)),\n 1257│                BrokerEvent::PairJoined(r) => return Ok(MetOutcome::Failed(r)),\n     │...\n 1277│        )?;\n*1278│        loop {\n 1279│            match self.read_event()? {\n 1280│                BrokerEvent::PairJoined(r) => return Ok(r),\n 1281│                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n     │...\n 1307│        let deadline = self.call_deadline();\n*1308│        loop {\n 1309│            match self.read_event_until(deadline)? {\n 1310│                BrokerEvent::NetDialed(d) => return Ok(d),\n 1311│                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n     │...\n 1337│        let deadline = self.call_deadline();\n*1338│        loop {\n 1339│            match self.read_event_until(deadline)? {\n 1340│                BrokerEvent::NetDialSubmitted => return Ok(()),\n 1341│                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n### brainproc.rs\n  41│/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n *42│/// never a hot respawn loop.\n  43│const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n  44│/// A run at least this long counts as healthy and resets the backoff floor.\n  45│/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n    │...\n  55│const TRIAL_PREREADY_BUDGET: u32 = 3;\n *56│/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n *57│const TICK: Duration = Duration::from_millis(200);\n  58│\n  59│/// How long the brain child retries connecting to the broker socket before\n  60│/// giving up (the broker may still be binding — the child is freshly spawned).\n    │...\n 107│/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n*108│/// its next wait tick, kills the current brain child, and respawns it with\n 109│/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n 110│/// between the broker serve path and the supervisor — both run in the broker\n 111│/// process. This is the seam that makes a routine `apply` actually run new code,\n    │...\n 171│    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n*172│    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n 173│    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n 174│    // *counter* is rehydrated, never the wall-clock stamp.\n 175│    eprintln!(\n    │...\n 223│    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n*224│    // online/offline exclusivity — a reconcile loop that runs each offline\n 225│    // instance's `wake_command` as a supervised watcher child. Relocated from\n 226│    // the broker process to the brain child: shellwake is brain-owned logic\n 227│    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    │...\n 238│    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n*239│    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\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    │...\n 258│    let mut consumers_started = false;\n*259│    loop {\n*260│        thread::sleep(BRAIN_HEARTBEAT);\n 261│        // `net-status` is both the net-capability probe and the broker-liveness\n 262│        // probe: an `Err` is the broker gone (or a future planned\n 263│        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n    │...\n 279│        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n*280│        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n 281│        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n 282│        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283│        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n    │...\n 289│        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n*290│        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291│        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292│        // net_status probe above already owns broker-liveness). Self-applies: it drives\n 293│        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n    │...\n 334│\n*335│/// Connect to the broker, retrying briefly while it finishes binding.\n 336│fn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n 337│    let mut last: Option<io::Error> = None;\n 338│    for _ in 0..tries {\n    │...\n 342│                last = Some(e);\n*343│                thread::sleep(Duration::from_millis(10));\n 344│            }\n 345│        }\n 346│    }\n    │...\n 438│/// deterministically with a scripted ready/no-ready + a capturing notif sink,\n*439│/// while production binds the real `spt_home` + relcache + notif sender\n 440│/// ([`ProductionTrialEnv`]). Every method is **best-effort / fail-safe**: a\n 441│/// missing record reads as \"no pending trial\", an absent ready file as\n 442│/// \"not ready\" — the broker's supervisor thread never panics (a panic there\n    │...\n 486│/// ready\" and every effect is a no-op. Used where there is no readiness gate to\n*487│/// drive — the supervisor then behaves exactly as the pre-D6 respawn loop.\n 488│pub struct NoTrialEnv;\n 489│\n 490│impl TrialEnv for NoTrialEnv {\n    │...\n 507│        // No readiness gate to drive here → no old-gen control plane to drain; the\n*508│        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n 509│        // no-op effects).\n 510│        true\n 511│    }\n    │...\n 594│/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n*595│/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n 596│/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n 597│/// any future broker-side live-notif is cross-process serialization (a file\n 598│/// lock), not relaxing this rule.\n    │...\n 624│\n*625│/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n 626│/// promptly mid-backoff (saturating — never an underflow panic).\n 627│fn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n 628│    let mut slept = Duration::ZERO;\n*629│    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630│        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n### broker.rs\n  23│//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n *24│//!   live. The brain dedups by `seq`. Output produced *while no brain was\n  25│//!   attached* sits in the ring and is replayed on the next subscribe — so a\n  26│//!   brain restart loses nothing.\n  27│//!\n    │...\n 167│/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n*168│/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n*169│/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n 170│/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 171│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 172│const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n    │...\n 197│const WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n*198│/// Poll slice while waiting on a racing wake's session to appear.\n 199│const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 200│\n 201│/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n    │...\n 280│/// doyle non-negotiable = W1's \"never park forever\"): the inject worker holds the\n*281│/// floor — buffering operator input — only while a binary's response to one\n 282│/// `{type:event}` is in flight. If no `{commit}` arrives within this window the\n 283│/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no\n 284│/// respawn → idle inbound then SPOOLS, never raw-injected), and never re-opens. A wedged or\n    │...\n 337│/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n*338│/// psyche crash-loop backoff's bounded give-up.\n 339│const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n 340│\n 341│/// The effective respawn budget. Production is the const above; the int gate tunes\n    │...\n 367│/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n*368│/// returns in a tick or two, well under this.\n 369│const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n*370│/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n 371│const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372│/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373│/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n    │...\n 512│    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n*513│    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n 514│    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n 515│    /// floor). Seeded `now` at [`OutputLog::become_controller`].\n 516│    last_ok: Instant,\n    │...\n 542│/// an isolated bounded queue + writer thread, evicted on overflow). Every\n*543│/// controller send and viewer `try_send` happens while the enclosing\n 544│/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\n 545│struct OutputLog {\n 546│    session_id: u64,\n    │...\n 628│/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\n*629│/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\n 630│// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n 631│fn stamp_divergence(\n 632│    rec_driven_by: Option<&str>,\n    │...\n 649│/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\n*650│/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\n 651│/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\n 652│/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\n 653│// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n    │...\n 728│    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n*729│    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n 730│    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n*731│    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n 732│    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n 733│    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n 734│    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n    │...\n 752│    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*753│    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n 754│    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 755│    fn clamp_or_reset(&mut self) -> bool {\n 756│        let within_cap = self.ring.len() <= self.cap_chunks;\n    │...\n 804│        self.ring.push_back((seq, bytes.to_vec()));\n*805│        while self.ring.len() > self.cap_chunks {\n 806│            self.ring.pop_front();\n 807│        }\n 808│        let frame = output_envelope(self.session_id, seq, bytes);\n    │...\n 830│        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n*831│        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n 832│        // slow controller falls behind the ring) unless it has been Full for the\n 833│        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n 834│        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n    │...\n 889│    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n*890│    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n 891│    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n 892│    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n 893│    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    │...\n 897│    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64, attach_gen: u64) {\n*898│        // Drop the prior controller sink first (its writer's live loop ends when\n 899│        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n 900│        // for the old controller can't unseat this one and (b) a prior writer\n 901│        // still flushing its initial batch returns before its next frame.\n    │...\n 975│    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n*976│    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n 977│    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n### codec.rs\n  9│//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n*10│//! contract, while [`crate::transport`] owns the OS-specific connection.\n 11│// [impl->REQ-DAEMON-2]\n 12│\n 13│use std::io::{self, Read, Write};\n### config.rs\n   5│//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n  *6│//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n   7│//! period is **durable and configurable**, sourced from a small JSON file under\n   8│//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n   9│//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n *10│//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n  11│//! from config, never a constant.\n  12│// [impl->REQ-DAEMON-1]\n  13│\n    │...\n  30│/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n *31│/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n  32│/// teardown for long. Configurable per machine via `crash_grace_ms`.\n  33│pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n  34│\n    │...\n  36│/// registry/notif feeds to trusted peers and pulls context syncs from them.\n *37│/// Conservative defaults — every loop is a thin call over an idempotent\n *38│/// driver, so a missed tick costs only latency, never correctness.\n  39│pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n  40│pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n  41│pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n    │...\n  45│/// authored by a node not heard within this window are evicted on the next\n *46│/// registry pump tick. Sized to ~10 default pump cadences — generous against\n  47│/// transient drops (a peer re-advertises every cadence, so one admitted feed\n  48│/// inside the window keeps its rows), short enough that a dead node identity\n  49│/// stops poisoning bare-id resolution within minutes.\n    │...\n 132│pub struct DaemonConfig {\n*133│    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).\n 134│    pub pulse_period: Duration,\n 135│    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136│    pub protection_window_ms: u64,\n    │...\n 156│    /// node not heard within this window are evicted on the next registry\n*157│    /// pump tick — a dead node identity must not poison bare-id resolution\n 158│    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n 159│    pub registry_evict_after: Duration,\n 160│    /// Notif feed push cadence (D9-1).\n 161│    pub notif_pump_period: Duration,\n*162│    /// Context sync pull cadence — the anti-entropy loop (D9-1).\n 163│    pub sync_pull_period: Duration,\n 164│    /// Update check cadence (D9-1; the pull rides the same trusted conns).\n 165│    pub update_check_period: Duration,\n### conn.rs\n  32│//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n *33│//! completion APC makes the wedged `SleepEx` loop return with\n  34│//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n  35│//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n  36│//! the same abort (the stuck op it waited behind is the wedge).\n    │...\n  46│//! synchronous return IS the completion handshake); (4) the retired half is\n *47│//! parked in a graveyard slot — never dropped while the watchdog might still\n  48│//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n  49│//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n  50│//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n    │...\n 132│struct Dog {\n*133│    /// `Some((op_id, deadline))` while a write is in flight.\n 134│    inflight: Option<(u64, Instant)>,\n 135│    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136│    shutdown: bool,\n    │...\n 337│\n*338│    /// The watchdog body: park until a write is in flight, then sleep to its\n 339│    /// absolute deadline. If the op is still in flight at the deadline,\n 340│    /// poison + cancel, re-canceling periodically until the op reports\n 341│    /// completion (the writer clears `inflight` the instant `write_frame`\n    │...\n 344│        let mut d = recover(&self.dog);\n*345│        loop {\n 346│            // Park until armed (or shutdown).\n*347│            let (op, deadline) = loop {\n 348│                if d.shutdown {\n 349│                    return;\n 350│                }\n    │...\n 358│            };\n*359│            // Sleep toward the deadline while THIS op stays in flight.\n*360│            let fired = loop {\n 361│                if d.shutdown {\n 362│                    return;\n 363│                }\n    │...\n 391│            d = recover(&self.dog);\n*392│            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n 393│                let (g, _) = self\n 394│                    .dog_cv\n 395│                    .wait_timeout(d, Duration::from_millis(100))\n    │...\n 511│        let mut g = recover(&inner.gate);\n*512│        loop {\n 513│            if inner.poisoned.load(Ordering::Acquire) {\n 514│                return Err(poisoned_err());\n 515│            }\n    │...\n 564│        // released. Clear the arm BEFORE deciding health, so the watchdog\n*565│        // never severs while the op is formally in flight.\n 566│        {\n 567│            let mut d = recover(&inner.dog);\n 568│            d.inflight = None;\n    │...\n 763│        // A first write can land in the OS buffer before the peer-death is\n*764│        // observable; loop until the failure surfaces (bounded).\n 765│        for _ in 0..200 {\n 766│            match conn.write(&env) {\n 767│                Err(e) => {\n### crc_swap.rs\n   9│//! hash differs (so identical files — and their still-running binaries — are\n *10│//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n  11│//! so a mid-swap failure never strands the install half-new (doyle W3 gate,\n  12│//! binding partial-failure refinement).\n  13│//!\n    │...\n  85│/// 1. **Stage** — copy every change to `<target>.new` (creating parent dirs).\n *86│/// 2. **Commit** — one tight loop: for each change, DISPLACE the existing original\n  87│///    to a FRESH `<target>.old[-N]` (if present) then rename `<target>.new` →\n  88│///    `<target>`.\n *89│/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n  90│///\n  91│/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n  92│/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n    │...\n 100│///\n*101│/// On ANY failure in the commit loop, the already-committed renames are **rolled\n 102│/// back** (restore each displaced original over `<target>`) and the error —\n 103│/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104│/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n    │...\n 113│/// unit-testable deterministically: the displace fix removes every rename-OVER-an-\n*114│/// existing-file (fresh names only), so no filesystem fixture can force a mid-loop\n 115│/// commit failure any more — the seam supplies one instead. Production passes\n 116│/// `std::fs::rename`.\n 117│// [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n    │...\n 147│\n*148│    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n 149│    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150│    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151│    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n    │...\n 178│\n*179│    // Phase 3 — whole loop committed: drop the displaced originals (best-effort; a\n 180│    // still-mapped one stays but is uniquely named → the next run's Phase-0 GC\n 181│    // retries it, never a brick).\n 182│    for (_, displaced) in &committed {\n    │...\n 422│\n*423│    /// Never-strand invariant: a mid-loop rename failure rolls back the renames\n 424│    /// that already committed. Because the C1 displace fix removes every\n 425│    /// rename-OVER-an-existing-file (fresh `.old[-N]` names only), no filesystem\n*426│    /// fixture can force a mid-loop commit failure any more — so we drive\n 427│    /// [`apply_crc_swap_with`] with an INJECTED rename that fails on the SECOND\n 428│    /// target's `.new` commit, AFTER swap 1 has landed. After the Err, the first\n 429│    /// target must hold its ORIGINAL bytes (rollback restored it, NOT the staged\n    │...\n 445│        // Swap 2: an ordinary existing target file whose `.new` commit rename the\n*446│        // injected seam forces to fail — landing the error mid-loop, after swap 1.\n 447│        let second_staged = s.join(\"second.src\");\n 448│        write(&second_staged, b\"second-bytes\");\n 449│        let second_target = i.join(\"second\");\n    │...\n 466│        // into place (`rename(second.new, second)`), which fails — the deterministic,\n*467│        // cross-platform mid-loop failure the displace fix otherwise makes impossible.\n 468│        let second_new = new_path(&second_target);\n 469│        let second_target_c = second_target.clone();\n 470│        let rename = move |from: &Path, to: &Path| -> std::io::Result<()> {\n    │...\n 481│\n*482│        assert!(result.is_err(), \"the injected mid-loop commit failure propagates\");\n 483│        assert_eq!(\n 484│            fs::read(&first_target).unwrap(),\n 485│            b\"BEFORE\",\n    │...\n 549│        #[cfg(target_os = \"linux\")]\n*550│        let image = std::path::PathBuf::from(\"/bin/sleep\");\n 551│        if !image.exists() {\n 552│            return; // no reference image on this host — inconclusive, not a fail\n 553│        }\n    │...\n 566│        // Give the OS a beat to map the image.\n*567│        std::thread::sleep(std::time::Duration::from_millis(150));\n 568│\n 569│        let plan = plan_crc_swap(s, i).unwrap();\n 570│        let result = apply_crc_swap(&plan);\n    │...\n 661│            stranded_old.exists() && fs::read(&stranded_old).unwrap() == b\"LAST-GOOD-V1\",\n*662│            \"the last-good `.old` is NOT GC'd while the target was missing — survives \\\n 663│             to be swept next run (never a no-binary double-fault window)\"\n 664│        );\n 665│    }\n### daemon.rs\n   35│const START_TIMEOUT: Duration = Duration::from_secs(5);\n  *36│/// Poll cadence while waiting for the spawned daemon to come up.\n   37│const START_POLL: Duration = Duration::from_millis(25);\n   38│\n   39│/// The per-machine broker/brain daemon process.\n     │...\n   43│    /// Run the daemon: bind the broker (served on a background thread) + the\n  *44│    /// seed-control channel (the foreground loop + liveness anchor) and serve\n   45│    /// until the process is killed. The body of `spt daemon`.\n   46│    pub fn run() -> io::Result<()> {\n   47│        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n     │...\n  272│        // nothing — the slot is ephemeral control, a restart correctly drops it).\n *273│        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n  274│        // [impl->REQ-SHELL-3]\n  275│        let drive_hub = Arc::new(crate::drivehub::DriveHub::new());\n  276│        {\n     │...\n  351│\n *352│        // The seed-control channel is the foreground loop. Binding it is what\n  353│        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n  354│        // to start fails this bind and exits, leaving the winner serving. It\n  355│        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n     │...\n  357│\n *358│        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n  359│        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n  360│        // does not respawn the brain we are about to kill) THEN reap the brain\n  361│        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n     │...\n  441│    let mut backoff = NET_RETRY_FIRST;\n *442│    loop {\n *443│        thread::sleep(backoff);\n  444│        if let Some(host) = try_start_net() {\n  445│            if broker.attach_net(host) {\n  446│                eprintln!(\"NET_ATTACHED: net endpoint bound on retry — brain starts the consumers\");\n     │...\n  470│/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the\n *471│/// daemon's foreground loop returns and the process exits cleanly.\n  472│// [impl->REQ-CLI-2]\n  473│pub fn request_stop() -> io::Result<()> {\n  474│    seedmap::request_stop(&seed_socket_name())\n     │...\n  482│    let mut waited = Duration::ZERO;\n *483│    while waited < START_TIMEOUT {\n  484│        if is_running() {\n  485│            return Ok(());\n  486│        }\n *487│        thread::sleep(START_POLL);\n  488│        waited += START_POLL;\n  489│    }\n  490│    Err(io::Error::new(\n     │...\n  501│/// daemon that would fight the service for the broker socket (the kitsubito\n *502│/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n  503│// [impl->REQ-DAEMON-8]\n  504│pub fn ensure_running() -> io::Result<()> {\n  505│    if is_running() {\n     │...\n  929│/// the real per-rung launchers and log which rung won. The caller still confirms\n *930│/// readiness via the existing `brain.ready` poll — the returned pid is for\n  931│/// logging/return, not readiness (WMI/schtasks pids are the launcher's view).\n  932│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n  933│#[cfg(windows)]\n     │...\n 1185│/// is recoverable from schtasks → returns a sentinel; readiness is the caller's\n*1186│/// `brain.ready` poll. This is the FALLBACK rung (WMI is primary); its `/tr`\n 1187│/// quoting is best-effort.\n 1188│// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1189│#[cfg(windows)]\n     │...\n 1225│    }\n*1226│    // No pid from schtasks; sentinel — readiness is the brain.ready poll.\n 1227│    Ok(0)\n 1228│}\n 1229│\n     │...\n 1299│    // and caps at NET_RETRY_CAP — so a long boot-race wait settles to a steady\n*1300│    // 30s poll, never an ever-growing or zero-length spin.\n 1301│    #[test]\n 1302│    fn net_retry_backoff_doubles_then_caps() {\n 1303│        assert_eq!(net_retry_backoff(NET_RETRY_FIRST), Duration::from_secs(2));\n     │...\n 1533│            std::fs::write(&pidfile, gc.to_string()).expect(\"launcher: write gc pid\");\n*1534│            std::thread::sleep(Duration::from_secs(300));\n 1535│            std::process::exit(0);\n 1536│        }\n 1537│\n     │...\n 1605│            let mut found = None;\n*1606│            while Instant::now() < deadline {\n 1607│                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608│                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609│                        if p != 0 {\n     │...\n 1614│                }\n*1615│                std::thread::sleep(Duration::from_millis(20));\n 1616│            }\n 1617│            found.expect(\"launcher reports its grandchild pid\")\n 1618│        };\n     │...\n 1625│        for _ in 0..25 {\n*1626│            std::thread::sleep(Duration::from_millis(40));\n 1627│            if !is_process_alive(gc) {\n 1628│                still_alive = false;\n 1629│                break;\n     │...\n 1711│                }\n*1712│                thread::sleep(Duration::from_millis(5));\n 1713│            }\n 1714│            assert!(is_running(), \"the seed channel answers → daemon is up\");\n 1715│            // ensure_running short-circuits (no real process spawn).\n### deadline.rs\n  *1│//! Durable absolute-deadline loop timing — ADR-0018 Q4 / restoration D5.\n   2│//!\n  *3│//! Phase-significant periodic timing (the pulse loop) and one-shot deadlines\n   4│//! (alarms) must survive a brain swap: the supervisor respawns a fresh brain\n   5│//! (`Brain::cold_start`) and the outgoing brain is gone before the new one starts\n   6│//! (D3-3), so continuity cannot ride a brain→brain frame — it must come from the\n    │...\n  10│//!\n *11│//! - [`DeadlineAnchor`] — a **periodic** `(anchor, interval)` grid. Persisted\n  12│//!   ONCE per fresh/crash start; every fire is derived functionally\n  13│//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n  14│//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n  15│//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n *16│//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n  17│//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n  18│//!   IPC round-trip.\n  19│//!\n    │...\n  31│//! (`pump`) need none — they already stagger from everything-due-now,\n *32│//! catch-up/idempotent/restart-safe; converting them would re-add the per-loop\n  33│//! writes Q4 minimizes.\n  34│\n  35│use std::path::{Path, PathBuf};\n    │...\n  41│\n *42│/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n  43│///\n  44│/// Keyed, not a singleton (ADR-0018 D5 amendment 6): a singleton would\n  45│/// cross-clobber the moment the live-agent adapter's **per-agent** pulse drivers\n    │...\n  65│impl DeadlineAnchor {\n *66│    /// Resolve a non-zero interval (doyle minor — ONE degrade rule, unit-tested):\n  67│    /// a zero `interval_ms` degrades to the config default pulse period (a `const`\n  68│    /// that is always `> 0`), so the derive can never divide by zero. The `else\n  69│    /// 1ms` floor is belt-and-braces should that default ever be made zero.\n    │...\n  81│    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n *82│    ///   only the interval refreshes from live config. If no file exists (updating\n  83│    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n  84│    ///   — never fail an update on a missing file.\n  85│    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n    │...\n 100│        let anchor = match (reason, Self::load(&path)) {\n*101│            // Update keeps the persisted phase; the interval refreshes to config.\n 102│            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103│                anchor_ms: prev.anchor_ms,\n 104│                interval_ms,\n    │...\n 133│    /// The next fire at or after `now_ms`, derived functionally — **NO write**:\n*134│    /// `anchor + interval · ⌈max(0, now − anchor) / interval⌉`.\n 135│    ///\n 136│    /// On an exact grid instant this returns `now` (fire-now); a long jump\n 137│    /// (`now ≫ anchor`) snaps to the next grid point, collapsing missed ticks to\n    │...\n 148│\n*149│    /// The grid point **strictly after** `fired_ms` — the loop's post-fire\n 150│    /// advance (doyle minor — structural double-fire guard). Deriving the next\n 151│    /// deadline from the FIRED grid point rather than a re-sampled `now` means a\n*152│    /// fast tick re-sampling within the same millisecond cannot re-yield the same\n*153│    /// instant and double-fire. The loop body must advance via this, not via a\n*154│    /// fresh `now` alone, making the guard an invariant of the loop, not just an\n 155│    /// assertion in a test.\n 156│    pub fn next_after(&self, fired_ms: u64) -> u64 {\n 157│        self.next_fire(fired_ms.saturating_add(1))\n    │...\n 160│    /// The next deadline after firing at `fired_ms`, observed at `now_ms` — the\n*161│    /// loop's post-fire advance (doyle amendment 7). Combines BOTH invariants the\n*162│    /// loop needs in one pure expression so neither can be lost:\n 163│    ///\n 164│    /// - **strictly after the fired grid point** (the double-fire guard: an\n*165│    ///   instant tick where `now == fired` still steps exactly one grid);\n 166│    /// - **skips past `now`** so a long stall — a `pulse_tick` that outran the\n*167│    ///   interval, or the real killer, a laptop suspend/resume with the daemon\n 168│    ///   resident — collapses to **exactly one** catch-up fire instead of N rapid\n 169│    ///   back-to-back fires. (`next_after(deadline)` alone steps a single grid per\n 170│    ///   fire, so a deadline N grids behind `now` would fire N times in a tight\n*171│    ///   loop — the exact storm the loop-entry `next_fire(now)` collapse promises\n 172│    ///   to avoid; this composes with that collapse so a suspend/resume fires once\n 173│    ///   total.)\n 174│    pub fn advance_after_fire(&self, fired_ms: u64, now_ms: u64) -> u64 {\n    │...\n 226│        assert_eq!(a.next_fire(1_300), 1_300);\n*227│        // The post-fire advance must move STRICTLY forward by one interval, so a\n 228│        // re-sample at the same instant cannot double-fire (structural guard).\n 229│        assert_eq!(a.next_after(1_300), 1_400);\n 230│        assert_eq!(a.next_after(1_000), 1_100);\n### deelevate.rs\n  81│/// (still carrying `SPT_HOME`) sees no daemon and prints `DAEMON_NOT_RUNNING`\n *82│/// while the orphaned daemon serves the wrong home.\n  83│// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n  84│pub fn apply_env_overrides(block: &[u16], overrides: &[(String, String)]) -> Vec<u16> {\n  85│    const NUL: u16 = 0;\n    │...\n 213│        let mut buf = vec![0u8; buf_len as usize];\n*214│        loop {\n 215│            let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };\n 216│            let mut result: *mut libc::passwd = std::ptr::null_mut();\n 217│            let rc = unsafe {\n    │...\n 332│        let mut buf = vec![0u8; buf_len as usize];\n*333│        loop {\n 334│            let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };\n 335│            let mut result: *mut libc::passwd = std::ptr::null_mut();\n 336│            let rc = unsafe {\n    │...\n 716│        let mut n = 0usize;\n*717│        while !(*p.add(n) == 0 && *p.add(n + 1) == 0) {\n 718│            n += 1;\n 719│        }\n 720│        // n indexes the last entry's terminating NUL; +2 includes it and the\n### digest.rs\n 117│/// `from` points at the changed tail (a minimal delta); a window slide evicts the\n*118│/// oldest turn → the heads differ from index 0 → `from == 0` (a full refresh).\n 119│pub fn common_prefix_len(a: &[Turn], b: &[Turn]) -> usize {\n 120│    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()\n 121│}\n    │...\n 531│    // [unit->REQ-TERM-4] the diff base: a shared head yields its length (a tail\n*532│    // delta); a divergent front yields 0 (a full refresh).\n 533│    #[test]\n 534│    fn common_prefix_len_finds_the_unchanged_head() {\n 535│        let a = vec![turn(\"a\"), turn(\"b\")];\n### digesthub.rs\n 121│        // The changed turns begin past the equal-by-value head; a window slide\n*122│        // (front eviction) diverges at 0 → a full refresh.\n 123│        let from = common_prefix_len(&st.snapshot.turns, &new.turns);\n 124│        st.version += 1;\n 125│        let update = DigestUpdate {\n    │...\n 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    │...\n 189│    recv_hello(&mut conn, Role::Brain)?;\n*190│    loop {\n 191│        let env = match read_frame(&mut conn) {\n 192│            Ok(e) => e,\n 193│            Err(_) => return Ok(()), // peer hung up\n    │...\n 239│                // write failure) or the hub drops the sender.\n*240│                while let Ok(up) = rx.recv() {\n 241│                    if up.version <= req.from_version {\n 242│                        continue;\n 243│                    }\n    │...\n 327│    )?;\n*328│    loop {\n 329│        let env = match read_frame(&mut conn) {\n 330│            Ok(e) => e,\n 331│            Err(_) => return Ok(()), // stream ended\n    │...\n 477│        assert_eq!(up.turns[0].input.as_deref(), Some(\"c\"));\n*478│        // A front eviction (window slide) diverges at 0 → full refresh.\n 479│        hub.project_and_publish(\"doyle\", digest(vec![turn(\"b\"), turn(\"c\"), turn(\"d\")]));\n 480│        let up = rx.recv().unwrap();\n 481│        assert_eq!(up.from, 0, \"front eviction resends from 0\");\n### dispatch.rs\n   7│//! and the bytes sat in the broker's stream log unread. This module is the\n  *8│//! missing consumer: a daemon-hosted loop that watches the broker's stream\n   9│//! table for **peer-initiated** streams, classifies each by its first NDJSON\n *10│//! record, and hands it to the right serve loop on its own worker thread.\n  11│//!\n  12│//! ## Direction (the gotcha-#9 fact, now first-class)\n  13│//!\n    │...\n  16│//! requester stream would deadlock the requester against ourselves. The flag\n *17│//! is broker-recorded at registration (`open_stream` vs the accept loop),\n  18│//! never inferred.\n  19│//!\n  20│//! ## Classification (first-record demux)\n    │...\n  49│//! the peek brain subscribes from 0 to read the first line and is dropped;\n *50│//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n  51│//! stream's event feed alone (no double-subscription interleave). Origin is\n  52│//! the table's `remote_id_hex` — handshake-proven, never payload\n  53│//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n    │...\n  84│\n *85│/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n  86│/// workers block on their stream's event feed).\n  87│pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n  88│\n    │...\n 132│        // The status-only convergence query rides the update family (M8\n*133│        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\n 134│        Some(\"status_query\") => StreamFamily::Update,\n 135│        Some(\"row\") => StreamFamily::Notif,\n 136│        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n    │...\n 160│pub enum DispatchOutcome {\n*161│    /// The family's serve loop ran to its own outcome (rendered for the log).\n 162│    Served(String),\n 163│    /// The first line never arrived / didn't parse — dropped.\n 164│    Unknown,\n    │...\n 173│/// distinguish in-flight vs terminal vs retryable, and a transient failure\n*174│/// requeues bounded (backoff-shaped, never a hot loop).\n 175│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 176│enum ClaimState {\n 177│    /// A worker owns the stream right now (`attempts` = prior failures).\n    │...\n 297│/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n*298│/// off). The unit-tested seam for the no-hot-loop ruling.\n 299│// [impl->REQ-DISPATCH-CLAIM-RETRY]\n 300│fn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n 301│    match state {\n    │...\n 353│/// propagate.rs `fetch_update` both `net_stream_send(.., finish=true)` on the\n*354│/// opener), so every pull that arrives while the brain is down reads\n*355│/// `finished=true` while genuinely PENDING — filtering it breaks context sync\n*356│/// across exactly the refresh window this milestone fixes.\n 357│///\n 358│/// The steal class is ATTACH-specific: `finished` on an attach stream is the\n 359│/// operator's clean detach — its exchange is definitionally over, and a\n    │...\n 389│/// are proven with real wedged conns (T1/T3/T4); this valve isolates the\n*390│/// loop's RESPONSE to the classification. Same latch discipline as the\n 391│/// transient valve.\n 392│fn take_injected_seatblocked() -> bool {\n 393│    static ARMED: OnceLock<AtomicBool> = OnceLock::new();\n    │...\n 433│    stop: &AtomicBool,\n*434│    poll: Duration,\n 435│) -> io::Result<()> {\n 436│    let mut brain = connect(broker_name)?;\n 437│    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    │...\n 441│    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n*442│    // loop emits carries it, so per-generation attach/poison censuses line up.\n 443│    let generation = now_ms();\n 444│    // Global circuit breaker (Amendment fix 5) + its current window, shared\n 445│    // with workers so a seat-blocked retry paces on it.\n 446│    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n*447│    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n*448│    // with a large backlog) claims at most this many rows per poll — the\n*449│    // rest wait for the next tick, batching the cold enumeration instead of\n 450│    // spawning one thread per row. No claim lock is held across any I/O:\n 451│    // enumeration runs lock-free, the claim map is touched in short scopes.\n 452│    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    │...\n 454│\n*455│    while !stop.load(Ordering::Relaxed) {\n 456│        if breaker.lock().unwrap().open(Instant::now()) {\n 457│            // Circuit open: a poisoned replay just happened — pause ALL\n 458│            // claiming for the window instead of hammering the wedged conn\n 459│            // with fresh replay subscribers (Amendment fix 5).\n*460│            thread::sleep(poll);\n 461│            continue;\n 462│        }\n 463│        let reply = brain.net_streams()?;\n### docshost.rs\n  61│    let mut i = 0;\n *62│    while i < bytes.len() {\n  63│        if bytes[i] == b'%' {\n  64│            let hex = bytes.get(i + 1..i + 3)?;\n  65│            let s = std::str::from_utf8(hex).ok()?;\n    │...\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### drivehub.rs\n   5│//! channel\" — the opaque control surface). An owner writes one drive frame for a\n  *6│//! shell; the shell binary drains it with an ephemeral `api drive-poll --link`.\n   7│//! Unlike the durable command channel (spooled on the shell perch), a drive\n   8│//! frame is held in a **single in-memory slot per shell** and overwritten on\n  *9│//! every write — a second write before the poll clobbers the first.\n  10│//!\n  11│//! **Where the slot lives (broker, not relay; broker, not brain).** The slot is\n  12│//! held by the always-up BROKER process (alongside [`crate::digesthub::DigestHub`]),\n    │...\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    │...\n 195│    recv_hello(&mut conn, Role::Brain)?;\n*196│    loop {\n 197│        let env = match read_frame(&mut conn) {\n 198│            Ok(e) => e,\n 199│            Err(_) => return Ok(()), // peer hung up\n    │...\n 310│\n*311│    // [unit->REQ-SHELL-3] latest-wins: a second write before the poll clobbers\n 312│    // the first; the take serves the latest frame.\n 313│    #[test]\n 314│    fn write_is_latest_wins() {\n    │...\n 324│\n*325│    // [unit->REQ-SHELL-3] take-and-clear is exactly-once: a second poll after a\n 326│    // single write returns empty (the slot was cleared on the first take).\n 327│    #[test]\n 328│    fn take_is_exactly_once() {\n    │...\n 343│    // [unit->REQ-SHELL-3] no-replay-across-relink: a frame written under one link\n*344│    // token is NEVER served to a poll presenting a different (relinked) token —\n*345│    // and the dead frame is evicted by that very poll. This is the crash-proof\n 346│    // guarantee that does not depend on the explicit clear-on-close running.\n 347│    #[test]\n 348│    fn dead_generation_frame_is_never_served() {\n    │...\n 359│            !hub.has_slot(\"doyle\", \"mock-shell-0\"),\n*360│            \"the dead frame is cleared by the mismatching poll\"\n 361│        );\n 362│    }\n 363│\n\n### bin/\n#### xlate_choreo_fixture.rs\n  28│//!     settle-gate + chunked write must deliver byte-complete (head AND tail intact)\n *29│//!     across a LOOP of injects.\n  30│//!\n  31│//! Privacy assertion support (doyle gate #1): every stdin line we receive is\n  32│//! appended verbatim to the file named by env `XLATE_FIXTURE_STDIN_LOG` (when\n    │...\n 125│        // adapter restores flow at the end of its sequence just the same; this keeps\n*126│        // the `ctrl+s` chord (and its real 0x13 mapping + floor exercise) intact while\n 127│        // making the payload's arrival observable, per the test-specialist's latitude.\n 128│        emit(&mut out, r#\"{\"key\":\"ctrl+q\"}\"#);\n 129│        emit(&mut out, r#\"{\"commit\":true}\"#);","truncation":{"content":"# crates/spt-daemon/\n## Cargo.toml\n 5|publish = false\n*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) — the `spt` binary is a thin CLI over it. B0 lands the crate + the version...\n 7|\n 8|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n 9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n\n## src/\n### applyhost.rs\n 34|//!    `apply_brain_only` handoff, the regression that re-attached a CLI-side\n*35|//!    subscriber while the live brain kept running the OLD code.\n 36|//!\n 37|//! New code runs **immediately** — the supervised respawn lands on the new\n 38|//! executable, no manual bounce, no logon. Readiness-gated promotion +\n...\n 134|    // failed readiness on this node and auto-rolled-back is quarantined — never\n*135|    // re-applied, or the self-heal becomes a self-harm loop (apply → fail → roll\n 136|    // back → re-apply → …). The `RolledBack` record is the quarantine marker; it\n 137|    // clears naturally when a newer version stages (N+1 ≠ the quarantined N). An\n 138|    // operator override (`--force`) to re-apply a genuinely-good version whose\n...\n 270|    // reached `ready`. Writing `applied` here is the optimism the design indicts\n*271|    // (the `applied.json={version:6}` observed on `enlyzeam` while the box still\n 272|    // ran the old code). Instead record the FIRST phase — `AppliedPending` —\n 273|    // carrying the last-known-good `rollback_binary` (the aside we just created)\n 274|    // so the broker's supervisor, a different process from this CLI, can promote\n...\n 331|    // level. This replaces the old in-process `apply_brain_only` handoff (the\n*332|    // regression: it re-attached a CLI-side subscriber while the live brain kept\n 333|    // running the OLD code). `classify` already gated this as brain-only, so the\n 334|    // broker never moves; only the brain restarts.\n 335|    //\n...\n 352|/// Cycle the brain of a RUNNING daemon with NO binary swap and NO staging\n*353|/// preconditions (REQ-DAEMON-REFRESH, `spt daemon refresh`): connect to the\n 354|/// broker and raise the same supervisor brain-restart signal the post-swap\n 355|/// apply rides ([`Brain::request_brain_restart`], ADR-0018 D3-3) — brain stop →\n 356|/// respawn → readiness trial → promote, broker + every hosted PTY untouched by\n...\n 362|/// (accepted, not acted on).\n*363|// [impl->REQ-DAEMON-REFRESH]\n 364|pub fn refresh_brain(broker_name: &str) -> Result<bool, String> {\n 365|    let mut brain = connect_retry(broker_name).map_err(|e| format!(\"broker connect: {e}\"))?;\n 366|    brain\n...\n 461|        }\n*462|        std::thread::sleep(Duration::from_millis(10));\n 463|    }\n 464|    Err(last)\n 465|}\n...\n 560|            }\n*561|            std::thread::sleep(Duration::from_millis(10));\n 562|        }\n 563|        panic!(\"brain cold-start could not connect\");\n 564|    }\n...\n 822|\n*823|    // [unit->REQ-DAEMON-REFRESH] `refresh_brain` routes the SAME supervisor\n 824|    // brain-cycle signal as the post-swap apply — against a bare broker with NO\n 825|    // staged release, NO release cache, NO binary swap. The verb has no staging\n 826|    // preconditions: the signal raises and `honored` reports the supervisor\n...\n 832|\n*833|        // No supervisor wired: the refresh round-trips and reports not-honored.\n 834|        assert!(\n*835|            !refresh_brain(&name).expect(\"refresh round-trips on a bare broker\"),\n 836|            \"no supervisor wired must report honored=false\"\n 837|        );\n 838|\n*839|        // Supervisor wired: refresh raises the same planned-restart signal the\n 840|        // apply path rides — with nothing staged anywhere.\n 841|        let signal = Arc::new(crate::brainproc::BrainRestart::new());\n 842|        assert!(broker.set_brain_restart(Arc::clone(&signal)), \"first wire wins\");\n 843|        assert!(\n*844|            refresh_brain(&name).expect(\"refresh round-trips\"),\n 845|            \"a wired supervisor must report honored=true\"\n 846|        );\n 847|        assert!(\n 848|            signal.take(),\n*849|            \"refresh must raise the planned-restart signal the supervisor consumes\"\n 850|        );\n 851|    }\n 852|\n...\n 1055|    // [unit->REQ-UPD-6] Broker-touching debug/update-set payloads are accepted\n*1056|    // as staged candidates, but apply refuses them typed while the broker-held\n 1057|    // runtime is live. The first broker-touching flow is refusal-only; it does\n 1058|    // not auto-suspend or cycle hosted work.\n 1059|    #[test]\n...\n 1139|    // is refused typed at the apply gate — never re-applied (no self-heal →\n*1140|    // self-harm loop), seat untouched. The guard is version-keyed, so a NEWER\n 1141|    // staged version is not quarantined (the marker clears naturally at N+1).\n 1142|    #[test]\n 1143|    fn apply_staged_refuses_a_quarantined_version() {\n### attach.rs\n 18|//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n*19|//! and *discards* everything interleaved — inside a full-duplex pump loop that\n 20|//! eats the operator's `Input` records (both sides starve; found the hard way\n 21|//! in `tests/attach.rs`). Idempotency holds without it because each direction\n 22|//! dedups at its **effect**: the PTY write is journaled by the operator-minted\n...\n 57|/// viewport ending detaches it (active → dormant; the transition echo is\n*58|/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like\n 59|/// [`mark_driven_by`]: a session served without a perch has no resting\n 60|/// record to move.\n 61|// [impl->REQ-INST-3]\n...\n 65|\n*66|/// Why a serve loop returned.\n 67|#[derive(Debug, Clone, PartialEq, Eq)]\n 68|pub enum AttachServeOutcome {\n 69|    /// The access gate refused the requesting origin, OR a `Control` attach hit\n...\n 85|\n*86|/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n 87|/// arrives — gates wake (controller-only) and PTY input.\n 88|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 89|enum ServeRole {\n...\n 99|/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With\n*100|/// it, the operator sees an intermittent LIVE burst each interval (forward\n*101|/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\n 102|const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n 103|\n 104|/// The CONTROLLER fell behind the live ring further than the ring holds — the\n...\n 106|/// exactly-once re-fetch is IMPOSSIBLE. Distinguishes the irrecoverable edge from\n*107|/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n 108|/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n 109|/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n 110|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n...\n 172|    let mut swallow_resume_ack = false;\n*173|    loop {\n 174|        match brain.read_event() {\n 175|            // Our own re-subscribe's Subscribed reply: swallow it (role is already\n 176|            // resolved) so it cannot mask a repeated gap as forward progress.\n...\n 283|/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n*284|/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n 285|/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n*286|/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n*287|/// round-trip inside the event loop would swallow interleaved attach records\n*288|/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n 289|/// the wire `session_id`.\n 290|///\n 291|/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\n...\n 321|    // forwards rode `brain` too, that stuck handler could not read them →\n*322|    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n 323|    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n 324|    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n 325|    // two directions on independent single-threaded handlers cannot block each\n...\n 329|    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n*330|    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n 331|    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n 332|    // the controller onto that async path would remove the inline-replay block at\n 333|    // the source, but it touches the authoritative cursor-advancing path.)\n...\n 360|    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\n*361|    // BEFORE the event loop, through the exact wire-Request path — a rolled\n 362|    // ring can no longer replay it. Non-Request seeds are ignored (the opener\n 363|    // of an attach stream is always its Request).\n 364|    if let Some(AttachRecord::Request {\n...\n 391|\n*392|    loop {\n 393|        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n 394|        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n 395|        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n...\n 443|                            // The dispatcher already resolved endpoint_id→session\n*444|                            // (passed as remote_session) BEFORE this loop; ignore\n*445|                            // the wire copy here (a mid-loop re-resolve would swallow\n 446|                            // interleaved attach records — see remote_session docs).\n 447|                            endpoint_id: _,\n 448|                            gen: req_gen,\n...\n 513|\n*514|            // ── Broker → loop: the resolved controller/viewer role. ─────────\n 515|            BrokerEvent::Subscribed { outcome, .. } => {\n 516|                use crate::msg::SubscribeOutcome as O;\n 517|                match outcome {\n...\n 553|                // No dormant edge: the endpoint stays driven by the taker (whose\n*554|                // own serve loop already fired the wake).\n 555|                return Ok(AttachServeOutcome::Displaced);\n 556|            }\n 557|\n### autostart.rs\n 22|/// replay thread starts alongside the broker's serve thread, so the very first\n*23|/// connect can race the accept loop up. Bounded and short: a genuine launch\n 24|/// failure (dead adapter binary, bad template) fails again instantly and we\n 25|/// move on after the last attempt.\n 26|const LAUNCH_ATTEMPTS: u32 = 3;\n...\n 104|                    if attempt < LAUNCH_ATTEMPTS {\n*105|                        std::thread::sleep(LAUNCH_RETRY_DELAY);\n 106|                    }\n 107|                }\n 108|            }\n...\n 190|            // Must return promptly — the skip happens before the bogus socket is\n*191|            // ever dialed, so no retry loop, no launch, no hang.\n 192|            replay_bounded(Duration::from_secs(10));\n 193|        });\n 194|    }\n### brain.rs\n 196|/// single-threaded; a brain-IPC read that blocks forever on a black-holed peer\n*197|/// wedges the whole loop (the 2.2h hfenduleam stall). The deadline must be\n 198|/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n 199|/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n 200|/// → deprecated `PIPE_NOWAIT`, which corrupts mid-stream — proven by the mesh\n...\n 237|                eprintln!(\"PUMP_IPC_READER: spawned\");\n*238|                loop {\n 239|                    let framed = read_frame(&mut recv);\n 240|                    let was_err = framed.is_err();\n 241|                    // Receiver gone (Brain dropped) → stop reading.\n...\n 298|    /// brain-IPC read waits forever, which is correct everywhere EXCEPT the\n*299|    /// single-threaded pump, where a black-holed peer would wedge the whole loop\n 300|    /// (KNOWN-HAZARDS — a blocked brain-IPC read must never wedge the pump). The\n 301|    /// two facts are coupled at construction: `Some` ⇔ [`BrainConn::Split`].\n 302|    io_timeout: Option<Duration>,\n...\n 455|    ///\n*456|    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\n 457|    /// calls [`Brain::read_event`] on every interleaved frame, so on a *resume-mode*\n 458|    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\n 459|    /// **different** session read here is cursor-processed (its cursor snaps; the\n...\n 470|        )?;\n*471|        loop {\n 472|            match self.read_event()? {\n 473|                BrokerEvent::Spawned { session_id, pid } => {\n 474|                    self.session_id = Some(session_id);\n...\n 528|    /// brain↔broker conn without returning to read. With no ack the broker's\n*529|    /// per-conn handler never writes back while servicing that flood, so the return\n 530|    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the\n 531|    /// broker still dedups by `(session, op_id)` at its applied-set.\n 532|    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n...\n 583|    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n*584|        loop {\n 585|            if contains(buf, needle) {\n 586|                return Ok(self.next_seq.saturating_sub(1));\n 587|            }\n...\n 607|    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n*608|    /// entry (a whole `loop { read_event }` shares it, so unrelated events\n 609|    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n 610|    /// pump mode (blocking, unbounded).\n 611|    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n...\n 636|    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n*637|        loop {\n 638|            let env = self.read_frame_until(deadline)?;\n 639|            match env.kind.as_str() {\n 640|                KIND_OUTPUT => {\n...\n 918|    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]\n*919|    /// loop) reads to learn whether it became controller / viewer / was refused.\n 920|    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the\n 921|    /// generation is untouched).\n 922|    ///\n...\n 986|    /// re-subscribe. The two viewer-survival mechanisms COMPOSE: this tolerates\n*987|    /// pre-eviction ring-roll gaps WHILE subscribed; [`Brain::attach_skip_to_live`]\n 988|    /// recovers POST-eviction (re-subscribe from the floor). VIEWER-only →\n 989|    /// B2-SAFE (a viewer never advances `delivered_through` / is not authoritative\n 990|    /// — the CONTROLLER keeps the strict reject-gap for exactly-once resume). The\n...\n 1118|        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;\n*1119|        loop {\n 1120|            match self.read_event()? {\n 1121|                BrokerEvent::NetStatus(s) => return Ok(s),\n 1122|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1139|        self.send(KIND_BROKER_IMAGE, serde_json::Value::Null)?;\n*1140|        loop {\n 1141|            let env = self.read_frame_until(None)?;\n 1142|            match env.kind.as_str() {\n 1143|                KIND_BROKER_IMAGE_REPLY => {\n...\n 1165|        self.send(KIND_STALL_EVICTS, serde_json::Value::Null)?;\n*1166|        loop {\n 1167|            let env = self.read_frame_until(None)?;\n 1168|            match env.kind.as_str() {\n 1169|                KIND_STALL_EVICTS_REPLY => {\n...\n 1192|        )?;\n*1193|        loop {\n 1194|            let env = self.read_frame_until(None)?;\n 1195|            match env.kind.as_str() {\n 1196|                KIND_APPLIED => return Ok(()),\n...\n 1228|        )?;\n*1229|        loop {\n 1230|            match self.read_event()? {\n 1231|                BrokerEvent::PairJoined(r) => return Ok(r),\n 1232|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1253|        )?;\n*1254|        loop {\n 1255|            match self.read_event()? {\n 1256|                BrokerEvent::MetMember(m) => return Ok(MetOutcome::Met(m)),\n 1257|                BrokerEvent::PairJoined(r) => return Ok(MetOutcome::Failed(r)),\n...\n 1277|        )?;\n*1278|        loop {\n 1279|            match self.read_event()? {\n 1280|                BrokerEvent::PairJoined(r) => return Ok(r),\n 1281|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1307|        let deadline = self.call_deadline();\n*1308|        loop {\n 1309|            match self.read_event_until(deadline)? {\n 1310|                BrokerEvent::NetDialed(d) => return Ok(d),\n 1311|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n...\n 1337|        let deadline = self.call_deadline();\n*1338|        loop {\n 1339|            match self.read_event_until(deadline)? {\n 1340|                BrokerEvent::NetDialSubmitted => return Ok(()),\n 1341|                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n### brainproc.rs\n 41|/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n*42|/// never a hot respawn loop.\n 43|const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n 44|/// A run at least this long counts as healthy and resets the backoff floor.\n 45|/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n...\n 55|const TRIAL_PREREADY_BUDGET: u32 = 3;\n*56|/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n*57|const TICK: Duration = Duration::from_millis(200);\n 58|\n 59|/// How long the brain child retries connecting to the broker socket before\n 60|/// giving up (the broker may still be binding — the child is freshly spawned).\n...\n 107|/// broker's `KIND_BRAIN_RESTART` handler raises it; the supervisor consumes it on\n*108|/// its next wait tick, kills the current brain child, and respawns it with\n 109|/// `start-reason=update` onto the (freshly-swapped) binary. Shared (`Arc`)\n 110|/// between the broker serve path and the supervisor — both run in the broker\n 111|/// process. This is the seam that makes a routine `apply` actually run new code,\n...\n 171|    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n*172|    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n 173|    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n 174|    // *counter* is rehydrated, never the wall-clock stamp.\n 175|    eprintln!(\n...\n 223|    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n*224|    // online/offline exclusivity — a reconcile loop that runs each offline\n 225|    // instance's `wake_command` as a supervised watcher child. Relocated from\n 226|    // the broker process to the brain child: shellwake is brain-owned logic\n 227|    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n...\n 238|    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n*239|    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\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...\n 258|    let mut consumers_started = false;\n*259|    loop {\n*260|        thread::sleep(BRAIN_HEARTBEAT);\n 261|        // `net-status` is both the net-capability probe and the broker-liveness\n 262|        // probe: an `Err` is the broker gone (or a future planned\n 263|        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n...\n 279|        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n*280|        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n 281|        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n 282|        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n 283|        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n...\n 289|        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n*290|        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n 291|        // best-effort — a transient sessions error must never fell the heartbeat (the\n 292|        // net_status probe above already owns broker-liveness). Self-applies: it drives\n 293|        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n...\n 334|\n*335|/// Connect to the broker, retrying briefly while it finishes binding.\n 336|fn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n 337|    let mut last: Option<io::Error> = None;\n 338|    for _ in 0..tries {\n...\n 342|                last = Some(e);\n*343|                thread::sleep(Duration::from_millis(10));\n 344|            }\n 345|        }\n 346|    }\n...\n 438|/// deterministically with a scripted ready/no-ready + a capturing notif sink,\n*439|/// while production binds the real `spt_home` + relcache + notif sender\n 440|/// ([`ProductionTrialEnv`]). Every method is **best-effort / fail-safe**: a\n 441|/// missing record reads as \"no pending trial\", an absent ready file as\n 442|/// \"not ready\" — the broker's supervisor thread never panics (a panic there\n...\n 486|/// ready\" and every effect is a no-op. Used where there is no readiness gate to\n*487|/// drive — the supervisor then behaves exactly as the pre-D6 respawn loop.\n 488|pub struct NoTrialEnv;\n 489|\n 490|impl TrialEnv for NoTrialEnv {\n...\n 507|        // No readiness gate to drive here → no old-gen control plane to drain; the\n*508|        // no-op env promotes exactly as the pre-D6 respawn loop (mirrors its other\n 509|        // no-op effects).\n 510|        true\n 511|    }\n...\n 594|/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n*595|/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n 596|/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n 597|/// any future broker-side live-notif is cross-process serialization (a file\n 598|/// lock), not relaxing this rule.\n...\n 624|\n*625|/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n 626|/// promptly mid-backoff (saturating — never an underflow panic).\n 627|fn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n 628|    let mut slept = Duration::ZERO;\n*629|    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630|        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n### broker.rs\n 23|//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\n*24|//!   live. The brain dedups by `seq`. Output produced *while no brain was\n 25|//!   attached* sits in the ring and is replayed on the next subscribe — so a\n 26|//!   brain restart loses nothing.\n 27|//!\n...\n 167|/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n*168|/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n*169|/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n 170|/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 171|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 172|const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n...\n 197|const WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n*198|/// Poll slice while waiting on a racing wake's session to appear.\n 199|const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 200|\n 201|/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n...\n 280|/// doyle non-negotiable = W1's \"never park forever\"): the inject worker holds the\n*281|/// floor — buffering operator input — only while a binary's response to one\n 282|/// `{type:event}` is in flight. If no `{commit}` arrives within this window the\n 283|/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no\n 284|/// respawn → idle inbound then SPOOLS, never raw-injected), and never re-opens. A wedged or\n...\n 337|/// budget shape: consecutive-failures bounded, healthy-run resets). Mirrors the\n*338|/// psyche crash-loop backoff's bounded give-up.\n 339|const TRANSLATE_RESPAWN_BUDGET: u32 = 3;\n 340|\n 341|/// The effective respawn budget. Production is the const above; the int gate tunes\n...\n 367|/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n*368|/// returns in a tick or two, well under this.\n 369|const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n*370|/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n 371|const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372|/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373|/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n...\n 512|    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n*513|    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n 514|    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n 515|    /// floor). Seeded `now` at [`OutputLog::become_controller`].\n 516|    last_ok: Instant,\n...\n 542|/// an isolated bounded queue + writer thread, evicted on overflow). Every\n*543|/// controller send and viewer `try_send` happens while the enclosing\n 544|/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.\n 545|struct OutputLog {\n 546|    session_id: u64,\n...\n 628|/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\n*629|/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\n 630|// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n 631|fn stamp_divergence(\n 632|    rec_driven_by: Option<&str>,\n...\n 649|/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\n*650|/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\n 651|/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\n 652|/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\n 653|// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n...\n 728|    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n*729|    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n 730|    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n*731|    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n 732|    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n 733|    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n 734|    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n...\n 752|    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*753|    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n 754|    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 755|    fn clamp_or_reset(&mut self) -> bool {\n 756|        let within_cap = self.ring.len() <= self.cap_chunks;\n...\n 804|        self.ring.push_back((seq, bytes.to_vec()));\n*805|        while self.ring.len() > self.cap_chunks {\n 806|            self.ring.pop_front();\n 807|        }\n 808|        let frame = output_envelope(self.session_id, seq, bytes);\n...\n 830|        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n*831|        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n 832|        // slow controller falls behind the ring) unless it has been Full for the\n 833|        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n 834|        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n...\n 889|    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n*890|    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n 891|    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n 892|    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n 893|    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n...\n 897|    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64, attach_gen: u64) {\n*898|        // Drop the prior controller sink first (its writer's live loop ends when\n 899|        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n 900|        // for the old controller can't unseat this one and (b) a prior writer\n 901|        // still flushing its initial batch returns before its next frame.\n...\n 975|    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n*976|    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n 977|    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n### codec.rs\n 9|//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n*10|//! contract, while [`crate::transport`] owns the OS-specific connection.\n 11|// [impl->REQ-DAEMON-2]\n 12|\n 13|use std::io::{self, Read, Write};\n### config.rs\n 5|//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n*6|//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n 7|//! period is **durable and configurable**, sourced from a small JSON file under\n 8|//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n 9|//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n*10|//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n 11|//! from config, never a constant.\n 12|// [impl->REQ-DAEMON-1]\n 13|\n...\n 30|/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n*31|/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n 32|/// teardown for long. Configurable per machine via `crash_grace_ms`.\n 33|pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n 34|\n...\n 36|/// registry/notif feeds to trusted peers and pulls context syncs from them.\n*37|/// Conservative defaults — every loop is a thin call over an idempotent\n*38|/// driver, so a missed tick costs only latency, never correctness.\n 39|pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40|pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41|pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n...\n 45|/// authored by a node not heard within this window are evicted on the next\n*46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n 47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n...\n 132|pub struct DaemonConfig {\n*133|    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).\n 134|    pub pulse_period: Duration,\n 135|    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136|    pub protection_window_ms: u64,\n...\n 156|    /// node not heard within this window are evicted on the next registry\n*157|    /// pump tick — a dead node identity must not poison bare-id resolution\n 158|    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n 159|    pub registry_evict_after: Duration,\n 160|    /// Notif feed push cadence (D9-1).\n 161|    pub notif_pump_period: Duration,\n*162|    /// Context sync pull cadence — the anti-entropy loop (D9-1).\n 163|    pub sync_pull_period: Duration,\n 164|    /// Update check cadence (D9-1; the pull rides the same trusted conns).\n 165|    pub update_check_period: Duration,\n### conn.rs\n 32|//! connection and aborts the in-flight I/O (`CancelIoEx` on Windows — the\n*33|//! completion APC makes the wedged `SleepEx` loop return with\n 34|//! `ERROR_OPERATION_ABORTED`; `shutdown(SHUT_RDWR)` on Unix — the blocked\n 35|//! write returns `EPIPE`). A gate-waiter that exhausts its own deadline fires\n 36|//! the same abort (the stuck op it waited behind is the wedge).\n...\n 46|//! synchronous return IS the completion handshake); (4) the retired half is\n*47|//! parked in a graveyard slot — never dropped while the watchdog might still\n 48|//! touch the raw handle (drop = `CloseHandle`, and a recycled handle value\n 49|//! must never see a late `CancelIoEx`), and never re-gated; (5) on Windows the\n 50|//! pipe is then severed (`DisconnectNamedPipe` — the server end forces the\n...\n 132|struct Dog {\n*133|    /// `Some((op_id, deadline))` while a write is in flight.\n 134|    inflight: Option<(u64, Instant)>,\n 135|    /// Raised by [`BrokerConn::drop`]; the watchdog exits.\n 136|    shutdown: bool,\n...\n 337|\n*338|    /// The watchdog body: park until a write is in flight, then sleep to its\n 339|    /// absolute deadline. If the op is still in flight at the deadline,\n 340|    /// poison + cancel, re-canceling periodically until the op reports\n 341|    /// completion (the writer clears `inflight` the instant `write_frame`\n...\n 344|        let mut d = recover(&self.dog);\n*345|        loop {\n 346|            // Park until armed (or shutdown).\n*347|            let (op, deadline) = loop {\n 348|                if d.shutdown {\n 349|                    return;\n 350|                }\n...\n 358|            };\n*359|            // Sleep toward the deadline while THIS op stays in flight.\n*360|            let fired = loop {\n 361|                if d.shutdown {\n 362|                    return;\n 363|                }\n...\n 391|            d = recover(&self.dog);\n*392|            while matches!(d.inflight, Some((cur, _)) if cur == op) && !d.shutdown {\n 393|                let (g, _) = self\n 394|                    .dog_cv\n 395|                    .wait_timeout(d, Duration::from_millis(100))\n...\n 511|        let mut g = recover(&inner.gate);\n*512|        loop {\n 513|            if inner.poisoned.load(Ordering::Acquire) {\n 514|                return Err(poisoned_err());\n 515|            }\n...\n 564|        // released. Clear the arm BEFORE deciding health, so the watchdog\n*565|        // never severs while the op is formally in flight.\n 566|        {\n 567|            let mut d = recover(&inner.dog);\n 568|            d.inflight = None;\n...\n 763|        // A first write can land in the OS buffer before the peer-death is\n*764|        // observable; loop until the failure surfaces (bounded).\n 765|        for _ in 0..200 {\n 766|            match conn.write(&env) {\n 767|                Err(e) => {\n### crc_swap.rs\n 9|//! hash differs (so identical files — and their still-running binaries — are\n*10|//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n 11|//! so a mid-swap failure never strands the install half-new (doyle W3 gate,\n 12|//! binding partial-failure refinement).\n 13|//!\n...\n 85|/// 1. **Stage** — copy every change to `<target>.new` (creating parent dirs).\n*86|/// 2. **Commit** — one tight loop: for each change, DISPLACE the existing original\n 87|///    to a FRESH `<target>.old[-N]` (if present) then rename `<target>.new` →\n 88|///    `<target>`.\n*89|/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n 90|///\n 91|/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n 92|/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n...\n 100|///\n*101|/// On ANY failure in the commit loop, the already-committed renames are **rolled\n 102|/// back** (restore each displaced original over `<target>`) and the error —\n 103|/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104|/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n...\n 113|/// unit-testable deterministically: the displace fix removes every rename-OVER-an-\n*114|/// existing-file (fresh names only), so no filesystem fixture can force a mid-loop\n 115|/// commit failure any more — the seam supplies one instead. Production passes\n 116|/// `std::fs::rename`.\n 117|// [impl->REQ-CRC-SWAP-OLD-DISPLACE]\n...\n 147|\n*148|    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n 149|    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150|    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151|    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n...\n 178|\n*179|    // Phase 3 — whole loop committed: drop the displaced originals (best-effort; a\n 180|    // still-mapped one stays but is uniquely named → the next run's Phase-0 GC\n 181|    // retries it, never a brick).\n 182|    for (_, displaced) in &committed {\n...\n 422|\n*423|    /// Never-strand invariant: a mid-loop rename failure rolls back the renames\n 424|    /// that already committed. Because the C1 displace fix removes every\n 425|    /// rename-OVER-an-existing-file (fresh `.old[-N]` names only), no filesystem\n*426|    /// fixture can force a mid-loop commit failure any more — so we drive\n 427|    /// [`apply_crc_swap_with`] with an INJECTED rename that fails on the SECOND\n 428|    /// target's `.new` commit, AFTER swap 1 has landed. After the Err, the first\n 429|    /// target must hold its ORIGINAL bytes (rollback restored it, NOT the staged\n...\n 445|        // Swap 2: an ordinary existing target file whose `.new` commit rename the\n*446|        // injected seam forces to fail — landing the error mid-loop, after swap 1.\n 447|        let second_staged = s.join(\"second.src\");\n 448|        write(&second_staged, b\"second-bytes\");\n 449|        let second_target = i.join(\"second\");\n...\n 466|        // into place (`rename(second.new, second)`), which fails — the deterministic,\n*467|        // cross-platform mid-loop failure the displace fix otherwise makes impossible.\n 468|        let second_new = new_path(&second_target);\n 469|        let second_target_c = second_target.clone();\n 470|        let rename = move |from: &Path, to: &Path| -> std::io::Result<()> {\n...\n 481|\n*482|        assert!(result.is_err(), \"the injected mid-loop commit failure propagates\");\n 483|        assert_eq!(\n 484|            fs::read(&first_target).unwrap(),\n 485|            b\"BEFORE\",\n...\n 549|        #[cfg(target_os = \"linux\")]\n*550|        let image = std::path::PathBuf::from(\"/bin/sleep\");\n 551|        if !image.exists() {\n 552|            return; // no reference image on this host — inconclusive, not a fail\n 553|        }\n...\n 566|        // Give the OS a beat to map the image.\n*567|        std::thread::sleep(std::time::Duration::from_millis(150));\n 568|\n 569|        let plan = plan_crc_swap(s, i).unwrap();\n 570|        let result = apply_crc_swap(&plan);\n...\n 661|            stranded_old.exists() && fs::read(&stranded_old).unwrap() == b\"LAST-GOOD-V1\",\n*662|            \"the last-good `.old` is NOT GC'd while the target was missing — survives \\\n 663|             to be swept next run (never a no-binary double-fault window)\"\n 664|        );\n 665|    }\n### daemon.rs\n 35|const START_TIMEOUT: Duration = Duration::from_secs(5);\n*36|/// Poll cadence while waiting for the spawned daemon to come up.\n 37|const START_POLL: Duration = Duration::from_millis(25);\n 38|\n 39|/// The per-machine broker/brain daemon process.\n...\n 43|    /// Run the daemon: bind the broker (served on a background thread) + the\n*44|    /// seed-control channel (the foreground loop + liveness anchor) and serve\n 45|    /// until the process is killed. The body of `spt daemon`.\n 46|    pub fn run() -> io::Result<()> {\n 47|        // Identity-env sanitize (F-036 / MSG-IDENTITY W1, KNOWN-HAZARDS 7.39):\n...\n 272|        // nothing — the slot is ephemeral control, a restart correctly drops it).\n*273|        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n 274|        // [impl->REQ-SHELL-3]\n 275|        let drive_hub = Arc::new(crate::drivehub::DriveHub::new());\n 276|        {\n...\n 351|\n*352|        // The seed-control channel is the foreground loop. Binding it is what\n 353|        // arbitrates the single-daemon-per-home invariant: a second daemon racing\n 354|        // to start fails this bind and exits, leaving the winner serving. It\n 355|        // returns only on a graceful `daemon stop` (the KIND_STOP latch).\n...\n 357|\n*358|        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop\n 359|        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it\n 360|        // does not respawn the brain we are about to kill) THEN reap the brain\n 361|        // subtree — the brain, its shellwake watchers, and its detached Psyches die\n...\n 441|    let mut backoff = NET_RETRY_FIRST;\n*442|    loop {\n*443|        thread::sleep(backoff);\n 444|        if let Some(host) = try_start_net() {\n 445|            if broker.attach_net(host) {\n 446|                eprintln!(\"NET_ATTACHED: net endpoint bound on retry — brain starts the consumers\");\n...\n 470|/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the\n*471|/// daemon's foreground loop returns and the process exits cleanly.\n 472|// [impl->REQ-CLI-2]\n 473|pub fn request_stop() -> io::Result<()> {\n 474|    seedmap::request_stop(&seed_socket_name())\n...\n 482|    let mut waited = Duration::ZERO;\n*483|    while waited < START_TIMEOUT {\n 484|        if is_running() {\n 485|            return Ok(());\n 486|        }\n*487|        thread::sleep(START_POLL);\n 488|        waited += START_POLL;\n 489|    }\n 490|    Err(io::Error::new(\n...\n 501|/// daemon that would fight the service for the broker socket (the kitsubito\n*502|/// 2026-06-08 restart-loop). No manager ⇒ the detached spawn path as before.\n 503|// [impl->REQ-DAEMON-8]\n 504|pub fn ensure_running() -> io::Result<()> {\n 505|    if is_running() {\n...\n 929|/// the real per-rung launchers and log which rung won. The caller still confirms\n*930|/// readiness via the existing `brain.ready` poll — the returned pid is for\n 931|/// logging/return, not readiness (WMI/schtasks pids are the launcher's view).\n 932|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 933|#[cfg(windows)]\n...\n 1185|/// is recoverable from schtasks → returns a sentinel; readiness is the caller's\n*1186|/// `brain.ready` poll. This is the FALLBACK rung (WMI is primary); its `/tr`\n 1187|/// quoting is best-effort.\n 1188|// [impl->REQ-HAZARD-VIEWER-CLOSE-DETACH]\n 1189|#[cfg(windows)]\n...\n 1225|    }\n*1226|    // No pid from schtasks; sentinel — readiness is the brain.ready poll.\n 1227|    Ok(0)\n 1228|}\n 1229|\n...\n 1299|    // and caps at NET_RETRY_CAP — so a long boot-race wait settles to a steady\n*1300|    // 30s poll, never an ever-growing or zero-length spin.\n 1301|    #[test]\n 1302|    fn net_retry_backoff_doubles_then_caps() {\n 1303|        assert_eq!(net_retry_backoff(NET_RETRY_FIRST), Duration::from_secs(2));\n...\n 1533|            std::fs::write(&pidfile, gc.to_string()).expect(\"launcher: write gc pid\");\n*1534|            std::thread::sleep(Duration::from_secs(300));\n 1535|            std::process::exit(0);\n 1536|        }\n 1537|\n...\n 1605|            let mut found = None;\n*1606|            while Instant::now() < deadline {\n 1607|                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608|                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609|                        if p != 0 {\n...\n 1614|                }\n*1615|                std::thread::sleep(Duration::from_millis(20));\n 1616|            }\n 1617|            found.expect(\"launcher reports its grandchild pid\")\n 1618|        };\n...\n 1625|        for _ in 0..25 {\n*1626|            std::thread::sleep(Duration::from_millis(40));\n 1627|            if !is_process_alive(gc) {\n 1628|                still_alive = false;\n 1629|                break;\n...\n 1711|                }\n*1712|                thread::sleep(Duration::from_millis(5));\n 1713|            }\n 1714|            assert!(is_running(), \"the seed channel answers → daemon is up\");\n 1715|            // ensure_running short-circuits (no real process spawn).\n### deadline.rs\n*1|//! Durable absolute-deadline loop timing — ADR-0018 Q4 / restoration D5.\n 2|//!\n*3|//! Phase-significant periodic timing (the pulse loop) and one-shot deadlines\n 4|//! (alarms) must survive a brain swap: the supervisor respawns a fresh brain\n 5|//! (`Brain::cold_start`) and the outgoing brain is gone before the new one starts\n 6|//! (D3-3), so continuity cannot ride a brain→brain frame — it must come from the\n...\n 10|//!\n*11|//! - [`DeadlineAnchor`] — a **periodic** `(anchor, interval)` grid. Persisted\n 12|//!   ONCE per fresh/crash start; every fire is derived functionally\n 13|//!   ([`DeadlineAnchor::next_fire`]) with **no per-fire write**. An\n 14|//!   [`StartReason::Update`] restart keeps the anchor (phase preserved, lands\n 15|//!   mid-grid); a [`StartReason::Crash`]/[`StartReason::Cold`] start rewrites\n*16|//!   `anchor = now` (phase reset acceptable — the loop is idempotent catch-up).\n 17|//!   The preserve-vs-reset decision is keyed purely on the D3 [`StartReason`], no\n 18|//!   IPC round-trip.\n 19|//!\n...\n 31|//! (`pump`) need none — they already stagger from everything-due-now,\n*32|//! catch-up/idempotent/restart-safe; converting them would re-add the per-loop\n 33|//! writes Q4 minimizes.\n 34|\n 35|use std::path::{Path, PathBuf};\n...\n 41|\n*42|/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n 43|///\n 44|/// Keyed, not a singleton (ADR-0018 D5 amendment 6): a singleton would\n 45|/// cross-clobber the moment the live-agent adapter's **per-agent** pulse drivers\n...\n 65|impl DeadlineAnchor {\n*66|    /// Resolve a non-zero interval (doyle minor — ONE degrade rule, unit-tested):\n 67|    /// a zero `interval_ms` degrades to the config default pulse period (a `const`\n 68|    /// that is always `> 0`), so the derive can never divide by zero. The `else\n 69|    /// 1ms` floor is belt-and-braces should that default ever be made zero.\n...\n 81|    /// - [`StartReason::Update`] → **keep** the existing anchor (phase preserved);\n*82|    ///   only the interval refreshes from live config. If no file exists (updating\n 83|    ///   *from* a pre-D5 binary that never wrote one), fall back to a fresh anchor\n 84|    ///   — never fail an update on a missing file.\n 85|    /// - [`StartReason::Crash`] / [`StartReason::Cold`] → write a **fresh** anchor\n...\n 100|        let anchor = match (reason, Self::load(&path)) {\n*101|            // Update keeps the persisted phase; the interval refreshes to config.\n 102|            (StartReason::Update, Some(prev)) => DeadlineAnchor {\n 103|                anchor_ms: prev.anchor_ms,\n 104|                interval_ms,\n...\n 133|    /// The next fire at or after `now_ms`, derived functionally — **NO write**:\n*134|    /// `anchor + interval · ⌈max(0, now − anchor) / interval⌉`.\n 135|    ///\n 136|    /// On an exact grid instant this returns `now` (fire-now); a long jump\n 137|    /// (`now ≫ anchor`) snaps to the next grid point, collapsing missed ticks to\n...\n 148|\n*149|    /// The grid point **strictly after** `fired_ms` — the loop's post-fire\n 150|    /// advance (doyle minor — structural double-fire guard). Deriving the next\n 151|    /// deadline from the FIRED grid point rather than a re-sampled `now` means a\n*152|    /// fast tick re-sampling within the same millisecond cannot re-yield the same\n*153|    /// instant and double-fire. The loop body must advance via this, not via a\n*154|    /// fresh `now` alone, making the guard an invariant of the loop, not just an\n 155|    /// assertion in a test.\n 156|    pub fn next_after(&self, fired_ms: u64) -> u64 {\n 157|        self.next_fire(fired_ms.saturating_add(1))\n...\n 160|    /// The next deadline after firing at `fired_ms`, observed at `now_ms` — the\n*161|    /// loop's post-fire advance (doyle amendment 7). Combines BOTH invariants the\n*162|    /// loop needs in one pure expression so neither can be lost:\n 163|    ///\n 164|    /// - **strictly after the fired grid point** (the double-fire guard: an\n*165|    ///   instant tick where `now == fired` still steps exactly one grid);\n 166|    /// - **skips past `now`** so a long stall — a `pulse_tick` that outran the\n*167|    ///   interval, or the real killer, a laptop suspend/resume with the daemon\n 168|    ///   resident — collapses to **exactly one** catch-up fire instead of N rapid\n 169|    ///   back-to-back fires. (`next_after(deadline)` alone steps a single grid per\n 170|    ///   fire, so a deadline N grids behind `now` would fire N times in a tight\n*171|    ///   loop — the exact storm the loop-entry `next_fire(now)` collapse promises\n 172|    ///   to avoid; this composes with that collapse so a suspend/resume fires once\n 173|    ///   total.)\n 174|    pub fn advance_after_fire(&self, fired_ms: u64, now_ms: u64) -> u64 {\n...\n 226|        assert_eq!(a.next_fire(1_300), 1_300);\n*227|        // The post-fire advance must move STRICTLY forward by one interval, so a\n 228|        // re-sample at the same instant cannot double-fire (structural guard).\n 229|        assert_eq!(a.next_after(1_300), 1_400);\n 230|        assert_eq!(a.next_after(1_000), 1_100);\n### deelevate.rs\n 81|/// (still carrying `SPT_HOME`) sees no daemon and prints `DAEMON_NOT_RUNNING`\n*82|/// while the orphaned daemon serves the wrong home.\n 83|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n 84|pub fn apply_env_overrides(block: &[u16], overrides: &[(String, String)]) -> Vec<u16> {\n 85|    const NUL: u16 = 0;\n...\n 213|        let mut buf = vec![0u8; buf_len as usize];\n*214|        loop {\n 215|            let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };\n 216|            let mut result: *mut libc::passwd = std::ptr::null_mut();\n 217|            let rc = unsafe {\n...\n 332|        let mut buf = vec![0u8; buf_len as usize];\n*333|        loop {\n 334|            let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };\n 335|            let mut result: *mut libc::passwd = std::ptr::null_mut();\n 336|            let rc = unsafe {\n...\n 716|        let mut n = 0usize;\n*717|        while !(*p.add(n) == 0 && *p.add(n + 1) == 0) {\n 718|            n += 1;\n 719|        }","truncated":true,"truncatedBy":"bytes","totalLines":1175,"totalBytes":61421,"outputLines":958,"outputBytes":51136,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":1175,"totalBytes":61421,"outputLines":958,"outputBytes":51136,"shownRange":{"start":1,"end":958},"nextOffset":959}}},"isError":false,"timestamp":1784271462462}}
{"type":"message","id":"d521f392","parentId":"14d3be0c","timestamp":"2026-07-17T06:57:51.785Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating project index and brainproc dependencies**"},{"type":"toolCall","id":"call_a35oJTi3WN1ZNuKkwMbMlJcX|fc_038f28d3e93bbfd4016a59d26fb9308196b94fd57c01646b06","name":"read","arguments":{"i":"Reading brain loop wiring","path":"crates/spt-daemon/src/brainproc.rs","selector":"138-333"}},{"type":"toolCall","id":"call_kDh8RtvEdPYKU64ukWBKAP8A|fc_038f28d3e93bbfd4016a59d26fb9408196812769e855e02d48","name":"read","arguments":{"i":"Reading project writer loop","path":"crates/spt-daemon/src/projwriter.rs","selector":"1-760"}},{"type":"toolCall","id":"call_DV4DK8GiRPN9J1xe62lTpIfs|fc_038f28d3e93bbfd4016a59d26fb9488196a88f8db7a8d67a97","name":"read","arguments":{"i":"Reading dispatcher loop","path":"crates/spt-daemon/src/dispatch.rs","selector":"1-260,400-570,1080-1220"}},{"type":"toolCall","id":"call_7DvDECbRcCx85yVpG97Mxf3w|fc_038f28d3e93bbfd4016a59d26fb9508196a496c0707e0e21a8","name":"read","arguments":{"i":"Reading live reconcile loops","path":"crates/spt-daemon/src/livehost.rs","selector":"1-180,570-700,900-1050"}},{"type":"toolCall","id":"call_9C95wJFvVGYwbqkvPpE2IU0u|fc_038f28d3e93bbfd4016a59d26fb95481969e1b071553c8e5aa","name":"read","arguments":{"i":"Reading wake scan loop","path":"crates/spt-daemon/src/shellwake.rs","selector":"1-180,450-580"}},{"type":"toolCall","id":"call_asm4vxNyBXv17hpiuthIHEyB|fc_038f28d3e93bbfd4016a59d26fb95c81968de472c46d8100f6","name":"grep","arguments":{"i":"Tracing project index APIs","pattern":"ProjectIndex|project_index|IndexWriter|reconcile_project|index writer|fingerprint","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":45529,"output":368,"cacheRead":15872,"cacheWrite":0,"totalTokens":61769,"reasoningTokens":86,"cost":{"input":0.22764500000000001,"output":0.01104,"cacheRead":0.007936,"cacheWrite":0,"total":0.246621}},"stopReason":"toolUse","timestamp":1784271462519,"responseId":"resp_038f28d3e93bbfd4016a59d266cac081969c0b0a44b4242c4a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d26916c0819683ea3cd969580462","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdJvuXQFrp45ybgXI5EqZnTswAKTEkl33OQJ9HGJUUKX6nwuxLBUP9akkerDEZEy7iIR8Bc0Y9S57ni5c8os6YZp6pKbDQ65T0IcwkOvXx_Jb26cRs-p-ED0jUXrQatYMjw73epxj5KwKwB7PBOvJQ99HXVSaKRHzZi5juh9GL4HbIhnm1_qpcGPD7ffaempp57A8VPquIBcg8MSfl3idaBefjzUlZPjigB9SOm2_lExNM3sQgcXuEz_1RSWfVJt8X2L8ksfBTWGvklD20Xn0V6wnfpbhHjdROApFeA4W64H3llZWaAXea2czqBx5JrZSAjIzv5uZVEmQeJpDMkuGUB879V9xd_n4Tn18qs2w2g2NLiyAci9ZscrDmwsSeRIvinSFzNtHM9wXz3X8vgDt-2VzgR0M3Q0wJNTGaGh-vnqVpXM4od-eguv9BfSynEE0NC9uJJcltUk2rMP_i0T5x-XrfXOTxEwoV44YO06hTG-a0sdSIqbFMQCWj5vzndTBPaUy34wUn8ppLWRgGTkFuochVwxeyOcP3udvZ9JHWz2RvFREfFPB4PRQEgLOBOB9w8QDzW8398KKtlB8eDpgMIW0mT-RrWZR1lubOfNHdlb0BDLOnl7mBW3NkYr-9ykdYvfkPVU3y5AbvVj7TEgEB5LUU7agOsueUHSMlAJ_ab0U5KKbryNK5VFNcWdqrLcXvWLSBnbPZHuzHQBXeNITYAR9nhs61pxEWVid9KZUjZDl1MnZLf7m1ImS4VvG6-B4vVfmW9eJqXR1KORaYRFIZHnFWMtv5BHkVJL0BVAX9XUF_hyK7Z1p5ryobbFaDOfN2a5RpzMGdthvW5VxKrUmn-QRb0NiZ6Mp35LGam6NWfHCo-HRNwNrDF4NvDDH47jTJZRCjACxjTDBqWeevxdT2hiRuZtS5YoA0LTYBqG73vyLYjRca25fxf7onvz54C3j8rhSDIQI87F_m8NBjxDXLqolcSmz3iHTyKqDWEVQdZSSVVaDTGSBqbqThVQpgp7MUR72uf-e6s3LixueU7I83cQqDAkLbQXHYFNVdz2UWYzNdcVm8a5k1wlpfGB87tnZJobTbkMWpyhesyq4c5xdsjw5I8HM2UtFQ6QSgSYgA4b0_xezq9-6LpdYXWQzNnWDWKftl0PA2oXs7ndq7FMlM12CmIpaOCTWd4_AN2kuPYuFx_RtiskxOinDiw6OB9wm17O5WiDDs9RTeWE1yd4SQAr8kuEqeHt8NxGGwzse-RyN2i0dC37Ze0DQJV2eme7S-oU46M-l_USY3Un-u9gd3X1iaYulhPu7vhJuAl7G7_f8o7VALiLFDHwQRMObhrQJ0dTtp-GTw-BGkJlDG1XyPgaVyIkNIS6-6KmeKeZIJ0Re4iDcHQb2eXunvWM-_wv1JL2bNWvpbzh8FFynphJtbY-0CsudLQAY2xLOpQ5hxSdHLs=","summary":[{"type":"summary_text","text":"**Investigating project index and brainproc dependencies**"}]},{"id":"fc_038f28d3e93bbfd4016a59d26fb9308196b94fd57c01646b06","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain loop wiring\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"138-333\"}","call_id":"call_a35oJTi3WN1ZNuKkwMbMlJcX","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d26fb9408196812769e855e02d48","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading project writer loop\",\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"selector\":\"1-760\"}","call_id":"call_kDh8RtvEdPYKU64ukWBKAP8A","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d26fb9488196a88f8db7a8d67a97","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher loop\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"1-260,400-570,1080-1220\"}","call_id":"call_DV4DK8GiRPN9J1xe62lTpIfs","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d26fb9508196a496c0707e0e21a8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading live reconcile loops\",\"path\":\"crates/spt-daemon/src/livehost.rs\",\"selector\":\"1-180,570-700,900-1050\"}","call_id":"call_7DvDECbRcCx85yVpG97Mxf3w","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d26fb95481969e1b071553c8e5aa","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading wake scan loop\",\"path\":\"crates/spt-daemon/src/shellwake.rs\",\"selector\":\"1-180,450-580\"}","call_id":"call_9C95wJFvVGYwbqkvPpE2IU0u","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d26fb95c81968de472c46d8100f6","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing project index APIs\",\"pattern\":\"ProjectIndex|project_index|IndexWriter|reconcile_project|index writer|fingerprint\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_asm4vxNyBXv17hpiuthIHEyB","name":"grep"}]},"duration":9264.938100010157,"ttft":2584.705400004983,"contextSnapshot":{"promptTokens":61401,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_a35oJTi3WN1ZNuKkwMbMlJcX|fc_038f28d3e93bbfd4016a59d26fb9308196b94fd57c01646b06","toolName":"read","startedAt":"2026-07-17T06:57:51.786Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain loop wiring"},"id":"1ae96be9","parentId":"d521f392","timestamp":"2026-07-17T06:57:51.786Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kDh8RtvEdPYKU64ukWBKAP8A|fc_038f28d3e93bbfd4016a59d26fb9408196812769e855e02d48","toolName":"read","startedAt":"2026-07-17T06:57:51.786Z","args":{"path":"crates/spt-daemon/src/projwriter.rs"},"intent":"Reading project writer loop"},"id":"4be0e3ed","parentId":"1ae96be9","timestamp":"2026-07-17T06:57:51.786Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DV4DK8GiRPN9J1xe62lTpIfs|fc_038f28d3e93bbfd4016a59d26fb9488196a88f8db7a8d67a97","toolName":"read","startedAt":"2026-07-17T06:57:51.786Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher loop"},"id":"b34bbfa0","parentId":"4be0e3ed","timestamp":"2026-07-17T06:57:51.786Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7DvDECbRcCx85yVpG97Mxf3w|fc_038f28d3e93bbfd4016a59d26fb9508196a496c0707e0e21a8","toolName":"read","startedAt":"2026-07-17T06:57:51.786Z","args":{"path":"crates/spt-daemon/src/livehost.rs"},"intent":"Reading live reconcile loops"},"id":"43b4f93c","parentId":"b34bbfa0","timestamp":"2026-07-17T06:57:51.786Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9C95wJFvVGYwbqkvPpE2IU0u|fc_038f28d3e93bbfd4016a59d26fb95481969e1b071553c8e5aa","toolName":"read","startedAt":"2026-07-17T06:57:51.786Z","args":{"path":"crates/spt-daemon/src/shellwake.rs"},"intent":"Reading wake scan loop"},"id":"c0bf3698","parentId":"43b4f93c","timestamp":"2026-07-17T06:57:51.786Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_asm4vxNyBXv17hpiuthIHEyB|fc_038f28d3e93bbfd4016a59d26fb95c81968de472c46d8100f6","toolName":"grep","startedAt":"2026-07-17T06:57:51.786Z","args":{"path":"crates"},"intent":"Tracing project index APIs"},"id":"acaf5d59","parentId":"c0bf3698","timestamp":"2026-07-17T06:57:51.786Z"}
{"type":"message","id":"00ab5658","parentId":"acaf5d59","timestamp":"2026-07-17T06:57:51.805Z","message":{"role":"toolResult","toolCallId":"call_9C95wJFvVGYwbqkvPpE2IU0u|fc_038f28d3e93bbfd4016a59d26fb95481969e1b071553c8e5aa","toolName":"read","content":[{"type":"text","text":"//! Shell wake-watcher hosting (M5-D4b — CONTEXT §Shell sleep/wake): the\n//! offline half of the online/offline mutual exclusivity. While a shell\n//! instance is **offline** and its manifest declares a `wake_command`, the\n//! daemon runs that template as a long-running **wake-watcher** child whose\n//! sole job is to fire a wake; while the shell binary runs, no watcher does\n//! ([`reconcile_once`] flips between them — spt-core owns the flip).\n//!\n//! **Exit-opcode supervision:** the watcher exiting with [`WAKE_OPCODE`] ⇒\n//! the wake resolution ([`resolve_wake`]) brings the shell online. Any other\n//! exit is a crash ⇒ respawn with exponential backoff ([`backoff_ms`]) and\n//! **give-up** after [`WakeParams::give_up_after`] consecutive crashes (a\n//! durable [`WAKER_GAVE_UP_FILE`] marker the next shell activity — any\n//! (re)launch — clears; crash-bug safety, the watcher can never crash-loop\n//! forever). One watcher per offline instance: the in-process [`WakeSet`]\n//! entry plus the perch's [`WAKER_PID_FILE`] (which also lets a freshly\n//! booted daemon kill a dead daemon's orphaned watcher before adopting).\n//!\n//! **Scheduling (KH 7.4):** every watcher gets its own supervision thread —\n//! a hung waker stalls nothing; the reconcile loop itself is one bounded\n//! sweep per tick off the daemon's control surfaces. Watcher children are\n//! *supervised* (waited) children, never detached-immortal — the KH 5.6\n//! pipe-inherit shape (an unwaitable child holding a capture pipe) does not\n//! arise; stdio is null regardless.\n// [impl->REQ-SHELL-2]\n\nuse std::collections::HashMap;\nuse std::path::Path;\nuse std::process::{Command, Stdio};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse spt_runtime::manifest::{AdapterKind, Manifest, Shell};\nuse spt_runtime::registry::AdapterRecord;\nuse spt_store::shellinfo::{self, SHELL_STATUS_OFFLINE};\n\n/// The wake opcode (documented in `docs/MANIFEST.md` §Shell adapters): a\n/// wake-watcher exiting with **86** fires the wake resolution; every other\n/// exit is a crash.\npub const WAKE_OPCODE: i32 = 86;\n\n/// The running watcher's OS pid, parked on the shell perch — the\n/// one-watcher-per-instance lock and the cross-daemon-restart kill handle.\npub const WAKER_PID_FILE: &str = \"waker.pid\";\n\n/// The give-up latch: present ⇒ the watcher crash-looped past its budget and\n/// the reconciler must NOT restart it. Cleared by the next shell activity\n/// (any (re)launch — [`crate::shellhost::launch_shell`]).\npub const WAKER_GAVE_UP_FILE: &str = \"waker.gaveup\";\n\n/// Watcher supervision knobs (injectable for tests; defaults are production).\n#[derive(Debug, Clone, Copy)]\npub struct WakeParams {\n    /// First-crash respawn delay; doubles per consecutive crash.\n    pub backoff_base_ms: u64,\n    /// Backoff ceiling.\n    pub backoff_cap_ms: u64,\n    /// Consecutive crash-exits before the give-up latch drops.\n    pub give_up_after: u32,\n}\n\nimpl Default for WakeParams {\n    fn default() -> Self {\n        Self {\n            backoff_base_ms: 1_000,\n            backoff_cap_ms: 60_000,\n            give_up_after: 6,\n        }\n    }\n}\n\n/// Exponential backoff for `failures` consecutive crash-exits:\n/// `base × 2^(failures-1)`, saturating at the cap (the\n/// `spt-net::pairing::ratelimit` recipe). Zero failures ⇒ no wait.\n// [impl->REQ-SHELL-2]\npub fn backoff_ms(params: &WakeParams, failures: u32) -> u64 {\n    if failures == 0 {\n        return 0;\n    }\n    params\n        .backoff_base_ms\n        .saturating_mul(1u64 << (failures - 1).min(20))\n        .min(params.backoff_cap_ms)\n}\n\n/// The live watcher registry: one supervision thread per offline instance,\n/// keyed `owner/shell_id`. Shared between the daemon's reconcile loop and\n/// anything that must stop a watcher.\n#[derive(Default)]\npub struct WakeSet {\n    inner: Mutex<HashMap<(String, String), WatcherHandle>>,\n}\n\nstruct WatcherHandle {\n    stop: Arc<AtomicBool>,\n    thread: JoinHandle<()>,\n}\n\nimpl WakeSet {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// How many watchers are live (finished threads pruned first).\n    pub fn len(&self) -> usize {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    fn contains(&self, owner: &str, shell_id: &str) -> bool {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        match map.get(&(owner.to_string(), shell_id.to_string())) {\n            Some(h) if !h.thread.is_finished() => true,\n            Some(_) => {\n                map.remove(&(owner.to_string(), shell_id.to_string()));\n                false\n            }\n            None => false,\n        }\n    }\n\n    fn insert(&self, owner: &str, shell_id: &str, handle: WatcherHandle) {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.insert((owner.to_string(), shell_id.to_string()), handle);\n    }\n\n    /// Signal one watcher to stop and kill its child (the perch pid file is\n    /// the kill handle). The thread sees the kill as a non-opcode exit with\n    /// its stop flag up and returns without counting a failure.\n    fn stop_watcher(&self, owlery: &Path, owner: &str, shell_id: &str) {\n        let handle = {\n            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n            map.remove(&(owner.to_string(), shell_id.to_string()))\n        };\n        if let Some(h) = handle {\n            h.stop.store(true, Ordering::SeqCst);\n            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);\n            kill_waker_at(&perch);\n            let _ = h.thread.join();\n        }\n    }\n}\n\n/// Kill the recorded waker pid (if any) and retire the pid file — the\n/// mutual-exclusivity enforcement point and the orphan cleanup\n/// ([`crate::shellhost::launch_shell`] calls this before the binary rises;\n/// the reconciler calls it before adopting an instance a dead daemon left).\n// [impl->REQ-SHELL-2]\npub fn kill_waker_at(shell_perch: &Path) {\n    let pid_file = shell_perch.join(WAKER_PID_FILE);\n    if let Ok(s) = std::fs::read_to_string(&pid_file) {\n        if let Ok(pid) = s.trim().parse::<u32>() {\n            if pid != 0 && spt_store::proc::is_process_alive(pid) {\n                crate::shellhost::kill_shell_pid(pid);\n            }\n        }\n    }\n    let _ = std::fs::remove_file(pid_file);\n}\n\n/// Clear the give-up latch — any (re)launch is \"next shell activity\".\npub fn clear_gave_up(shell_perch: &Path) {\n    let _ = std::fs::remove_file(shell_perch.join(WAKER_GAVE_UP_FILE));\n}\n\n/// One watcher's supervision loop (runs on its own thread): spawn the\n/// tokenized `wake_command`, park the pid, wait; `exit(WAKE_OPCODE)` ⇒ run\n/// `resolve` once and finish; any other exit ⇒ respawn with exponential\n/// backoff until the give-up budget, then drop the durable latch. A raised\n/// `stop` flag ends the loop without counting the (killed) child as a crash.\n// [impl->REQ-SHELL-2]\npub fn watcher_run(\n    owlery: &Path,\n    owner: &str,\n…\n) {\n…\n}\n…\nfn fill_wake_command(\n…\n) -> Result<Vec<String>, String> {\n…\n    let tokens =\n        spt_runtime::runtime::fill_template_tokens(wake_command, &keys).map_err(|e| e.to_string())?;\n    if tokens.is_empty() {\n        return Err(\"empty wake_command\".into());\n    }\n    Ok(tokens)\n}\n\n/// One reconcile sweep — the invariant holder for the online/offline mutual\n/// exclusivity (CONTEXT: \"spt-core flips between them\"):\n///\n/// - **offline + `wake_command` + no give-up latch + no live watcher** ⇒\n///   kill any orphaned waker pid (a dead daemon's leftover), then start a\n///   supervision thread.\n/// - **not offline (onlined / torn down / deregistered) + live watcher** ⇒\n///   stop it (flag + kill).\n///\n/// Runs at daemon boot and every tick — lifecycle flips happen in CLI-process\n/// library code, so the *reconciler* holds the invariant, not the flipping\n/// caller; a flip is at most one tick stale.\n// [impl->REQ-SHELL-2]\npub fn reconcile_once(\n    owlery: &Path,\n    registered: &[(AdapterRecord, Manifest)],\n    adapters_dir: &Path,\n    set: &Arc<WakeSet>,\n    params: &WakeParams,\n) {\n    // Stop side first: watchers whose instance is gone or no longer eligible.\n    let live: Vec<(String, String)> = {\n        let mut map = set.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.keys().cloned().collect()\n    };\n    for (owner, shell_id) in live {\n        let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);\n        let still_eligible = shellinfo::read_shell_info(&perch)\n            .map(|i| i.status == SHELL_STATUS_OFFLINE)\n            .unwrap_or(false);\n        if !still_eligible {\n            set.stop_watcher(owlery, &owner, &shell_id);\n        }\n    }\n\n    // Start side: every offline instance with a wake_command and headroom.\n    for owner in spt_store::perch::list_self_perch_ids(owlery) {\n        for (shell_id, info) in shellinfo::list_shells(owlery, &owner) {\n            if info.status != SHELL_STATUS_OFFLINE || set.contains(&owner, &shell_id) {\n                continue;\n            }\n            let Some(shell) = shell_section_of(registered, adapters_dir, &info.adapter_name) else {\n                continue; // deregistered adapter: nothing to run\n            };\n            let Some(wake_command) = shell.wake_command.as_deref() else {\n                continue;\n            };\n            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);\n            if perch.join(WAKER_GAVE_UP_FILE).exists() {\n                continue; // crash-latched until the next shell activity\n            }\n            // Adopt cleanly: a dead daemon's watcher must not double-run.\n            kill_waker_at(&perch);\n            let tokens = match fill_wake_command(&shell_id, &info.adapter_name, wake_command) {\n                Ok(t) => t,\n                Err(e) => {\n                    eprintln!(\"WAKER_TEMPLATE:{owner}/{shell_id}: {e}\");\n                    continue;\n                }\n            };\n            let stop = Arc::new(AtomicBool::new(false));\n            let handle = {\n                let stop = Arc::clone(&stop);\n                let owlery = owlery.to_path_buf();\n                let owner = owner.clone();\n                let shell_id = shell_id.clone();\n                let adapter_name = info.adapter_name.clone();\n                let params = *params;\n                std::thread::spawn(move || {\n                    watcher_run(&owlery, &owner, &shell_id, &tokens, &params, &stop, || {\n                        resolve_wake(&owlery, &owner, &shell_id, &adapter_name, &shell)\n                    });\n                })\n            };\n            set.insert(\n                &owner,\n                &shell_id,\n                WatcherHandle {\n                    stop,\n                    thread: handle,\n                },\n            );\n        }\n    }\n}\n\n/// The reconcile cadence (ms): lifecycle flips land in CLI processes, so the\n/// daemon's loop is the invariant holder — a flip is at most one tick stale.\npub const RECONCILE_INTERVAL_MS: u64 = 5_000;\n\n/// Spawn the daemon's wake host: one thread sweeping [`reconcile_once`] at\n/// boot and every [`RECONCILE_INTERVAL_MS`] until `stop`. The registered set\n/// is re-read each sweep (adapter add/remove lands between ticks).\n// [impl->REQ-SHELL-2]\npub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(WakeSet::new());\n        let params = WakeParams::default();\n        while !stop.load(Ordering::SeqCst) {\n            let owlery = spt_store::perch::owlery_dir();\n            let adapters_dir = spt_store::perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &params);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_store::shellinfo::{spawn_record, SHELL_STATUS_ONLINE};\n\n    fn fast_params() -> WakeParams {\n        WakeParams {\n            backoff_base_ms: 10,\n…\n        }\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! Shell wake-watcher hosting (M5-D4b — CONTEXT §Shell sleep/wake): the\n//! offline half of the online/offline mutual exclusivity. While a shell\n//! instance is **offline** and its manifest declares a `wake_command`, the\n//! daemon runs that template as a long-running **wake-watcher** child whose\n//! sole job is to fire a wake; while the shell binary runs, no watcher does\n//! ([`reconcile_once`] flips between them — spt-core owns the flip).\n//!\n//! **Exit-opcode supervision:** the watcher exiting with [`WAKE_OPCODE`] ⇒\n//! the wake resolution ([`resolve_wake`]) brings the shell online. Any other\n//! exit is a crash ⇒ respawn with exponential backoff ([`backoff_ms`]) and\n//! **give-up** after [`WakeParams::give_up_after`] consecutive crashes (a\n//! durable [`WAKER_GAVE_UP_FILE`] marker the next shell activity — any\n//! (re)launch — clears; crash-bug safety, the watcher can never crash-loop\n//! forever). One watcher per offline instance: the in-process [`WakeSet`]\n//! entry plus the perch's [`WAKER_PID_FILE`] (which also lets a freshly\n//! booted daemon kill a dead daemon's orphaned watcher before adopting).\n//!\n//! **Scheduling (KH 7.4):** every watcher gets its own supervision thread —\n//! a hung waker stalls nothing; the reconcile loop itself is one bounded\n//! sweep per tick off the daemon's control surfaces. Watcher children are\n//! *supervised* (waited) children, never detached-immortal — the KH 5.6\n//! pipe-inherit shape (an unwaitable child holding a capture pipe) does not\n//! arise; stdio is null regardless.\n// [impl->REQ-SHELL-2]\n\nuse std::collections::HashMap;\nuse std::path::Path;\nuse std::process::{Command, Stdio};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse spt_runtime::manifest::{AdapterKind, Manifest, Shell};\nuse spt_runtime::registry::AdapterRecord;\nuse spt_store::shellinfo::{self, SHELL_STATUS_OFFLINE};\n\n/// The wake opcode (documented in `docs/MANIFEST.md` §Shell adapters): a\n/// wake-watcher exiting with **86** fires the wake resolution; every other\n/// exit is a crash.\npub const WAKE_OPCODE: i32 = 86;\n\n/// The running watcher's OS pid, parked on the shell perch — the\n/// one-watcher-per-instance lock and the cross-daemon-restart kill handle.\npub const WAKER_PID_FILE: &str = \"waker.pid\";\n\n/// The give-up latch: present ⇒ the watcher crash-looped past its budget and\n/// the reconciler must NOT restart it. Cleared by the next shell activity\n/// (any (re)launch — [`crate::shellhost::launch_shell`]).\npub const WAKER_GAVE_UP_FILE: &str = \"waker.gaveup\";\n\n/// Watcher supervision knobs (injectable for tests; defaults are production).\n#[derive(Debug, Clone, Copy)]\npub struct WakeParams {\n    /// First-crash respawn delay; doubles per consecutive crash.\n    pub backoff_base_ms: u64,\n    /// Backoff ceiling.\n    pub backoff_cap_ms: u64,\n    /// Consecutive crash-exits before the give-up latch drops.\n    pub give_up_after: u32,\n}\n\nimpl Default for WakeParams {\n    fn default() -> Self {\n        Self {\n            backoff_base_ms: 1_000,\n            backoff_cap_ms: 60_000,\n            give_up_after: 6,\n        }\n    }\n}\n\n/// Exponential backoff for `failures` consecutive crash-exits:\n/// `base × 2^(failures-1)`, saturating at the cap (the\n/// `spt-net::pairing::ratelimit` recipe). Zero failures ⇒ no wait.\n// [impl->REQ-SHELL-2]\npub fn backoff_ms(params: &WakeParams, failures: u32) -> u64 {\n    if failures == 0 {\n        return 0;\n    }\n    params\n        .backoff_base_ms\n        .saturating_mul(1u64 << (failures - 1).min(20))\n        .min(params.backoff_cap_ms)\n}\n\n/// The live watcher registry: one supervision thread per offline instance,\n/// keyed `owner/shell_id`. Shared between the daemon's reconcile loop and\n/// anything that must stop a watcher.\n#[derive(Default)]\npub struct WakeSet {\n    inner: Mutex<HashMap<(String, String), WatcherHandle>>,\n}\n\nstruct WatcherHandle {\n    stop: Arc<AtomicBool>,\n    thread: JoinHandle<()>,\n}\n\nimpl WakeSet {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// How many watchers are live (finished threads pruned first).\n    pub fn len(&self) -> usize {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    fn contains(&self, owner: &str, shell_id: &str) -> bool {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        match map.get(&(owner.to_string(), shell_id.to_string())) {\n            Some(h) if !h.thread.is_finished() => true,\n            Some(_) => {\n                map.remove(&(owner.to_string(), shell_id.to_string()));\n                false\n            }\n            None => false,\n        }\n    }\n\n    fn insert(&self, owner: &str, shell_id: &str, handle: WatcherHandle) {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.insert((owner.to_string(), shell_id.to_string()), handle);\n    }\n\n    /// Signal one watcher to stop and kill its child (the perch pid file is\n    /// the kill handle). The thread sees the kill as a non-opcode exit with\n    /// its stop flag up and returns without counting a failure.\n    fn stop_watcher(&self, owlery: &Path, owner: &str, shell_id: &str) {\n        let handle = {\n            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n            map.remove(&(owner.to_string(), shell_id.to_string()))\n        };\n        if let Some(h) = handle {\n            h.stop.store(true, Ordering::SeqCst);\n            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);\n            kill_waker_at(&perch);\n            let _ = h.thread.join();\n        }\n    }\n}\n\n/// Kill the recorded waker pid (if any) and retire the pid file — the\n/// mutual-exclusivity enforcement point and the orphan cleanup\n/// ([`crate::shellhost::launch_shell`] calls this before the binary rises;\n/// the reconciler calls it before adopting an instance a dead daemon left).\n// [impl->REQ-SHELL-2]\npub fn kill_waker_at(shell_perch: &Path) {\n    let pid_file = shell_perch.join(WAKER_PID_FILE);\n    if let Ok(s) = std::fs::read_to_string(&pid_file) {\n        if let Ok(pid) = s.trim().parse::<u32>() {\n            if pid != 0 && spt_store::proc::is_process_alive(pid) {\n                crate::shellhost::kill_shell_pid(pid);\n            }\n        }\n    }\n    let _ = std::fs::remove_file(pid_file);\n}\n\n/// Clear the give-up latch — any (re)launch is \"next shell activity\".\npub fn clear_gave_up(shell_perch: &Path) {\n    let _ = std::fs::remove_file(shell_perch.join(WAKER_GAVE_UP_FILE));\n}\n\n/// One watcher's supervision loop (runs on its own thread): spawn the\n/// tokenized `wake_command`, park the pid, wait; `exit(WAKE_OPCODE)` ⇒ run\n/// `resolve` once and finish; any other exit ⇒ respawn with exponential\n/// backoff until the give-up budget, then drop the durable latch. A raised\n/// `stop` flag ends the loop without counting the (killed) child as a crash.\n// [impl->REQ-SHELL-2]\npub fn watcher_run(\n    owlery: &Path,\n    owner: &str,\n…\n) {\n…\n}\n…\nfn fill_wake_command(\n…\n) -> Result<Vec<String>, String> {\n…\n    let tokens =\n        spt_runtime::runtime::fill_template_tokens(wake_command, &keys).map_err(|e| e.to_string())?;\n    if tokens.is_empty() {\n        return Err(\"empty wake_command\".into());\n    }\n    Ok(tokens)\n}\n\n/// One reconcile sweep — the invariant holder for the online/offline mutual\n/// exclusivity (CONTEXT: \"spt-core flips between them\"):\n///\n/// - **offline + `wake_command` + no give-up latch + no live watcher** ⇒\n///   kill any orphaned waker pid (a dead daemon's leftover), then start a\n///   supervision thread.\n/// - **not offline (onlined / torn down / deregistered) + live watcher** ⇒\n///   stop it (flag + kill).\n///\n/// Runs at daemon boot and every tick — lifecycle flips happen in CLI-process\n/// library code, so the *reconciler* holds the invariant, not the flipping\n/// caller; a flip is at most one tick stale.\n// [impl->REQ-SHELL-2]\npub fn reconcile_once(\n    owlery: &Path,\n    registered: &[(AdapterRecord, Manifest)],\n    adapters_dir: &Path,\n    set: &Arc<WakeSet>,\n    params: &WakeParams,\n) {\n    // Stop side first: watchers whose instance is gone or no longer eligible.\n    let live: Vec<(String, String)> = {\n        let mut map = set.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.keys().cloned().collect()\n    };\n    for (owner, shell_id) in live {\n        let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);\n        let still_eligible = shellinfo::read_shell_info(&perch)\n            .map(|i| i.status == SHELL_STATUS_OFFLINE)\n            .unwrap_or(false);\n        if !still_eligible {\n            set.stop_watcher(owlery, &owner, &shell_id);\n        }\n    }\n\n    // Start side: every offline instance with a wake_command and headroom.\n    for owner in spt_store::perch::list_self_perch_ids(owlery) {\n        for (shell_id, info) in shellinfo::list_shells(owlery, &owner) {\n            if info.status != SHELL_STATUS_OFFLINE || set.contains(&owner, &shell_id) {\n                continue;\n            }\n            let Some(shell) = shell_section_of(registered, adapters_dir, &info.adapter_name) else {\n                continue; // deregistered adapter: nothing to run\n            };\n            let Some(wake_command) = shell.wake_command.as_deref() else {\n                continue;\n            };\n            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);\n            if perch.join(WAKER_GAVE_UP_FILE).exists() {\n                continue; // crash-latched until the next shell activity\n            }\n            // Adopt cleanly: a dead daemon's watcher must not double-run.\n            kill_waker_at(&perch);\n            let tokens = match fill_wake_command(&shell_id, &info.adapter_name, wake_command) {\n                Ok(t) => t,\n                Err(e) => {\n                    eprintln!(\"WAKER_TEMPLATE:{owner}/{shell_id}: {e}\");\n                    continue;\n                }\n            };\n            let stop = Arc::new(AtomicBool::new(false));\n            let handle = {\n                let stop = Arc::clone(&stop);\n                let owlery = owlery.to_path_buf();\n                let owner = owner.clone();\n                let shell_id = shell_id.clone();\n                let adapter_name = info.adapter_name.clone();\n                let params = *params;\n                std::thread::spawn(move || {\n                    watcher_run(&owlery, &owner, &shell_id, &tokens, &params, &stop, || {\n                        resolve_wake(&owlery, &owner, &shell_id, &adapter_name, &shell)\n                    });\n                })\n            };\n            set.insert(\n                &owner,\n                &shell_id,\n                WatcherHandle {\n                    stop,\n                    thread: handle,\n                },\n            );\n        }\n    }\n}\n\n/// The reconcile cadence (ms): lifecycle flips land in CLI processes, so the\n/// daemon's loop is the invariant holder — a flip is at most one tick stale.\npub const RECONCILE_INTERVAL_MS: u64 = 5_000;\n\n/// Spawn the daemon's wake host: one thread sweeping [`reconcile_once`] at\n/// boot and every [`RECONCILE_INTERVAL_MS`] until `stop`. The registered set\n/// is re-read each sweep (adapter add/remove lands between ticks).\n// [impl->REQ-SHELL-2]\npub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(WakeSet::new());\n        let params = WakeParams::default();\n        while !stop.load(Ordering::SeqCst) {\n            let owlery = spt_store::perch::owlery_dir();\n            let adapters_dir = spt_store::perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &params);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_store::shellinfo::{spawn_record, SHELL_STATUS_ONLINE};\n\n    fn fast_params() -> WakeParams {\n        WakeParams {\n            backoff_base_ms: 10,\n…\n        }\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,null,186,null,252,null,435,null,439,null,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,null,583,584,null,1000]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\shellwake.rs"}}},"isError":false,"timestamp":1784271471805}}
{"type":"message","id":"07e4a128","parentId":"00ab5658","timestamp":"2026-07-17T06:57:51.834Z","message":{"role":"toolResult","toolCallId":"call_DV4DK8GiRPN9J1xe62lTpIfs|fc_038f28d3e93bbfd4016a59d26fb9488196a88f8db7a8d67a97","toolName":"read","content":[{"type":"text","text":"//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\n//! the hand-wired serve loops of D5–D8 into production behavior.\n//!\n//! Every E2E test so far drove the serve half manually (a probe brain calling\n//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\n//! a known stream). In production nothing did — a peer could dial and write\n//! and the bytes sat in the broker's stream log unread. This module is the\n//! missing consumer: a daemon-hosted loop that watches the broker's stream\n//! table for **peer-initiated** streams, classifies each by its first NDJSON\n//! record, and hands it to the right serve loop on its own worker thread.\n//!\n//! ## Direction (the gotcha-#9 fact, now first-class)\n//!\n//! The stream table holds locally-opened outbound streams too. The dispatcher\n//! claims only rows with `initiated_locally == false` — serving our own\n//! requester stream would deadlock the requester against ourselves. The flag\n//! is broker-recorded at registration (`open_stream` vs the accept loop),\n//! never inferred.\n//!\n//! ## Classification (first-record demux)\n//!\n//! Streams are single-purpose; the opener's first record names the protocol.\n//! The kind-tagged families collide on variant names (`request` is both\n//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n//! demux keys on the first line's **shape**, not its kind alone:\n//!\n//! | first line | family |\n//! |---|---|\n//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\n//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\n//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\n//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\n//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\n//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n//! | anything else | unknown — dropped, loud |\n//!\n//! A family whose production wiring lands in a later slice is still\n//! classified here (the table is complete) but routed to a typed\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n//!\n//! ## Worker model\n//!\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\n//! connection (the broker is multi-client by design; the probe-brain pattern\n//! every E2E test uses). The peek and the serve use **separate** connections:\n//! the peek brain subscribes from 0 to read the first line and is dropped;\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n//! stream's event feed alone (no double-subscription interleave). Origin is\n//! the table's `remote_id_hex` — handshake-proven, never payload\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n// [impl->REQ-NET-1]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n\nuse std::collections::HashMap;\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::notif::NotifDecoder;\nuse spt_net::net::replicate::{\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n};\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\nuse spt_store::contextstore::ContextStore;\nuse spt_store::notif::NotifStore;\nuse spt_store::perch;\nuse spt_store::roster::RosterStore;\n\nuse crate::attach::serve_attach;\nuse crate::brain::{Brain, BrokerEvent};\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\nuse crate::propagate::serve_update;\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\nuse crate::relcache::ReleaseCache;\nuse crate::sync::{serve_sync, SyncPolicy};\nuse crate::wan::receive_wan;\nuse crate::xfer::serve_xfer;\n\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n/// workers block on their stream's event feed).\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum StreamFamily {\n    /// A context sync pull (`SyncRecord::Request`).\n    Sync,\n    /// An update pull (`UpdRecord::Query`).\n    Update,\n    /// A notif replication feed (`NotifRecord::Row`).\n    Notif,\n    /// A WAN message feed (untagged `WanMessage` lines).\n    WanMsg,\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\n    Registry,\n    /// A remote-drive attach (`AttachRecord::Request`).\n    Attach,\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\n    Xfer,\n    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\n    /// node, M5-D5b).\n    Rest,\n    /// A cross-node shell-link op (`ShellLinkRecord::Request` — relink/cmd\n    /// against an owner's shell on this node, M5-D8c).\n    ShellLink,\n    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\n    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\n    ServeProbe,\n    /// Unclassifiable — dropped.\n    Unknown,\n}\n\n/// Classify one decoded first line by shape (module-docs demux table).\npub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {\n    match line.get(\"kind\").and_then(|k| k.as_str()) {\n        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\n        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\n        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\n            StreamFamily::Rest\n        }\n        Some(\"request\") if line.get(\"owner\").is_some() && line.get(\"shell_ref\").is_some() => {\n            StreamFamily::ShellLink\n        }\n        Some(\"request\") if line.get(\"serve_probe\").is_some() => StreamFamily::ServeProbe,\n        Some(\"query\") => StreamFamily::Update,\n        // The status-only convergence query rides the update family (M8\n        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\n        Some(\"status_query\") => StreamFamily::Update,\n        Some(\"row\") => StreamFamily::Notif,\n        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n        Some(_) => StreamFamily::Unknown,\n        None => {\n            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\n                StreamFamily::WanMsg\n            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\n                StreamFamily::Registry\n            } else if line.get(\"subnet\").is_some()\n                && line.get(\"node\").is_some()\n                && line.get(\"label\").is_some()\n            {\n                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\n                // sends label records first (no instance rows), so the feed's\n                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n                StreamFamily::Registry\n            } else {\n                StreamFamily::Unknown\n            }\n        }\n    }\n}\n\n/// Why a dispatched stream's worker ended — loud + typed, never silent.\n#[derive(Debug)]\npub enum DispatchOutcome {\n    /// The family's serve loop ran to its own outcome (rendered for the log).\n    Served(String),\n    /// The first line never arrived / didn't parse — dropped.\n    Unknown,\n    /// The worker failed (IO against broker or store).\n    Failed(String),\n}\n\n/// A claimed stream's dispatch lifecycle (ADR-0038 C,\n/// REQ-DISPATCH-CLAIM-RETRY). The pre-fix `claimed` was a one-shot `HashSet`\n/// insert performed BEFORE worker spawn and never cleared — one transient\n/// worker-setup failure permanently abandoned the stream. Claims now\n/// distinguish in-flight vs terminal vs retryable, and a transient failure\n/// requeues bounded (backoff-shaped, never a hot loop).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ClaimState {\n    /// A worker owns the stream right now (`attempts` = prior failures).\n    InFlight { attempts: u32 },\n    /// Done for this dispatcher's lifetime: served, unclassifiable, or\n    /// retries exhausted. Terminal outcomes never re-enter the queue.\n    Terminal,\n    /// A transient failure released the claim; eligible again at `next_at`.\n    Retry { attempts: u32, next_at: Instant },\n}\n\n/// Bounded retry budget per stream per dispatcher lifetime — transient setup\n/// failures recover, a persistently-failing stream goes terminal loud.\nconst MAX_DISPATCH_ATTEMPTS: u32 = 3;\n\n/// Backoff base for a released claim (doubles per failed attempt).\nconst DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n\n/// The backoff before retry number `attempts` (1-based) runs.\nfn retry_backoff(attempts: u32) -> Duration {\n    DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))\n}\n\n/// Why a worker failed — the retry CLASSIFICATION (ADR-0038 Amendment fix 5,\n/// REQ-DISPATCH-FALLBACK-CIRCUIT). The two classes retry differently:\n/// a pre-setup transient keeps the bounded ×3 budget (the D3 semantics);\n/// a seat-blocked failure (poisoned replay / prior subscriber still draining)\n/// trips the GLOBAL circuit breaker and requeues WITHOUT burning budget —\n/// the stream must recover once the wedged seat's writer dies (≤ the 15s\n/// bounded-write deadline), and three fast retries would burn to Terminal\n/// before that (the v0.33 abandonment this explicitly must not regress to).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum FailClass {\n    /// Ordinary transient (broker IPC hiccup, opener not pinned yet, …).\n    Transient,\n    /// The stream's subscriber seat is poisoned or still draining — retrying\n    /// immediately would manufacture another replay victim.\n    SeatBlocked,\n}\n\n/// Classify a worker failure message (the seat-blocked shapes are the two\n/// strings the nethost subscriber discipline emits).\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn classify_failure(msg: &str) -> FailClass {\n    if msg.contains(\"subscriber busy\")\n        || msg.contains(\"replay halted\")\n        || msg.contains(\"lease canceled\")\n    {\n        FailClass::SeatBlocked\n    } else {\n        FailClass::Transient\n    }\n}\n\n/// Whether an opener-query error is the OLD broker's unknown-verb answer —\n/// the ONLY error class that may fall back to the ring peek (ADR-0038\n/// Amendment fix 1). `send_error` on an unknown kind replies exactly\n/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n/// wire fact. Everything else (transport timeout, EOF, poison) is a\n/// transient — Failed → bounded requeue, never a second replay subscriber.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn is_unsupported_verb(e: &io::Error) -> bool {\n    e.to_string().contains(\"unknown command kind\")\n}\n\n/// The dispatcher's GLOBAL circuit breaker (ADR-0038 Amendment fix 5): a\n/// seat-blocked failure trips it — claiming pauses dispatcher-wide for a\n/// doubling window (base 2s, cap 30s), so retry machinery cannot hammer a\n/// wedged conn with fresh replay subscribers. Any successful serve resets it.\n/// PURE (instant-in, decision-out) — unit-tested trip/gate/reset.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n#[derive(Debug, Default)]\nstruct CircuitBreaker {\n    until: Option<Instant>,\n    consecutive: u32,\n}\n\nconst BREAKER_BASE: Duration = Duration::from_secs(2);\nconst BREAKER_CAP: Duration = Duration::from_secs(30);\n\nimpl CircuitBreaker {\n    /// A seat-blocked failure landed: open (or extend) the window.\n    fn trip(&mut self, now: Instant) -> Duration {\n        let window = (BREAKER_BASE * 2u32.saturating_pow(self.consecutive)).min(BREAKER_CAP);\n        self.consecutive = self.consecutive.saturating_add(1);\n        self.until = Some(now + window);\n…\n    }\n…\n}\n…\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n/// worker (policies/stores re-load from the canonical paths per request —\n/// the access-gate stance, config edits take effect without a restart).\n#[derive(Debug, Clone)]\npub struct DispatchPaths {\n    /// The perch tree (WAN message funnel target).\n    pub owlery: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// Transient bundle/artifact scratch.\n    pub scratch: PathBuf,\n}\n\nimpl DispatchPaths {\n    /// The production layout under [`perch::spt_home`].\n    pub fn production() -> DispatchPaths {\n        let home = perch::spt_home();\n        DispatchPaths {\n            owlery: perch::owlery_dir(),\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n…\n            });\n        }\n…\n    }\n…\n}\n…\nfn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {\n…\n    let mut applied = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                match apply_notif_feed(&store, origin, &records, &policy) {\n                    Ok(verdicts) => {\n                        applied += verdicts.len();\n                        surface_fresh_rows(&store, &records, &verdicts, &self_hex);\n                    }\n                    Err(e) => return DispatchOutcome::Failed(e),\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"notif feed: {applied} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// The receiving half of the cross-node notif redirect (REQ-PRES-1): first-\n/// fire every **freshly inserted** undismissed row from an applied feed\n/// batch, under this node's own presence resolution. Best-effort and loud —\n/// a failed surface costs a boundary-resurface delay, never the feed.\n// [impl->REQ-PRES-1]\nfn surface_fresh_rows(\n    store: &NotifStore,\n    records: &[spt_net::net::notif::NotifRecord],\n    verdicts: &[crate::notifsync::NotifApplyVerdict],\n    self_hex: &str,\n) {\n    use crate::notifsync::NotifApplyVerdict;\n    use spt_store::notif::MergeOutcome;\n    let fresh = records.iter().zip(verdicts).filter_map(|(rec, v)| {\n        let spt_net::net::notif::NotifRecord::Row { row } = rec;\n        (matches!(v, NotifApplyVerdict::Applied(MergeOutcome::Inserted)) && !row.dismissed)\n            .then_some(row)\n    });\n    let policy = crate::notif::NotifSurfacePolicy::load();\n    let regs = crate::presence::load_registry_snapshots(&crate::presence::registry_snapshot_dir());\n    let owlery = perch::owlery_dir();\n    for row in fresh {\n        match crate::notif::first_fire_at(store, row, &policy, &owlery, self_hex, &regs, now_ms()) {\n            Ok(_) => {}\n            Err(e) => eprintln!(\"NOTIF_FEED_SURFACE_FAIL:{}: {e}\", row.notif_id),\n        }\n    }\n}\n\n/// WAN message arm: decode `WanMessage` lines, funnel each through the\n/// access-gate + op-id-dedup path, until the peer finishes.\n// [impl->REQ-NET-1]\nfn serve_wan_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = WanDecoder::new();\n    let mut handled = 0usize;\n    let mut replied = false;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                for msg in decoder.push(&bytes) {\n                    let outcome = receive_wan(&msg, origin, &paths.owlery, registry);\n                    handled += 1;\n                    // #9/#10 (REQ-WAN-SEND-DELIVERY): reply the outcome so the\n                    // sender reports an HONEST SENT (was: outcome discarded, sender\n                    // optimistically printed SENT(WAN) the moment bytes left its\n                    // broker). `spt send` is one message per stream, so the reply\n                    // finishes the stream; a batch feed only confirms its first\n                    // record (older bulk feeders read no reply and are unaffected).\n                    if !replied {\n                        let reply = WanReply {\n                            outcome: outcome.token().to_string(),\n                        };\n                        let _ = brain.net_stream_send(\n                            stream_id,\n                            &reply.encode_line(),\n                            None,\n                            true,\n                        );\n                        replied = true;\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Spawn the dispatcher on its own thread against the canonical broker\n/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\npub fn spawn_dispatcher(\n    broker_name: String,\n    registry: Arc<RegistryHost>,\n    stop: Arc<AtomicBool>,\n) -> thread::JoinHandle<()> {\n    thread::spawn(move || {\n        let paths = DispatchPaths::production();\n        if let Err(e) =\n            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n        {\n            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n\n    // [unit->REQ-NET-1] the demux table: every family's first record\n    // classifies correctly, including the `request` kind collision.\n    #[test]\n    fn classifies_every_family_by_first_line_shape() {\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! Inbound net-stream dispatch (D9-1) — the daemon-side acceptor that turns\n//! the hand-wired serve loops of D5–D8 into production behavior.\n//!\n//! Every E2E test so far drove the serve half manually (a probe brain calling\n//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against\n//! a known stream). In production nothing did — a peer could dial and write\n//! and the bytes sat in the broker's stream log unread. This module is the\n//! missing consumer: a daemon-hosted loop that watches the broker's stream\n//! table for **peer-initiated** streams, classifies each by its first NDJSON\n//! record, and hands it to the right serve loop on its own worker thread.\n//!\n//! ## Direction (the gotcha-#9 fact, now first-class)\n//!\n//! The stream table holds locally-opened outbound streams too. The dispatcher\n//! claims only rows with `initiated_locally == false` — serving our own\n//! requester stream would deadlock the requester against ourselves. The flag\n//! is broker-recorded at registration (`open_stream` vs the accept loop),\n//! never inferred.\n//!\n//! ## Classification (first-record demux)\n//!\n//! Streams are single-purpose; the opener's first record names the protocol.\n//! The kind-tagged families collide on variant names (`request` is both\n//! sync's and attach's opener; `chunk`/`done`/`err` recur everywhere), so the\n//! demux keys on the first line's **shape**, not its kind alone:\n//!\n//! | first line | family |\n//! |---|---|\n//! | `kind=request` + `sync_id` | sync pull ([`crate::sync::serve_sync`]) |\n//! | `kind=request` + `session_id` | remote-drive attach (D9-1-3) |\n//! | `kind=request` + `endpoint`+`event` | remote rest op ([`crate::resthost::serve_rest`]) |\n//! | `kind=request` + `owner`+`shell_ref` | cross-node shell link ([`crate::linkhost::serve_shell_link`]) |\n//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\n//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n//! | anything else | unknown — dropped, loud |\n//!\n//! A family whose production wiring lands in a later slice is still\n//! classified here (the table is complete) but routed to a typed\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n//!\n//! ## Worker model\n//!\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\n//! connection (the broker is multi-client by design; the probe-brain pattern\n//! every E2E test uses). The peek and the serve use **separate** connections:\n//! the peek brain subscribes from 0 to read the first line and is dropped;\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n//! stream's event feed alone (no double-subscription interleave). Origin is\n//! the table's `remote_id_hex` — handshake-proven, never payload\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n// [impl->REQ-NET-1]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n\nuse std::collections::HashMap;\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::notif::NotifDecoder;\nuse spt_net::net::replicate::{\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n};\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\nuse spt_store::contextstore::ContextStore;\nuse spt_store::notif::NotifStore;\nuse spt_store::perch;\nuse spt_store::roster::RosterStore;\n\nuse crate::attach::serve_attach;\nuse crate::brain::{Brain, BrokerEvent};\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\nuse crate::propagate::serve_update;\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\nuse crate::relcache::ReleaseCache;\nuse crate::sync::{serve_sync, SyncPolicy};\nuse crate::wan::receive_wan;\nuse crate::xfer::serve_xfer;\n\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n/// workers block on their stream's event feed).\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum StreamFamily {\n    /// A context sync pull (`SyncRecord::Request`).\n    Sync,\n    /// An update pull (`UpdRecord::Query`).\n    Update,\n    /// A notif replication feed (`NotifRecord::Row`).\n    Notif,\n    /// A WAN message feed (untagged `WanMessage` lines).\n    WanMsg,\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\n    Registry,\n    /// A remote-drive attach (`AttachRecord::Request`).\n    Attach,\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\n    Xfer,\n    /// A remote rest op (`RestRecord::Request` — suspend/wake from another\n    /// node, M5-D5b).\n    Rest,\n    /// A cross-node shell-link op (`ShellLinkRecord::Request` — relink/cmd\n    /// against an owner's shell on this node, M5-D8c).\n    ShellLink,\n    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — \"serving\n    /// subnet X?\", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).\n    ServeProbe,\n    /// Unclassifiable — dropped.\n    Unknown,\n}\n\n/// Classify one decoded first line by shape (module-docs demux table).\npub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {\n    match line.get(\"kind\").and_then(|k| k.as_str()) {\n        Some(\"request\") if line.get(\"sync_id\").is_some() => StreamFamily::Sync,\n        Some(\"request\") if line.get(\"session_id\").is_some() => StreamFamily::Attach,\n        Some(\"request\") if line.get(\"endpoint\").is_some() && line.get(\"event\").is_some() => {\n            StreamFamily::Rest\n        }\n        Some(\"request\") if line.get(\"owner\").is_some() && line.get(\"shell_ref\").is_some() => {\n            StreamFamily::ShellLink\n        }\n        Some(\"request\") if line.get(\"serve_probe\").is_some() => StreamFamily::ServeProbe,\n        Some(\"query\") => StreamFamily::Update,\n        // The status-only convergence query rides the update family (M8\n        // decision 19, REQ-UPD-6) — same serve loop, no artifact bytes.\n        Some(\"status_query\") => StreamFamily::Update,\n        Some(\"row\") => StreamFamily::Notif,\n        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n        Some(_) => StreamFamily::Unknown,\n        None => {\n            if line.get(\"target\").is_some() && line.get(\"op_id\").is_some() {\n                StreamFamily::WanMsg\n            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\n                StreamFamily::Registry\n            } else if line.get(\"subnet\").is_some()\n                && line.get(\"node\").is_some()\n                && line.get(\"label\").is_some()\n            {\n                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\n                // sends label records first (no instance rows), so the feed's\n                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n                StreamFamily::Registry\n            } else {\n                StreamFamily::Unknown\n            }\n        }\n    }\n}\n\n/// Why a dispatched stream's worker ended — loud + typed, never silent.\n#[derive(Debug)]\npub enum DispatchOutcome {\n    /// The family's serve loop ran to its own outcome (rendered for the log).\n    Served(String),\n    /// The first line never arrived / didn't parse — dropped.\n    Unknown,\n    /// The worker failed (IO against broker or store).\n    Failed(String),\n}\n\n/// A claimed stream's dispatch lifecycle (ADR-0038 C,\n/// REQ-DISPATCH-CLAIM-RETRY). The pre-fix `claimed` was a one-shot `HashSet`\n/// insert performed BEFORE worker spawn and never cleared — one transient\n/// worker-setup failure permanently abandoned the stream. Claims now\n/// distinguish in-flight vs terminal vs retryable, and a transient failure\n/// requeues bounded (backoff-shaped, never a hot loop).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ClaimState {\n    /// A worker owns the stream right now (`attempts` = prior failures).\n    InFlight { attempts: u32 },\n    /// Done for this dispatcher's lifetime: served, unclassifiable, or\n    /// retries exhausted. Terminal outcomes never re-enter the queue.\n    Terminal,\n    /// A transient failure released the claim; eligible again at `next_at`.\n    Retry { attempts: u32, next_at: Instant },\n}\n\n/// Bounded retry budget per stream per dispatcher lifetime — transient setup\n/// failures recover, a persistently-failing stream goes terminal loud.\nconst MAX_DISPATCH_ATTEMPTS: u32 = 3;\n\n/// Backoff base for a released claim (doubles per failed attempt).\nconst DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n\n/// The backoff before retry number `attempts` (1-based) runs.\nfn retry_backoff(attempts: u32) -> Duration {\n    DISPATCH_RETRY_BASE * 2u32.saturating_pow(attempts.saturating_sub(1))\n}\n\n/// Why a worker failed — the retry CLASSIFICATION (ADR-0038 Amendment fix 5,\n/// REQ-DISPATCH-FALLBACK-CIRCUIT). The two classes retry differently:\n/// a pre-setup transient keeps the bounded ×3 budget (the D3 semantics);\n/// a seat-blocked failure (poisoned replay / prior subscriber still draining)\n/// trips the GLOBAL circuit breaker and requeues WITHOUT burning budget —\n/// the stream must recover once the wedged seat's writer dies (≤ the 15s\n/// bounded-write deadline), and three fast retries would burn to Terminal\n/// before that (the v0.33 abandonment this explicitly must not regress to).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum FailClass {\n    /// Ordinary transient (broker IPC hiccup, opener not pinned yet, …).\n    Transient,\n    /// The stream's subscriber seat is poisoned or still draining — retrying\n    /// immediately would manufacture another replay victim.\n    SeatBlocked,\n}\n\n/// Classify a worker failure message (the seat-blocked shapes are the two\n/// strings the nethost subscriber discipline emits).\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn classify_failure(msg: &str) -> FailClass {\n    if msg.contains(\"subscriber busy\")\n        || msg.contains(\"replay halted\")\n        || msg.contains(\"lease canceled\")\n    {\n        FailClass::SeatBlocked\n    } else {\n        FailClass::Transient\n    }\n}\n\n/// Whether an opener-query error is the OLD broker's unknown-verb answer —\n/// the ONLY error class that may fall back to the ring peek (ADR-0038\n/// Amendment fix 1). `send_error` on an unknown kind replies exactly\n/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n/// wire fact. Everything else (transport timeout, EOF, poison) is a\n/// transient — Failed → bounded requeue, never a second replay subscriber.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn is_unsupported_verb(e: &io::Error) -> bool {\n    e.to_string().contains(\"unknown command kind\")\n}\n\n/// The dispatcher's GLOBAL circuit breaker (ADR-0038 Amendment fix 5): a\n/// seat-blocked failure trips it — claiming pauses dispatcher-wide for a\n/// doubling window (base 2s, cap 30s), so retry machinery cannot hammer a\n/// wedged conn with fresh replay subscribers. Any successful serve resets it.\n/// PURE (instant-in, decision-out) — unit-tested trip/gate/reset.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n#[derive(Debug, Default)]\nstruct CircuitBreaker {\n    until: Option<Instant>,\n    consecutive: u32,\n}\n\nconst BREAKER_BASE: Duration = Duration::from_secs(2);\nconst BREAKER_CAP: Duration = Duration::from_secs(30);\n\nimpl CircuitBreaker {\n    /// A seat-blocked failure landed: open (or extend) the window.\n    fn trip(&mut self, now: Instant) -> Duration {\n        let window = (BREAKER_BASE * 2u32.saturating_pow(self.consecutive)).min(BREAKER_CAP);\n        self.consecutive = self.consecutive.saturating_add(1);\n        self.until = Some(now + window);\n…\n    }\n…\n}\n…\n/// The SPT_HOME-anchored paths the serve arms need. One value, cloned per\n/// worker (policies/stores re-load from the canonical paths per request —\n/// the access-gate stance, config edits take effect without a restart).\n#[derive(Debug, Clone)]\npub struct DispatchPaths {\n    /// The perch tree (WAN message funnel target).\n    pub owlery: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// Transient bundle/artifact scratch.\n    pub scratch: PathBuf,\n}\n\nimpl DispatchPaths {\n    /// The production layout under [`perch::spt_home`].\n    pub fn production() -> DispatchPaths {\n        let home = perch::spt_home();\n        DispatchPaths {\n            owlery: perch::owlery_dir(),\n            releases: home.join(\"releases\"),\n            scratch: home.join(\"scratch\").join(\"net\"),\n        }\n    }\n}\n\n/// Watch the broker's stream table and dispatch every **peer-initiated**\n/// stream to a family worker, until `stop` is set. Blocks the calling thread\n/// (the daemon spawns it on its own); workers detach (each ends with its\n/// stream).\npub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n…\n            });\n        }\n…\n    }\n…\n}\n…\nfn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {\n…\n    let mut applied = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                match apply_notif_feed(&store, origin, &records, &policy) {\n                    Ok(verdicts) => {\n                        applied += verdicts.len();\n                        surface_fresh_rows(&store, &records, &verdicts, &self_hex);\n                    }\n                    Err(e) => return DispatchOutcome::Failed(e),\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"notif feed: {applied} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// The receiving half of the cross-node notif redirect (REQ-PRES-1): first-\n/// fire every **freshly inserted** undismissed row from an applied feed\n/// batch, under this node's own presence resolution. Best-effort and loud —\n/// a failed surface costs a boundary-resurface delay, never the feed.\n// [impl->REQ-PRES-1]\nfn surface_fresh_rows(\n    store: &NotifStore,\n    records: &[spt_net::net::notif::NotifRecord],\n    verdicts: &[crate::notifsync::NotifApplyVerdict],\n    self_hex: &str,\n) {\n    use crate::notifsync::NotifApplyVerdict;\n    use spt_store::notif::MergeOutcome;\n    let fresh = records.iter().zip(verdicts).filter_map(|(rec, v)| {\n        let spt_net::net::notif::NotifRecord::Row { row } = rec;\n        (matches!(v, NotifApplyVerdict::Applied(MergeOutcome::Inserted)) && !row.dismissed)\n            .then_some(row)\n    });\n    let policy = crate::notif::NotifSurfacePolicy::load();\n    let regs = crate::presence::load_registry_snapshots(&crate::presence::registry_snapshot_dir());\n    let owlery = perch::owlery_dir();\n    for row in fresh {\n        match crate::notif::first_fire_at(store, row, &policy, &owlery, self_hex, &regs, now_ms()) {\n            Ok(_) => {}\n            Err(e) => eprintln!(\"NOTIF_FEED_SURFACE_FAIL:{}: {e}\", row.notif_id),\n        }\n    }\n}\n\n/// WAN message arm: decode `WanMessage` lines, funnel each through the\n/// access-gate + op-id-dedup path, until the peer finishes.\n// [impl->REQ-NET-1]\nfn serve_wan_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = WanDecoder::new();\n    let mut handled = 0usize;\n    let mut replied = false;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                for msg in decoder.push(&bytes) {\n                    let outcome = receive_wan(&msg, origin, &paths.owlery, registry);\n                    handled += 1;\n                    // #9/#10 (REQ-WAN-SEND-DELIVERY): reply the outcome so the\n                    // sender reports an HONEST SENT (was: outcome discarded, sender\n                    // optimistically printed SENT(WAN) the moment bytes left its\n                    // broker). `spt send` is one message per stream, so the reply\n                    // finishes the stream; a batch feed only confirms its first\n                    // record (older bulk feeders read no reply and are unaffected).\n                    if !replied {\n                        let reply = WanReply {\n                            outcome: outcome.token().to_string(),\n                        };\n                        let _ = brain.net_stream_send(\n                            stream_id,\n                            &reply.encode_line(),\n                            None,\n                            true,\n                        );\n                        replied = true;\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Spawn the dispatcher on its own thread against the canonical broker\n/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\npub fn spawn_dispatcher(\n    broker_name: String,\n    registry: Arc<RegistryHost>,\n    stop: Arc<AtomicBool>,\n) -> thread::JoinHandle<()> {\n    thread::spawn(move || {\n        let paths = DispatchPaths::production();\n        if let Err(e) =\n            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n        {\n            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n\n    // [unit->REQ-NET-1] the demux table: every family's first record\n    // classifies correctly, including the `request` kind collision.\n    #[test]\n    fn classifies_every_family_by_first_line_shape() {\n…\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,null,262,null,274,null,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,null,575,576,null,578,null,580,null,1063,null,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,null,1250,null,1557]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784271471834}}
{"type":"message","id":"84d3f546","parentId":"07e4a128","timestamp":"2026-07-17T06:57:51.834Z","message":{"role":"toolResult","toolCallId":"call_7DvDECbRcCx85yVpG97Mxf3w|fc_038f28d3e93bbfd4016a59d26fb9508196a496c0707e0e21a8","toolName":"read","content":[{"type":"text","text":"//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) — re-host the\n//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.\n//!\n//! This is the production caller `run_pulse_loop` lacked: the brain instantiates\n//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n//! disk): same shape — enumerate Self perches, resolve each manifest from its\n//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run\n//! one bounded reconcile sweep at boot and every tick. Where the shell waker\n//! hosts **offline** shells, this hosts **online** live endpoints — `status` is\n//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the\n//! online ones re-hosts exactly what was live before a brain (re)start, and\n//! never resurrects a gracefully-departed (offline) agent.\n//!\n//! **Online-only is staged behind the interim.** The interim `spt api listen`\n//! process still first-hosts its live agents today; those perches are\n//! `status`-ABSENT (not \"online\"), so this scan ignores them — no double-host in\n//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the\n//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this\n//! reconcile reacts within one tick, the same way the shell waker reacts to a\n//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is\n//! proven through the real brain process by the W0.4 production int.\n// [impl->REQ-DAEMON-1]\n\nuse std::collections::{BTreeSet, HashMap};\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse spt_runtime::registry::AdapterRecord;\nuse spt_runtime::Manifest;\nuse spt_store::liveness::{STATUS_ONLINE, STATUS_UNBOUND};\nuse spt_store::perch::{self, ParentHint};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::brainproc::StartReason;\nuse crate::config::DaemonConfig;\nuse crate::lifecycle::BrainLifecycle;\n\n/// The `InfoJson::state` tag of a live agent — only these get a daemon-hosted\n/// Psyche; a `ready_agent` (messaging-only) never does.\nconst LIVE_AGENT_STATE: &str = \"live_agent\";\n\n/// The reconcile cadence (ms): a lifecycle flip lands in a CLI process, so the\n/// daemon loop is the invariant holder — a flip is at most one tick stale\n/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).\npub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;\n\n/// One brain-hosted live lifecycle: its config-paced pulse-driver thread and the\n/// held stop flag (tripped to un-host on an offline-transition). F-030 W3: the\n/// per-event psyche driver spawns NO resident child, so this holds no psyche\n/// handle and no residency anchor — a psyche is a bounded per-event turn, never a\n/// process to own/reap (the resident residency/reap machinery retired here).\nstruct HostedLife {\n    stop: Arc<AtomicBool>,\n    thread: JoinHandle<()>,\n}\n\n/// The brain's hosted live-endpoint table, keyed by endpoint id. Shared between\n/// the reconcile loop and anything that must stop a host. Mirror of\n/// [`crate::shellwake::WakeSet`].\n#[derive(Default)]\npub struct LiveSet {\n    inner: Mutex<HashMap<String, HostedLife>>,\n}\n\nimpl LiveSet {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// How many lifecycles are hosted (finished driver threads pruned first).\n    pub fn len(&self) -> usize {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    /// Is `id` currently hosted (driver thread alive)? Prunes a finished entry so\n    /// a crashed driver is re-hosted next sweep.\n    fn contains(&self, id: &str) -> bool {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        match map.get(id) {\n            Some(h) if !h.thread.is_finished() => true,\n            Some(_) => {\n                map.remove(id);\n                false\n            }\n            None => false,\n        }\n    }\n\n    fn insert(&self, id: &str, handle: HostedLife) {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.insert(id.to_string(), handle);\n    }\n\n    /// The currently-tabled ids (finished pruned) — the stop-side reconcile input.\n    fn ids(&self) -> Vec<String> {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.keys().cloned().collect()\n    }\n\n    /// Un-host one lifecycle: trip its stop flag, join the driver, de-table. The\n    /// pulse loop checks the flag each sleep-slice, so the join is prompt. We do\n    /// NOT re-stamp `offline` — the perch already left the online set (graceful\n    /// signoff cleared it, or the perch is gone); a brain *crash* deliberately\n    /// leaves `status=online` so the next brain rehydrates it (CONTEXT:194).\n    fn stop_host(&self, id: &str) {\n        let handle = {\n            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n            map.remove(id)\n        };\n        if let Some(h) = handle {\n            h.stop.store(true, Ordering::SeqCst);\n            let _ = h.thread.join();\n            // F-030 W3: nothing detached to reap — the per-event psyche driver spawns\n            // no resident child, so joining the driver thread + de-tabling above is the\n            // whole un-host (the resident-psyche handle-reap + orphan-reap retired with\n            // the residency machinery). Un-hosting a genuinely-offline endpoint is legit.\n        }\n    }\n}\n\n/// One reconcile sweep — the brain's live host-set invariant holder, run at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the\n/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):\n///\n/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a\n///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).\n/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.\n/// - **leave-alone** (online ∧ tabled).\n///\n/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are\n/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.\n// [impl->REQ-DAEMON-1]\npub fn reconcile_once(\n    owlery: &Path,\n    registered: &[(AdapterRecord, Manifest)],\n    adapters_dir: &Path,\n    set: &Arc<LiveSet>,\n    cfg: &DaemonConfig,\n    reason: StartReason,\n) {\n    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n    // on a TRANSIENT read failure (M11-W0.4 (ii)): a momentarily-unreadable\n    // info.json must not stop+respawn the driver (Psyche churn on a disk blip). The\n    // discriminator is the perch DIR:\n    //   - dir gone        → torn down (teardown removed the perch) → un-host;\n    //   - dir present, read_info Some(status≠online) → signed-off/offline → un-host;\n    //   - dir present, read_info None (transient/torn read) → LEAVE the driver, retry.\n    // [impl->REQ-DAEMON-1]\n    for id in set.ids() {\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let unhost = if !perch.exists() {\n            true // the perch dir is gone — torn down\n        } else {\n            match spt_store::info::read_info(&perch) {\n                Some(info) => info.status.as_deref() != Some(STATUS_ONLINE),\n                None => false, // dir present but unreadable: transient → keep hosting\n            }\n        };\n        if unhost {\n            set.stop_host(&id);\n            continue;\n        }\n        // F-030 W3: the residency-confirm + thrash-detect + orphan-reap arms retired\n        // with the resident psyche (a psyche is now a bounded per-event turn, not a\n        // process to confirm resident / rate-limit / reap). A psyche that cannot work\n        // is surfaced by the W1 per-event turn-failure budget (`note_turn_outcome`\n        // stamps `psyche_host_error` on the PARENT — psyche fields only, NEVER the\n…\n    }\n…\n}\n…\npub fn resume_restart_orphaned_endpoints(\n…\n) {\n…\n    for id in perch::list_self_perch_ids(owlery) {\n…\n        match restart_resume_gate(\n…\n            live.contains(&id),\n            relay_present,\n            custody_pid_alive,\n        ) {\n            RestartResume::Skip => {}\n            RestartResume::RefuseLivePid => {\n                // BELT 2 breadcrumb (feeds the W4 dup-wake rig, seed #7): never re-run\n                // over a live custody process — the dup-spawn / stomped-info.json class.\n                eprintln!(\n                    \"DAEMON_RESTART_RESUME_REFUSED:{id}: custody pid still alive — refusing \\\n                     to re-run over a live process (dup guard)\"\n                );\n            }\n            RestartResume::Resume => {\n                // Resume material from the newest ledger row (session + adapter + cwd).\n                let Some(last) = spt_store::sessions::last_k(&perch, 1).into_iter().next_back()\n                else {\n                    eprintln!(\n                        \"DAEMON_RESTART_RESUME_SKIP:{id}: online spt-hosted but no ledger \\\n                         session to resume\"\n                    );\n                    continue;\n                };\n                let Some(adapter) = last.adapter.as_deref().or(info.adapter.as_deref()) else {\n                    eprintln!(\"DAEMON_RESTART_RESUME_SKIP:{id}: no adapter recorded to resume under\");\n                    continue;\n                };\n                launch_ledger_resume(\n                    &perch,\n                    &id,\n                    adapter,\n                    &last.session_id,\n                    last.cwd.as_deref(),\n                    registered,\n                    adapters_dir,\n                    \"DAEMON_RESTART\",\n                );\n            }\n        }\n    }\n}\n\n/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the\n/// daemon online-stamp makes liveness daemon-authoritative), then start the\n/// config-paced pulse driver on its own supervised thread with a held stop flag.\n/// A Psyche-spawn failure tables nothing — the next sweep retries.\n// [impl->REQ-DAEMON-1]\nfn host_one(\n    set: &Arc<LiveSet>,\n    id: &str,\n    lifecycle: BrainLifecycle,\n    session_id: String,\n    reason: StartReason,\n) {\n    lifecycle.sweep_on_start();\n    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n    // (`BrainLifecycle::run_psyche_event_turn` on each armed pulse fire), never a\n    // long-lived process — so the child never self-resolves home/subnet and the\n    // direction-(a) multi-subnet poll-refusal churn is impossible by construction.\n    // The pulse loop STAYS as the daemon-side scheduler; only what a fire DOES\n    // changed. W3 retired the resident residency/reap machinery entirely: there is no\n    // psyche handle to own, no residency anchor to stamp, nothing to reap on un-host.\n    // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n    // [impl->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]\n    let stop = Arc::new(AtomicBool::new(false));\n    let handle = {\n        let stop = Arc::clone(&stop);\n        std::thread::spawn(move || {\n            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});\n        })\n    };\n    set.insert(id, HostedLife { stop, thread: handle });\n}\n\n/// B2 KEYSTONE — PULL liveness reconcile (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE):\n/// mark OFFLINE any spt-hosted (controllable) `status=online` `live_agent` perch\n/// whose broker session is GONE — the correction for the one-way `status=online`\n/// latch (the broker exit-waiter reaps its session but never clears `info.json`).\n/// This is the LIVE-STATUS analog of the v0.11.0 roster ghost-heal\n/// (`advertise_local` → Offline): same crash-robust, self-healing-next-tick shape.\n///\n/// `live_sessions` is the set of endpoint ids the broker currently hosts a PTY\n/// session for ([`crate::msg::SessionsReply`] via `KIND_SESSIONS`). A perch that\n/// is online + controllable but ABSENT from it has a dead/exited harness → mark it\n/// offline; the existing [`reconcile_once`] stop-side then un-hosts its driver and\n/// the start-side stops re-hosting it.\n///\n/// **Controllable gate (load-bearing):** only `controllable == Some(true)`\n/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay\n/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online\n/// with NO broker session (its liveness is the relay + ready marker), so it is\n/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /\n/// dead-psyche like wall-a) is also exempt here — its liveness/phantom class is\n/// not the broker-session arm's concern. (The former residency-confirm arm that\n/// once handled it was retired with the resident-Psyche model in F-030; a\n/// genuinely stranded pre-upgrade resident is now reaped by the one-shot\n/// legacy-resident sweep at brain start.)\n///\n/// The caller passes `live_sessions` ONLY when the broker is reachable (it runs\n/// this from boot tick 1 — the B5 boot-gate — so a phantom is never revived, and\n/// SKIPS the pass entirely when the broker is unreachable so a transient hiccup\n/// never mass-offlines). Returns the ids it marked offline.\n// [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\npub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {\n    let mut offlined = Vec::new();\n    for id in perch::list_self_perch_ids(owlery) {\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let Some(info) = spt_store::info::read_info(&perch) else {\n            continue;\n        };\n        if info.state != LIVE_AGENT_STATE {\n            continue; // not a live agent\n        }\n        let has_session = live_sessions.contains(&id);\n\n        // B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT): control stamps must not outlive the\n        // broker session that owns them. A spt-hosted (controllable Some(true)) live\n        // agent with NO live broker session must carry NEITHER `controlled` NOR\n        // `driven_by` — reaped here REGARDLESS of status so a perch that is ALREADY\n        // OFFLINE yet still reads controlled=true / driven_by=Some (the sticky stamp\n        // that survived clean-exit AND a daemon restart — perri F-b, controlled >20min\n        // + across a bounce) is swept, not only on the online->offline transition\n        // below. `attached_node` derives from these two honest stamps, so clearing\n        // both clears it. RACE-FREE: with no live session there is no controller to\n        // re-stamp concurrently (the broker single-writer invariant is uncontended —\n        // the same argument as the Gap-B driven_by self-heal). A harness-hosted relay\n        // (controllable Some(false)) never carries these stamps (no broker PTY to\n        // drive), so the controllable gate is exact. This is also the BOOT-TIME sweep\n        // (path iv): the caller runs the reconcile from boot tick 1.\n        // [impl->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n        if !has_session\n            && info.controllable == Some(true)\n            && (info.controlled || info.driven_by.is_some())\n…\n        }\n…\n    }\n…\n}\n…\n/// `.live-bin` per pre-W5 endpoint forever. Runs ONCE at brain start AFTER\n/// [`sweep_legacy_resident_psyches`] (which kills any wrapper running FROM the image →\n/// the dir then unlocks). A still-image-locked dir → F-028 C1 DISPLACE (a locked exe\n/// can be MOVED even when it can't be unlinked): rename the dir aside to a fresh\n/// `.live-bin.old-N` sibling + best-effort remove; a still-locked displaced dir stays\n/// uniquely-named for a later boot's GC. NEVER delete-fail-abort. Also sweeps stale\n/// `.live-bin.old*` litter a prior locked displace left behind.\n// [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\npub fn gc_live_bin_dirs(owlery: &Path) {\n    gc_live_bin_dirs_with(owlery, &|d| std::fs::remove_dir_all(d));\n}\n\n/// [`gc_live_bin_dirs`] over an injected `remove` (the crc_swap injectable-seam\n/// pattern) so the image-locked DISPLACE path is unit-testable deterministically\n/// without a real OS lock: a unit feeds a `remove` that fails for `.live-bin` and\n/// asserts the dir is DISPLACED (renamed aside), never errored/aborted.\nfn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\n    for id in perch::list_self_perch_ids(owlery) {\n        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n        let Ok(entries) = std::fs::read_dir(&perch) else {\n            continue;\n        };\n        for entry in entries.flatten() {\n            let name = entry.file_name();\n            let name = name.to_string_lossy();\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\n                continue;\n            }\n            let dir = entry.path();\n            if !dir.is_dir() {\n                continue;\n            }\n            if remove(&dir).is_ok() {\n                continue;\n            }\n            // Still image-locked. An already-displaced `.old*` just waits for the next\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\n            // perch no longer carries the stale own-copy in place. NEVER abort.\n            if name == \".live-bin\" {\n                let displaced = fresh_live_bin_old(&dir);\n                match std::fs::rename(&dir, &displaced) {\n                    Ok(()) => {\n                        let _ = remove(&displaced);\n                    }\n                    Err(e) => eprintln!(\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\n                    ),\n                }\n            }\n        }\n    }\n}\n\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n    let base = live_bin.with_file_name(\".live-bin.old\");\n    if !base.exists() {\n        return base;\n    }\n    for n in 1u64.. {\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\n        if !candidate.exists() {\n            return candidate;\n        }\n    }\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\n}\n\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n/// config are re-read each sweep (adapter / pulse-period changes land between\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n/// for symmetry — the threads die with the brain process and the next brain\n/// rehydrates from disk.\n// [impl->REQ-DAEMON-1]\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(LiveSet::new());\n        {\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\n            // residency machinery's corpse-burial, not its resurrection.\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\n            // running FROM the own-copy image is killed first (its dir then unlocks).\n            gc_live_bin_dirs(&owlery);\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            let cfg = DaemonConfig::load();\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\n            // latch for any spt-hosted endpoint whose broker session is gone — and\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\n            // so the same-tick stop-side un-hosts the now-offline perch and the\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\n            // cold start after an unclean stop from spawning N phantom Psyches for N\n            // dead-but-latched perches. Skipped only when the broker is unreachable\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\n            // run_brain connects to the broker BEFORE this host spawns, so the query\n            // returns the REAL session set: empty at a cold start (every online\n            // controllable perch is a phantom → offlined), the surviving sessions at\n            // a brain restart (live agents stay, only the dead are offlined). The\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\n            if let Some(live) = query_live_session_endpoints() {\n                reconcile_hosted_liveness(&owlery, &live);\n            }\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n…\n            }\n        }\n    })\n}"}],"details":{"displayContent":{"text":"//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) — re-host the\n//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.\n//!\n//! This is the production caller `run_pulse_loop` lacked: the brain instantiates\n//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n//! disk): same shape — enumerate Self perches, resolve each manifest from its\n//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run\n//! one bounded reconcile sweep at boot and every tick. Where the shell waker\n//! hosts **offline** shells, this hosts **online** live endpoints — `status` is\n//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the\n//! online ones re-hosts exactly what was live before a brain (re)start, and\n//! never resurrects a gracefully-departed (offline) agent.\n//!\n//! **Online-only is staged behind the interim.** The interim `spt api listen`\n//! process still first-hosts its live agents today; those perches are\n//! `status`-ABSENT (not \"online\"), so this scan ignores them — no double-host in\n//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the\n//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this\n//! reconcile reacts within one tick, the same way the shell waker reacts to a\n//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is\n//! proven through the real brain process by the W0.4 production int.\n// [impl->REQ-DAEMON-1]\n\nuse std::collections::{BTreeSet, HashMap};\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse spt_runtime::registry::AdapterRecord;\nuse spt_runtime::Manifest;\nuse spt_store::liveness::{STATUS_ONLINE, STATUS_UNBOUND};\nuse spt_store::perch::{self, ParentHint};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::brainproc::StartReason;\nuse crate::config::DaemonConfig;\nuse crate::lifecycle::BrainLifecycle;\n\n/// The `InfoJson::state` tag of a live agent — only these get a daemon-hosted\n/// Psyche; a `ready_agent` (messaging-only) never does.\nconst LIVE_AGENT_STATE: &str = \"live_agent\";\n\n/// The reconcile cadence (ms): a lifecycle flip lands in a CLI process, so the\n/// daemon loop is the invariant holder — a flip is at most one tick stale\n/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).\npub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;\n\n/// One brain-hosted live lifecycle: its config-paced pulse-driver thread and the\n/// held stop flag (tripped to un-host on an offline-transition). F-030 W3: the\n/// per-event psyche driver spawns NO resident child, so this holds no psyche\n/// handle and no residency anchor — a psyche is a bounded per-event turn, never a\n/// process to own/reap (the resident residency/reap machinery retired here).\nstruct HostedLife {\n    stop: Arc<AtomicBool>,\n    thread: JoinHandle<()>,\n}\n\n/// The brain's hosted live-endpoint table, keyed by endpoint id. Shared between\n/// the reconcile loop and anything that must stop a host. Mirror of\n/// [`crate::shellwake::WakeSet`].\n#[derive(Default)]\npub struct LiveSet {\n    inner: Mutex<HashMap<String, HostedLife>>,\n}\n\nimpl LiveSet {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// How many lifecycles are hosted (finished driver threads pruned first).\n    pub fn len(&self) -> usize {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    /// Is `id` currently hosted (driver thread alive)? Prunes a finished entry so\n    /// a crashed driver is re-hosted next sweep.\n    fn contains(&self, id: &str) -> bool {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        match map.get(id) {\n            Some(h) if !h.thread.is_finished() => true,\n            Some(_) => {\n                map.remove(id);\n                false\n            }\n            None => false,\n        }\n    }\n\n    fn insert(&self, id: &str, handle: HostedLife) {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.insert(id.to_string(), handle);\n    }\n\n    /// The currently-tabled ids (finished pruned) — the stop-side reconcile input.\n    fn ids(&self) -> Vec<String> {\n        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n        map.retain(|_, h| !h.thread.is_finished());\n        map.keys().cloned().collect()\n    }\n\n    /// Un-host one lifecycle: trip its stop flag, join the driver, de-table. The\n    /// pulse loop checks the flag each sleep-slice, so the join is prompt. We do\n    /// NOT re-stamp `offline` — the perch already left the online set (graceful\n    /// signoff cleared it, or the perch is gone); a brain *crash* deliberately\n    /// leaves `status=online` so the next brain rehydrates it (CONTEXT:194).\n    fn stop_host(&self, id: &str) {\n        let handle = {\n            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());\n            map.remove(id)\n        };\n        if let Some(h) = handle {\n            h.stop.store(true, Ordering::SeqCst);\n            let _ = h.thread.join();\n            // F-030 W3: nothing detached to reap — the per-event psyche driver spawns\n            // no resident child, so joining the driver thread + de-tabling above is the\n            // whole un-host (the resident-psyche handle-reap + orphan-reap retired with\n            // the residency machinery). Un-hosting a genuinely-offline endpoint is legit.\n        }\n    }\n}\n\n/// One reconcile sweep — the brain's live host-set invariant holder, run at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the\n/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):\n///\n/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a\n///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).\n/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.\n/// - **leave-alone** (online ∧ tabled).\n///\n/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are\n/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.\n// [impl->REQ-DAEMON-1]\npub fn reconcile_once(\n    owlery: &Path,\n    registered: &[(AdapterRecord, Manifest)],\n    adapters_dir: &Path,\n    set: &Arc<LiveSet>,\n    cfg: &DaemonConfig,\n    reason: StartReason,\n) {\n    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n    // on a TRANSIENT read failure (M11-W0.4 (ii)): a momentarily-unreadable\n    // info.json must not stop+respawn the driver (Psyche churn on a disk blip). The\n    // discriminator is the perch DIR:\n    //   - dir gone        → torn down (teardown removed the perch) → un-host;\n    //   - dir present, read_info Some(status≠online) → signed-off/offline → un-host;\n    //   - dir present, read_info None (transient/torn read) → LEAVE the driver, retry.\n    // [impl->REQ-DAEMON-1]\n    for id in set.ids() {\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let unhost = if !perch.exists() {\n            true // the perch dir is gone — torn down\n        } else {\n            match spt_store::info::read_info(&perch) {\n                Some(info) => info.status.as_deref() != Some(STATUS_ONLINE),\n                None => false, // dir present but unreadable: transient → keep hosting\n            }\n        };\n        if unhost {\n            set.stop_host(&id);\n            continue;\n        }\n        // F-030 W3: the residency-confirm + thrash-detect + orphan-reap arms retired\n        // with the resident psyche (a psyche is now a bounded per-event turn, not a\n        // process to confirm resident / rate-limit / reap). A psyche that cannot work\n        // is surfaced by the W1 per-event turn-failure budget (`note_turn_outcome`\n        // stamps `psyche_host_error` on the PARENT — psyche fields only, NEVER the\n…\n    }\n…\n}\n…\npub fn resume_restart_orphaned_endpoints(\n…\n) {\n…\n    for id in perch::list_self_perch_ids(owlery) {\n…\n        match restart_resume_gate(\n…\n            live.contains(&id),\n            relay_present,\n            custody_pid_alive,\n        ) {\n            RestartResume::Skip => {}\n            RestartResume::RefuseLivePid => {\n                // BELT 2 breadcrumb (feeds the W4 dup-wake rig, seed #7): never re-run\n                // over a live custody process — the dup-spawn / stomped-info.json class.\n                eprintln!(\n                    \"DAEMON_RESTART_RESUME_REFUSED:{id}: custody pid still alive — refusing \\\n                     to re-run over a live process (dup guard)\"\n                );\n            }\n            RestartResume::Resume => {\n                // Resume material from the newest ledger row (session + adapter + cwd).\n                let Some(last) = spt_store::sessions::last_k(&perch, 1).into_iter().next_back()\n                else {\n                    eprintln!(\n                        \"DAEMON_RESTART_RESUME_SKIP:{id}: online spt-hosted but no ledger \\\n                         session to resume\"\n                    );\n                    continue;\n                };\n                let Some(adapter) = last.adapter.as_deref().or(info.adapter.as_deref()) else {\n                    eprintln!(\"DAEMON_RESTART_RESUME_SKIP:{id}: no adapter recorded to resume under\");\n                    continue;\n                };\n                launch_ledger_resume(\n                    &perch,\n                    &id,\n                    adapter,\n                    &last.session_id,\n                    last.cwd.as_deref(),\n                    registered,\n                    adapters_dir,\n                    \"DAEMON_RESTART\",\n                );\n            }\n        }\n    }\n}\n\n/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the\n/// daemon online-stamp makes liveness daemon-authoritative), then start the\n/// config-paced pulse driver on its own supervised thread with a held stop flag.\n/// A Psyche-spawn failure tables nothing — the next sweep retries.\n// [impl->REQ-DAEMON-1]\nfn host_one(\n    set: &Arc<LiveSet>,\n    id: &str,\n    lifecycle: BrainLifecycle,\n    session_id: String,\n    reason: StartReason,\n) {\n    lifecycle.sweep_on_start();\n    // F-030 W1+W3: NO resident Psyche is spawned. The Psyche is a PER-EVENT TURN\n    // (`BrainLifecycle::run_psyche_event_turn` on each armed pulse fire), never a\n    // long-lived process — so the child never self-resolves home/subnet and the\n    // direction-(a) multi-subnet poll-refusal churn is impossible by construction.\n    // The pulse loop STAYS as the daemon-side scheduler; only what a fire DOES\n    // changed. W3 retired the resident residency/reap machinery entirely: there is no\n    // psyche handle to own, no residency anchor to stamp, nothing to reap on un-host.\n    // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n    // [impl->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]\n    let stop = Arc::new(AtomicBool::new(false));\n    let handle = {\n        let stop = Arc::clone(&stop);\n        std::thread::spawn(move || {\n            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});\n        })\n    };\n    set.insert(id, HostedLife { stop, thread: handle });\n}\n\n/// B2 KEYSTONE — PULL liveness reconcile (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE):\n/// mark OFFLINE any spt-hosted (controllable) `status=online` `live_agent` perch\n/// whose broker session is GONE — the correction for the one-way `status=online`\n/// latch (the broker exit-waiter reaps its session but never clears `info.json`).\n/// This is the LIVE-STATUS analog of the v0.11.0 roster ghost-heal\n/// (`advertise_local` → Offline): same crash-robust, self-healing-next-tick shape.\n///\n/// `live_sessions` is the set of endpoint ids the broker currently hosts a PTY\n/// session for ([`crate::msg::SessionsReply`] via `KIND_SESSIONS`). A perch that\n/// is online + controllable but ABSENT from it has a dead/exited harness → mark it\n/// offline; the existing [`reconcile_once`] stop-side then un-hosts its driver and\n/// the start-side stops re-hosting it.\n///\n/// **Controllable gate (load-bearing):** only `controllable == Some(true)`\n/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay\n/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online\n/// with NO broker session (its liveness is the relay + ready marker), so it is\n/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /\n/// dead-psyche like wall-a) is also exempt here — its liveness/phantom class is\n/// not the broker-session arm's concern. (The former residency-confirm arm that\n/// once handled it was retired with the resident-Psyche model in F-030; a\n/// genuinely stranded pre-upgrade resident is now reaped by the one-shot\n/// legacy-resident sweep at brain start.)\n///\n/// The caller passes `live_sessions` ONLY when the broker is reachable (it runs\n/// this from boot tick 1 — the B5 boot-gate — so a phantom is never revived, and\n/// SKIPS the pass entirely when the broker is unreachable so a transient hiccup\n/// never mass-offlines). Returns the ids it marked offline.\n// [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\npub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {\n    let mut offlined = Vec::new();\n    for id in perch::list_self_perch_ids(owlery) {\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let Some(info) = spt_store::info::read_info(&perch) else {\n            continue;\n        };\n        if info.state != LIVE_AGENT_STATE {\n            continue; // not a live agent\n        }\n        let has_session = live_sessions.contains(&id);\n\n        // B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT): control stamps must not outlive the\n        // broker session that owns them. A spt-hosted (controllable Some(true)) live\n        // agent with NO live broker session must carry NEITHER `controlled` NOR\n        // `driven_by` — reaped here REGARDLESS of status so a perch that is ALREADY\n        // OFFLINE yet still reads controlled=true / driven_by=Some (the sticky stamp\n        // that survived clean-exit AND a daemon restart — perri F-b, controlled >20min\n        // + across a bounce) is swept, not only on the online->offline transition\n        // below. `attached_node` derives from these two honest stamps, so clearing\n        // both clears it. RACE-FREE: with no live session there is no controller to\n        // re-stamp concurrently (the broker single-writer invariant is uncontended —\n        // the same argument as the Gap-B driven_by self-heal). A harness-hosted relay\n        // (controllable Some(false)) never carries these stamps (no broker PTY to\n        // drive), so the controllable gate is exact. This is also the BOOT-TIME sweep\n        // (path iv): the caller runs the reconcile from boot tick 1.\n        // [impl->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n        if !has_session\n            && info.controllable == Some(true)\n            && (info.controlled || info.driven_by.is_some())\n…\n        }\n…\n    }\n…\n}\n…\n/// `.live-bin` per pre-W5 endpoint forever. Runs ONCE at brain start AFTER\n/// [`sweep_legacy_resident_psyches`] (which kills any wrapper running FROM the image →\n/// the dir then unlocks). A still-image-locked dir → F-028 C1 DISPLACE (a locked exe\n/// can be MOVED even when it can't be unlinked): rename the dir aside to a fresh\n/// `.live-bin.old-N` sibling + best-effort remove; a still-locked displaced dir stays\n/// uniquely-named for a later boot's GC. NEVER delete-fail-abort. Also sweeps stale\n/// `.live-bin.old*` litter a prior locked displace left behind.\n// [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\npub fn gc_live_bin_dirs(owlery: &Path) {\n    gc_live_bin_dirs_with(owlery, &|d| std::fs::remove_dir_all(d));\n}\n\n/// [`gc_live_bin_dirs`] over an injected `remove` (the crc_swap injectable-seam\n/// pattern) so the image-locked DISPLACE path is unit-testable deterministically\n/// without a real OS lock: a unit feeds a `remove` that fails for `.live-bin` and\n/// asserts the dir is DISPLACED (renamed aside), never errored/aborted.\nfn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\n    for id in perch::list_self_perch_ids(owlery) {\n        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n        let Ok(entries) = std::fs::read_dir(&perch) else {\n            continue;\n        };\n        for entry in entries.flatten() {\n            let name = entry.file_name();\n            let name = name.to_string_lossy();\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\n                continue;\n            }\n            let dir = entry.path();\n            if !dir.is_dir() {\n                continue;\n            }\n            if remove(&dir).is_ok() {\n                continue;\n            }\n            // Still image-locked. An already-displaced `.old*` just waits for the next\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\n            // perch no longer carries the stale own-copy in place. NEVER abort.\n            if name == \".live-bin\" {\n                let displaced = fresh_live_bin_old(&dir);\n                match std::fs::rename(&dir, &displaced) {\n                    Ok(()) => {\n                        let _ = remove(&displaced);\n                    }\n                    Err(e) => eprintln!(\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\n                    ),\n                }\n            }\n        }\n    }\n}\n\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n    let base = live_bin.with_file_name(\".live-bin.old\");\n    if !base.exists() {\n        return base;\n    }\n    for n in 1u64.. {\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\n        if !candidate.exists() {\n            return candidate;\n        }\n    }\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\n}\n\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n/// config are re-read each sweep (adapter / pulse-period changes land between\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n/// for symmetry — the threads die with the brain process and the next brain\n/// rehydrates from disk.\n// [impl->REQ-DAEMON-1]\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(LiveSet::new());\n        {\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\n            // residency machinery's corpse-burial, not its resurrection.\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\n            // running FROM the own-copy image is killed first (its dir then unlocks).\n            gc_live_bin_dirs(&owlery);\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            let cfg = DaemonConfig::load();\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\n            // latch for any spt-hosted endpoint whose broker session is gone — and\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\n            // so the same-tick stop-side un-hosts the now-offline perch and the\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\n            // cold start after an unclean stop from spawning N phantom Psyches for N\n            // dead-but-latched perches. Skipped only when the broker is unreachable\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\n            // run_brain connects to the broker BEFORE this host spawns, so the query\n            // returns the REAL session set: empty at a cold start (every online\n            // controllable perch is a phantom → offlined), the surviving sessions at\n            // a brain restart (live agents stay, only the dead are offlined). The\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\n            if let Some(live) = query_live_session_endpoints() {\n                reconcile_hosted_liveness(&owlery, &live);\n            }\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n…\n            }\n        }\n    })\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,null,185,null,269,null,550,null,554,null,558,null,566,null,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,null,709,null,743,null,745,null,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,null,1053,1054,1055,1056]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\livehost.rs"}}},"isError":false,"timestamp":1784271471834}}
{"type":"message","id":"f9b55e17","parentId":"84d3f546","timestamp":"2026-07-17T06:57:51.854Z","message":{"role":"toolResult","toolCallId":"call_kDh8RtvEdPYKU64ukWBKAP8A|fc_038f28d3e93bbfd4016a59d26fb9408196812769e855e02d48","toolName":"read","content":[{"type":"text","text":"//! The materialized project-index WRITER — the daemon's derived-state\n//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\n//!\n//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\n//! `$SPT_HOME/index/project-index.json` (format + read path owned by\n//! `spt_store::projindex`, W1). One brain-hosted thread:\n//!\n//! - loads the persisted index at start and is READY WITHOUT WARM — the boot\n//!   reconcile runs in the background; a cold daemon never blocks on it;\n//! - reconciles with BATCHED work, `O(P + B + F + C)`: ONE branch enumeration\n//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\n//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\n//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\n//!   in a project repo touch neither the path nor `.git/config`, so they are\n//!   a NO-OP by construction — ADR-0037 §3);\n//! - publishes atomically via `projindex::write_index_at` and preserves\n//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\n//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n//!   scan;\n//! - maintains an observability sidecar (`project-index-stats.json`) — index\n//!   presence alone is not health: generated time, source generation, pending\n//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n//!   (wall-clock budgets are manual acceptance ONLY).\n// [impl->REQ-PROJECT-INDEX-WRITER]\n\nuse std::collections::{BTreeMap, BTreeSet, HashMap};\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse spt_store::branchstore::BranchStore;\nuse spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};\nuse spt_store::info;\nuse spt_store::perch;\nuse spt_store::projderive;\nuse spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\nuse spt_store::projinval::{self, Coalesced};\nuse spt_store::sessions;\n\nuse crate::brain::now_ms;\n\n/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\nconst POLL_MS: u64 = 500;\n/// Debounce window after the first observed event — a burst (an adapter\n/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\nconst DEBOUNCE_MS: u64 = 250;\n/// The low-frequency periodic FULL reconcile — the backstop that catches a\n/// lost nudge (`SPT_PROJINDEX_PERIODIC_MS` override for int rigs).\nconst PERIODIC_MS: u64 = 300_000;\n\n/// The complexity counters — the deterministic CI gate surface (gate ruling:\n/// counters gate, wall-clock is manual acceptance only).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct CycleCounters {\n    /// Branch enumerations this cycle (contract: exactly 1 when the store exists).\n    pub branch_enumerations: u64,\n    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\n    pub tree_scans: u64,\n    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\n    pub derivations: u64,\n}\n\nimpl CycleCounters {\n    fn add_to(&self, total: &mut CycleCounters) {\n        total.branch_enumerations += self.branch_enumerations;\n        total.tree_scans += self.tree_scans;\n        total.derivations += self.derivations;\n    }\n}\n\n/// The observability sidecar (`$SPT_HOME/index/project-index-stats.json`) —\n/// written atomically by the writer after every cycle; read by\n/// `spt daemon status` and by the int gates. Additive serde-default shape\n/// (derived + regenerable, like the index itself).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct IndexWriterStats {\n    /// When the current snapshot was generated (epoch ms; 0 = never).\n    #[serde(default)]\n    pub generated_ms: u64,\n    /// The published snapshot's branch-tip fingerprint.\n    #[serde(default)]\n    pub source_generation: String,\n    /// Whether a refresh is queued/underway right now.\n    #[serde(default)]\n    pub pending_refresh: bool,\n    /// When the last reconcile cycle ran (epoch ms).\n    #[serde(default)]\n    pub last_run_ms: u64,\n    /// How long it took (ms).\n    #[serde(default)]\n    pub last_duration_ms: u64,\n    /// The last cycle failure, if the most recent cycle failed (`None` = last\n    /// cycle succeeded). While set, readers are on last-known-good.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub last_error: Option<String>,\n    /// Row counts of the published snapshot.\n    #[serde(default)]\n    pub endpoints: u64,\n    #[serde(default)]\n    pub projects: u64,\n    /// Distinct cwds in the derivation cache.\n    #[serde(default)]\n    pub cwds: u64,\n    /// Cumulative cwd-cache hit/miss census.\n    #[serde(default)]\n    pub cwd_cache_hits: u64,\n    #[serde(default)]\n    pub cwd_cache_misses: u64,\n    /// Cycles that FAILED to publish — every read until the next success is a\n    /// stale (last-known-good) read by contract.\n    #[serde(default)]\n    pub stale_reads: u64,\n    /// Publishes that REPLACED an unreadable / schema-mismatched index file\n    /// (the writer repairing a torn or foreign snapshot).\n    #[serde(default)]\n    pub repairs: u64,\n    /// The most recent cycle's complexity counters (the CI-gate surface).\n    #[serde(default)]\n    pub last_cycle: CycleCounters,\n    /// Lifetime totals (this brain generation).\n    #[serde(default)]\n    pub cumulative: CycleCounters,\n}\n\n/// The stats sidecar path, beside the index inside `projindex::index_dir()`.\npub fn stats_path() -> PathBuf {\n    projindex::index_dir().join(\"project-index-stats.json\")\n}\n\n/// Read the stats sidecar (tolerant: absent/corrupt → `None`) — the\n/// `spt daemon status` consumer half.\npub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n    let raw = std::fs::read_to_string(path).ok()?;\n    serde_json::from_str(&raw).ok()\n}\n\n/// Every filesystem root the engine touches, injected so units run against\n/// plain temp dirs with zero env games.\n#[derive(Debug, Clone)]\npub struct WriterPaths {\n    pub owlery: PathBuf,\n    /// The context store's bare seed repo (`tracked/.seed.git`).\n    pub seed_git: PathBuf,\n    pub index_path: PathBuf,\n    pub stats_path: PathBuf,\n    pub invalidations_dir: PathBuf,\n}\n\nimpl WriterPaths {\n    /// The production layout under the live `$SPT_HOME`.\n    pub fn production() -> WriterPaths {\n        WriterPaths {\n            owlery: perch::owlery_dir(),\n            seed_git: contextstore::seed_git_dir(&perch::tracked_dir()),\n            index_path: projindex::index_path(),\n            stats_path: stats_path(),\n            invalidations_dir: projinval::invalidations_dir(),\n        }\n    }\n}\n\n/// One scanned branch: its tip at scan time + the endpoint ids holding\n/// `<id>/project-context.md` in that tip tree.\n#[derive(Debug, Clone)]\nstruct BranchScan {\n    tip: String,\n    members: BTreeSet<String>,\n}\n\n/// One cached cwd derivation, stamped with the repo-identity marker that\n/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n/// for a linked worktree). Identity refreshes ONLY when the marker path or\n/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\n/// §3: commits do not alter project identity).\n#[derive(Debug, Clone)]\nstruct CwdDerivation {\n    id: String,\n    display: String,\n    marker: Option<PathBuf>,\n    stamp: Option<(u64, u64)>,\n}\n\n/// What one reconcile cycle did — the unit-gate surface.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct CycleReport {\n    pub counters: CycleCounters,\n    /// A new snapshot was written.\n    pub published: bool,\n    /// The cycle proved the store unchanged and exited without scanning,\n    /// deriving, or writing — the warm-start / no-op contract.\n    pub skipped_unchanged: bool,\n}\n\n/// The single-flight reconcile engine. ONE instance lives on ONE writer\n/// thread — single-flight is by construction, not by locking.\npub struct WriterEngine {\n    paths: WriterPaths,\n    /// The last published snapshot (in-memory copy; `None` until the first\n    /// load/publish).\n    current: Option<ProjectIndex>,\n    /// The fingerprint the published snapshot was derived from.\n    last_fingerprint: String,\n    /// branch → last scan (tips + membership), the ≤1-scan-per-changed-branch\n    /// cache. Brain-local: a brain restart re-scans (bounded by B) only when\n    /// the fingerprint moved or rows must be recomposed.\n    branch_cache: BTreeMap<String, BranchScan>,\n    /// `p-*` branches in recency order as of the last enumeration:\n    /// `(project_id, branch)`.\n    branch_order: Vec<(String, String)>,\n    /// normalized cwd → stamped derivation (the C term's cross-cycle cache).\n    cwd_cache: HashMap<String, CwdDerivation>,\n    /// Pending repair: the on-disk index was unreadable/foreign at load; the\n    /// next successful publish counts as a repair.\n    repair_pending: bool,\n    pub stats: IndexWriterStats,\n}\n\nimpl WriterEngine {\n    /// Build the engine and LOAD the persisted index (ADR-0037 §2: load at\n    /// startup, ready without warm — this constructor performs pure file I/O\n    /// and returns immediately; reconciling is the thread's job).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    pub fn new(paths: WriterPaths) -> WriterEngine {\n        let (current, repair_pending) = match projindex::read_index_at(&paths.index_path) {\n            IndexRead::Snapshot(idx) => (Some(idx), false),\n            IndexRead::Absent(AbsentReason::Missing) => (None, false),\n            // Torn/corrupt/foreign file present: readers are degrading to `-`;\n            // our next publish REPAIRS it (and is counted as such).\n            IndexRead::Absent(_) => (None, true),\n        };\n        let last_fingerprint = current\n            .as_ref()\n            .map(|i| i.source_generation.clone())\n            .unwrap_or_default();\n        let mut stats = IndexWriterStats {\n            generated_ms: current.as_ref().map(|i| i.generated_ms).unwrap_or(0),\n            source_generation: last_fingerprint.clone(),\n            pending_refresh: true, // the boot reconcile is queued by definition\n            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n            ..IndexWriterStats::default()\n        };\n        stats.projects = current\n            .as_ref()\n            .map(|i| distinct_projects(i) as u64)\n            .unwrap_or(0);\n        WriterEngine {\n            paths,\n            current,\n            last_fingerprint,\n            branch_cache: BTreeMap::new(),\n            branch_order: Vec::new(),\n            cwd_cache: HashMap::new(),\n            repair_pending,\n            stats,\n        }\n    }\n\n    /// Run ONE reconcile cycle over the coalesced request. `force_full`\n    /// recomposes every endpoint even on an unchanged fingerprint (the\n    /// periodic backstop); a non-forced global request with an unchanged\n    /// fingerprint and no scoped work SKIPS everything — the warm-start\n    /// no-scan contract. Any error leaves the published index untouched\n    /// (last-known-good) and surfaces in `stats.last_error`.\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    pub fn reconcile(&mut self, req: &Coalesced, force_full: bool) -> Result<CycleReport, String> {\n        let started = std::time::Instant::now();\n        let now = now_ms();\n        let mut counters = CycleCounters::default();\n\n        // Explicit cwd invalidations (new session in a dir whose identity may\n        // have moved): drop those cache entries up front so the compose pass\n        // re-derives them exactly once.\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n        for cwd in &req.cwds {\n            self.cwd_cache.remove(&projderive::normalize_path(Path::new(cwd)));\n        }\n\n        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\n        let store = if self.paths.seed_git.exists() {\n            Some(\n                BranchStore::open_or_init(&self.paths.seed_git)\n                    .map_err(|e| format!(\"branch store open: {e}\"))?,\n            )\n        } else {\n            None // no context store yet — sessions/origin still index\n        };\n        let tips: Vec<(String, String)> = match &store {\n            Some(store) => {\n                counters.branch_enumerations += 1;\n                store\n                    .branch_tips_by_recency()\n                    .map_err(|e| format!(\"branch enumeration: {e}\"))?\n            }\n            None => Vec::new(),\n        };\n        let fingerprint = projindex::fingerprint_tips(&tips);\n        let fingerprint_changed = fingerprint != self.last_fingerprint;\n\n        // ── 2. The no-work exit: unchanged generation performs NO scan ───\n        if !fingerprint_changed\n            && !force_full\n            && req.endpoints.is_empty()\n            && req.cwds.is_empty()\n            && self.current.is_some()\n        {\n            let report = CycleReport { counters, published: false, skipped_unchanged: true };\n            self.finish_cycle(&report, now, started.elapsed());\n            return Ok(report);\n        }\n\n        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\n        self.branch_order = tips\n            .iter()\n            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\n            .collect();\n        if let (false, Some(store)) = (self.branch_order.is_empty(), &store) {\n            let live: BTreeSet<&str> =\n                self.branch_order.iter().map(|(_, b)| b.as_str()).collect();\n            self.branch_cache.retain(|b, _| live.contains(b.as_str()));\n            for (branch, tip) in tips.iter().filter(|(b, _)| b.starts_with(\"p-\")) {\n                let cached = self.branch_cache.get(branch);\n                if cached.map(|c| &c.tip) == Some(tip) {\n                    continue; // unchanged tip → cached membership, no scan\n                }\n                let files = store\n                    .ls_files_at(tip)\n                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\n                counters.tree_scans += 1;\n                let members = files\n                    .iter()\n                    .filter_map(|f| {\n                        let (id, rest) = f.split_once('/')?;\n                        (rest == PROJECT_CONTEXT_FILE).then(|| id.to_string())\n                    })\n                    .collect();\n                self.branch_cache\n                    .insert(branch.clone(), BranchScan { tip: tip.clone(), members });\n            }\n        } else {\n            self.branch_cache.clear();\n        }\n\n        // ── 4. Which endpoints recompose ──────────────────────────────────\n        // A membership change can touch ANY endpoint, so a changed fingerprint\n        // (or the backstop / a missing snapshot) recomposes the full roster;\n        // otherwise only the scoped rows are patched.\n        let full = force_full || fingerprint_changed || self.current.is_none();\n        let roster = enumerate_perches(&self.paths.owlery);\n        let targets: Vec<(String, PathBuf, Option<String>)> = if full {\n            roster\n        } else {\n            let by_id: HashMap<String, (PathBuf, Option<String>)> = roster\n                .into_iter()\n                .map(|(id, path, cwd)| (id, (path, cwd)))\n                .collect();\n            req.endpoints\n                .iter()\n                .filter_map(|id| {\n                    by_id.get(id).map(|(p, c)| (id.clone(), p.clone(), c.clone()))\n                })\n                .collect()\n        };\n\n        // ── 5. Compose rows through the SHARED precedence kernel ─────────\n        // (parity by construction — ADR-0037 §4). The derive closure is the\n        // cwd cache: one real derivation per distinct normalized cwd.\n        let cache = std::cell::RefCell::new(&mut self.cwd_cache);\n        let hits = std::cell::Cell::new(0u64);\n        let misses = std::cell::Cell::new(0u64);\n        let derivations = std::cell::Cell::new(0u64);\n        let derive = |dir: &Path| -> (String, String) {\n            let key = projderive::normalize_path(dir);\n            let marker = discover_git_marker(dir);\n            let stamp = marker.as_deref().and_then(stamp_of);\n            let mut cache = cache.borrow_mut();\n            if let Some(hit) = cache.get(&key) {\n                if hit.marker == marker && hit.stamp == stamp {\n                    hits.set(hits.get() + 1);\n                    return (hit.id.clone(), hit.display.clone());\n                }\n            }\n            misses.set(misses.get() + 1);\n            derivations.set(derivations.get() + 1);\n            let (id, display) = spt_store::project::project_id_and_display_for_dir(dir);\n            cache.insert(\n                key,\n                CwdDerivation { id: id.clone(), display: display.clone(), marker, stamp },\n            );\n            (id, display)\n        };\n\n        let mut endpoints: BTreeMap<String, EndpointProject> = if full {\n            BTreeMap::new()\n        } else {\n            self.current.as_ref().map(|i| i.endpoints.clone()).unwrap_or_default()\n        };\n        if !full {\n            // A scoped id whose perch vanished (purge) drops its row — the\n            // roster is the authority; a stale attribution never survives it.\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n            let live: BTreeSet<&String> = targets.iter().map(|(id, _, _)| id).collect();\n            for id in &req.endpoints {\n                if !live.contains(id) {\n                    endpoints.remove(id);\n                }\n            }\n        }\n        for (id, perch_path, origin_cwd) in &targets {\n            let entries = sessions::read_all(perch_path);\n            let store_ids: Vec<String> = self\n                .branch_order\n                .iter()\n                .filter(|(_, branch)| {\n                    self.branch_cache\n                        .get(branch)\n                        .is_some_and(|scan| scan.members.contains(id))\n                })\n                .map(|(project, _)| project.clone())\n                .collect();\n            let refs = projderive::project_refs_from(\n                &entries,\n                origin_cwd.as_deref(),\n                store_ids,\n                &self.paths.owlery,\n                derive, // captures only shared refs → Copy; each call reuses one closure\n            );\n            // The FULL precedence-ordered history is persisted (W3 additive\n            // `refs`, REQ-PICKER-PROJECT-HISTORY-TRUTH: the picker projects\n            // history, not head); the head fields stay beside it for W2-shaped\n            // consumers and the status surface.\n            // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\n            let head = refs.first().cloned();\n            let ref_entries: Vec<projindex::ProjectRefEntry> = refs\n                .into_iter()\n                .map(|r| projindex::ProjectRefEntry {\n                    id: r.id,\n                    dir: r.dir,\n                    display: r.display,\n                    source: Some(r.source.as_str().to_string()),\n                })\n                .collect();\n            endpoints.insert(\n                id.clone(),\n                EndpointProject {\n                    project_id: head.as_ref().map(|h| h.id.clone()),\n                    display: head.as_ref().map(|h| h.display.clone()),\n                    source: head.as_ref().map(|h| h.source.as_str().to_string()),\n                    updated_ms: now,\n                    refs: ref_entries,\n                },\n            );\n        }\n        counters.derivations += derivations.get();\n        self.stats.cwd_cache_hits += hits.get();\n        self.stats.cwd_cache_misses += misses.get();\n\n        // The persisted cwd map (W3 additive `cwds` — the resume pane's\n        // per-session-cwd source, REQ-RESUME-ROW-PER-PROJECT without git): a\n        // FULL cycle publishes exactly the post-compose cache (fresh + pruned\n        // of dead cwds); a scoped patch merges the cycle's cache over the\n        // previous snapshot so rows this cycle did not visit keep their\n        // entries (a restarted brain's cache starts empty).\n        // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\n        let mut cwds: BTreeMap<String, projindex::CwdProject> = if full {\n            BTreeMap::new()\n        } else {\n            self.current.as_ref().map(|i| i.cwds.clone()).unwrap_or_default()\n        };\n        for (norm, entry) in self.cwd_cache.iter() {\n            if !entry.id.is_empty() {\n                cwds.insert(\n                    norm.clone(),\n                    projindex::CwdProject { id: entry.id.clone(), display: entry.display.clone() },\n                );\n            }\n        }\n\n        // ── 6. Atomic publish; last-known-good on failure ─────────────────\n        let index = ProjectIndex {\n            schema: projindex::PROJECT_INDEX_SCHEMA,\n            generated_ms: now,\n            source_generation: fingerprint.clone(),\n            endpoints,\n            cwds,\n        };\n        projindex::write_index_at(&self.paths.index_path, &index)\n            .map_err(|e| format!(\"publish: {e}\"))?;\n        if self.repair_pending {\n            self.stats.repairs += 1;\n            self.repair_pending = false;\n        }\n        self.last_fingerprint = fingerprint;\n        self.stats.generated_ms = now;\n        self.stats.source_generation = index.source_generation.clone();\n        self.stats.endpoints = index.endpoints.len() as u64;\n        self.stats.projects = distinct_projects(&index) as u64;\n        self.current = Some(index);\n\n        let report = CycleReport { counters, published: true, skipped_unchanged: false };\n        self.finish_cycle(&report, now, started.elapsed());\n        Ok(report)\n    }\n\n    /// Book a FAILED cycle: the published snapshot is untouched\n    /// (last-known-good) and every read until the next success is a stale\n    /// read by contract.\n    pub fn record_failure(&mut self, error: String) {\n        self.stats.last_error = Some(error);\n        self.stats.stale_reads += 1;\n        self.stats.last_run_ms = now_ms();\n        self.stats.pending_refresh = true; // the work is still owed\n        self.write_stats();\n    }\n\n    fn finish_cycle(&mut self, report: &CycleReport, now: u64, took: Duration) {\n        report.counters.add_to(&mut self.stats.cumulative);\n        self.stats.last_cycle = report.counters.clone();\n        self.stats.last_run_ms = now;\n        self.stats.last_duration_ms = took.as_millis() as u64;\n        self.stats.last_error = None;\n        self.stats.pending_refresh = false;\n        self.stats.cwds = self.cwd_cache.len() as u64;\n        self.write_stats();\n    }\n\n    /// Publish the observability sidecar (atomic, best-effort — stats must\n    /// never fail the index itself).\n    pub fn write_stats(&self) {\n        if let Some(dir) = self.paths.stats_path.parent() {\n            let _ = std::fs::create_dir_all(dir);\n        }\n        if let Ok(body) = serde_json::to_string(&self.stats) {\n            let _ = spt_store::atomic::atomic_write_string(&self.paths.stats_path, &body);\n        }\n    }\n}\n\nfn distinct_projects(index: &ProjectIndex) -> usize {\n    index\n        .endpoints\n        .values()\n        .filter_map(|e| e.project_id.as_deref())\n        .collect::<BTreeSet<_>>()\n        .len()\n}\n\n/// Enumerate the flat perch roster: every top-level owlery dir with a\n/// PARSEABLE `info.json` → `(id, perch_path, origin_cwd)`. Mirrors the\n/// list/picker roster semantics (a corrupt record renders `-` there and has\n/// no derivable attribution here; nested psyche perches are not list rows).\nfn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\n    let Ok(entries) = std::fs::read_dir(owlery) else {\n        return Vec::new();\n    };\n    let mut out = Vec::new();\n    for entry in entries.flatten() {\n        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {\n            continue;\n        }\n        let path = entry.path();\n        if let Some(rec) = info::read_info(&path) {\n            out.push((rec.id, path, rec.cwd));\n        }\n    }\n    out\n}\n\n/// The repo-identity marker for a dir: the nearest ancestor's `.git/config`\n/// (a normal checkout) or the `.git` gitfile itself (a linked worktree — the\n/// file's content moves when the repo pointer moves). Pure filesystem walk,\n/// NO git spawn — this is what lets cache validity be checked per cycle for\n/// free.\nfn discover_git_marker(dir: &Path) -> Option<PathBuf> {\n    for anc in dir.ancestors() {\n        let g = anc.join(\".git\");\n        if g.is_dir() {\n            return Some(g.join(\"config\"));\n        }\n        if g.is_file() {\n            return Some(g);\n        }\n    }\n    None\n}\n\n/// (mtime epoch-ms, len) of a marker file — the cheap identity stamp.\nfn stamp_of(p: &Path) -> Option<(u64, u64)> {\n    let meta = std::fs::metadata(p).ok()?;\n    let mtime = meta\n        .modified()\n        .ok()?\n        .duration_since(std::time::UNIX_EPOCH)\n        .ok()?\n        .as_millis() as u64;\n    Some((mtime, meta.len()))\n}\n\nfn env_ms(name: &str, default: u64) -> u64 {\n    std::env::var(name)\n        .ok()\n        .and_then(|v| v.parse().ok())\n        .unwrap_or(default)\n}\n\n/// Spawn the brain's index-writer host: ONE thread (single-flight by\n/// construction) that loads the persisted index, runs the boot reconcile\n/// (fingerprint-gated: a warm start with an unchanged generation scans\n/// nothing), then drains + debounces + coalesces the invalidation queue and\n/// runs the periodic full backstop until `stop`. Mirrors\n/// `livehost::spawn_live_host`; the thread dies with the brain and the next\n/// brain rehydrates from the persisted index (ready without warm).\n// [impl->REQ-PROJECT-INDEX-WRITER]\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\n        // untouched): rigs that need a writer-silent daemon set it — the\n        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\n        if std::env::var_os(\"SPT_PROJINDEX_DISABLE\").is_some() {\n            return;\n        }\n        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\n        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\n        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\n\n        let mut engine = WriterEngine::new(WriterPaths::production());\n        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\n\n        // Boot reconcile: global but NOT forced — a warm start over an\n        // unchanged store exits without a scan (the cold/warm start gate).\n        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n        let boot = Coalesced { global: true, ..boot };\n        if let Err(e) = engine.reconcile(&boot, false) {\n            engine.record_failure(e);\n        }\n\n        let mut since_periodic = Duration::ZERO;\n        while !stop.load(Ordering::SeqCst) {\n            // Sleep one poll slice (sliced so a stop lands promptly).\n            let mut left = poll.as_millis() as u64;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n            if stop.load(Ordering::SeqCst) {\n                break;\n            }\n            since_periodic += poll;\n\n            let due_periodic = since_periodic >= periodic;\n            let has_events = projinval::pending_at(&engine.paths.invalidations_dir);\n            if !has_events && !due_periodic {\n                continue;\n            }\n\n            if has_events {\n                // Debounce: let the burst land, then drain it ALL — the\n                // coalescing window (multiple invalidations → one refresh).\n                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n                std::thread::sleep(debounce);\n                engine.stats.pending_refresh = true;\n                engine.write_stats();\n            }\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n            if due_periodic {\n                since_periodic = Duration::ZERO;\n            }\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\n            if req.is_empty() && !due_periodic {\n                continue; // a racer consumed it (never happens in production)\n            }\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\n                engine.record_failure(e);\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_store::contextstore::ContextStore;\n    use spt_store::projinval::InvalScope;\n    use std::time::Instant;\n    use tempfile::TempDir;\n\n    /// A self-contained home-shaped fixture tree (NO env, NO real $SPT_HOME):\n    /// owlery + tracked context store + index paths, all under one TempDir.\n    struct Fixture {\n        _tmp: TempDir,\n        home: PathBuf,\n        paths: WriterPaths,\n    }\n\n    impl Fixture {\n        fn new() -> Fixture {\n            let tmp = TempDir::new().unwrap();\n            let home = tmp.path().to_path_buf();\n            let paths = WriterPaths {\n                owlery: home.join(\"owlery\"),\n                seed_git: contextstore::seed_git_dir(&home.join(\"tracked\")),\n                index_path: home.join(\"index\").join(\"project-index.json\"),\n                stats_path: home.join(\"index\").join(\"project-index-stats.json\"),\n                invalidations_dir: home.join(\"index\").join(\"invalidations\"),\n            };\n            std::fs::create_dir_all(&paths.owlery).unwrap();\n            Fixture { _tmp: tmp, home, paths }\n        }\n\n        fn store(&self) -> ContextStore {\n            ContextStore::open_or_init_in(&self.home.join(\"tracked\")).unwrap()\n        }\n\n        /// A perch with an optional origin cwd and optional session-ledger cwd.\n        fn perch(&self, id: &str, origin_cwd: Option<&Path>, session_cwd: Option<&Path>) {\n            let perch = self.paths.owlery.join(id);\n            std::fs::create_dir_all(&perch).unwrap();\n            let mut rec = info::InfoJson::new(id, \"t\", 0, &format!(\"sid-{id}\"), \"live_agent\");\n            rec.cwd = origin_cwd.map(|p| p.to_string_lossy().to_string());\n            info::write_info(&perch, &rec).unwrap();\n            if let Some(cwd) = session_cwd {\n                sessions::append(\n                    &perch,\n                    &sessions::SessionEntry {\n                        ts: \"2026-07-15T00:00:00Z\".to_string(),\n                        session_id: format!(\"s-{id}\"),\n                        trigger: sessions::SessionTrigger::Boot,\n                        cwd: Some(cwd.to_string_lossy().to_string()),\n                        ordinal: None,\n                        adapter: None,\n                    },\n                )\n                .unwrap();\n            }\n        }\n\n        /// Give `id` committed context in `project` (a p-* membership row).\n        fn membership(&self, project: &str, id: &str) {\n            let cs = self.store();\n            let file = cs.project_context_path(project, id).unwrap();\n            std::fs::write(&file, format!(\"{id} in {project}\")).unwrap();\n            cs.commit_project(project, &format!(\"{id} slice\")).unwrap().unwrap();\n        }\n\n        /// A plain (non-git) project dir whose folder name becomes the id.\n        fn project_dir(&self, name: &str) -> PathBuf {\n            let dir = self.home.join(\"workdirs\").join(name);\n            std::fs::create_dir_all(&dir).unwrap();\n            dir\n        }\n    }\n\n    fn global() -> Coalesced {\n        Coalesced { global: true, ..Coalesced::default() }\n    }\n…\n}\n\n[Showing lines 1-763 of 1270. Use :764 to continue]"}],"details":{"truncation":{"content":"//! The materialized project-index WRITER — the daemon's derived-state\r\n//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\r\n//!\r\n//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\r\n//! `$SPT_HOME/index/project-index.json` (format + read path owned by\r\n//! `spt_store::projindex`, W1). One brain-hosted thread:\r\n//!\r\n//! - loads the persisted index at start and is READY WITHOUT WARM — the boot\r\n//!   reconcile runs in the background; a cold daemon never blocks on it;\r\n//! - reconciles with BATCHED work, `O(P + B + F + C)`: ONE branch enumeration\r\n//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\r\n//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\r\n//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\r\n//!   in a project repo touch neither the path nor `.git/config`, so they are\r\n//!   a NO-OP by construction — ADR-0037 §3);\r\n//! - publishes atomically via `projindex::write_index_at` and preserves\r\n//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\r\n//! - drains + coalesces the `projinval` event queue (multiple invalidations →\r\n//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\r\n//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\r\n//!   scan;\r\n//! - maintains an observability sidecar (`project-index-stats.json`) — index\r\n//!   presence alone is not health: generated time, source generation, pending\r\n//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\r\n//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\r\n//!   (wall-clock budgets are manual acceptance ONLY).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n\r\nuse std::collections::{BTreeMap, BTreeSet, HashMap};\r\nuse std::path::{Path, PathBuf};\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread::JoinHandle;\r\nuse std::time::Duration;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse spt_store::branchstore::BranchStore;\r\nuse spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};\r\nuse spt_store::info;\r\nuse spt_store::perch;\r\nuse spt_store::projderive;\r\nuse spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\r\nuse spt_store::projinval::{self, Coalesced};\r\nuse spt_store::sessions;\r\n\r\nuse crate::brain::now_ms;\r\n\r\n/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\r\n/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\r\nconst POLL_MS: u64 = 500;\r\n/// Debounce window after the first observed event — a burst (an adapter\r\n/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\r\nconst DEBOUNCE_MS: u64 = 250;\r\n/// The low-frequency periodic FULL reconcile — the backstop that catches a\r\n/// lost nudge (`SPT_PROJINDEX_PERIODIC_MS` override for int rigs).\r\nconst PERIODIC_MS: u64 = 300_000;\r\n\r\n/// The complexity counters — the deterministic CI gate surface (gate ruling:\r\n/// counters gate, wall-clock is manual acceptance only).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct CycleCounters {\r\n    /// Branch enumerations this cycle (contract: exactly 1 when the store exists).\r\n    pub branch_enumerations: u64,\r\n    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\r\n    pub tree_scans: u64,\r\n    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\r\n    pub derivations: u64,\r\n}\r\n\r\nimpl CycleCounters {\r\n    fn add_to(&self, total: &mut CycleCounters) {\r\n        total.branch_enumerations += self.branch_enumerations;\r\n        total.tree_scans += self.tree_scans;\r\n        total.derivations += self.derivations;\r\n    }\r\n}\r\n\r\n/// The observability sidecar (`$SPT_HOME/index/project-index-stats.json`) —\r\n/// written atomically by the writer after every cycle; read by\r\n/// `spt daemon status` and by the int gates. Additive serde-default shape\r\n/// (derived + regenerable, like the index itself).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct IndexWriterStats {\r\n    /// When the current snapshot was generated (epoch ms; 0 = never).\r\n    #[serde(default)]\r\n    pub generated_ms: u64,\r\n    /// The published snapshot's branch-tip fingerprint.\r\n    #[serde(default)]\r\n    pub source_generation: String,\r\n    /// Whether a refresh is queued/underway right now.\r\n    #[serde(default)]\r\n    pub pending_refresh: bool,\r\n    /// When the last reconcile cycle ran (epoch ms).\r\n    #[serde(default)]\r\n    pub last_run_ms: u64,\r\n    /// How long it took (ms).\r\n    #[serde(default)]\r\n    pub last_duration_ms: u64,\r\n    /// The last cycle failure, if the most recent cycle failed (`None` = last\r\n    /// cycle succeeded). While set, readers are on last-known-good.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub last_error: Option<String>,\r\n    /// Row counts of the published snapshot.\r\n    #[serde(default)]\r\n    pub endpoints: u64,\r\n    #[serde(default)]\r\n    pub projects: u64,\r\n    /// Distinct cwds in the derivation cache.\r\n    #[serde(default)]\r\n    pub cwds: u64,\r\n    /// Cumulative cwd-cache hit/miss census.\r\n    #[serde(default)]\r\n    pub cwd_cache_hits: u64,\r\n    #[serde(default)]\r\n    pub cwd_cache_misses: u64,\r\n    /// Cycles that FAILED to publish — every read until the next success is a\r\n    /// stale (last-known-good) read by contract.\r\n    #[serde(default)]\r\n    pub stale_reads: u64,\r\n    /// Publishes that REPLACED an unreadable / schema-mismatched index file\r\n    /// (the writer repairing a torn or foreign snapshot).\r\n    #[serde(default)]\r\n    pub repairs: u64,\r\n    /// The most recent cycle's complexity counters (the CI-gate surface).\r\n    #[serde(default)]\r\n    pub last_cycle: CycleCounters,\r\n    /// Lifetime totals (this brain generation).\r\n    #[serde(default)]\r\n    pub cumulative: CycleCounters,\r\n}\r\n\r\n/// The stats sidecar path, beside the index inside `projindex::index_dir()`.\r\npub fn stats_path() -> PathBuf {\r\n    projindex::index_dir().join(\"project-index-stats.json\")\r\n}\r\n\r\n/// Read the stats sidecar (tolerant: absent/corrupt → `None`) — the\r\n/// `spt daemon status` consumer half.\r\npub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\r\n    let raw = std::fs::read_to_string(path).ok()?;\r\n    serde_json::from_str(&raw).ok()\r\n}\r\n\r\n/// Every filesystem root the engine touches, injected so units run against\r\n/// plain temp dirs with zero env games.\r\n#[derive(Debug, Clone)]\r\npub struct WriterPaths {\r\n    pub owlery: PathBuf,\r\n    /// The context store's bare seed repo (`tracked/.seed.git`).\r\n    pub seed_git: PathBuf,\r\n    pub index_path: PathBuf,\r\n    pub stats_path: PathBuf,\r\n    pub invalidations_dir: PathBuf,\r\n}\r\n\r\nimpl WriterPaths {\r\n    /// The production layout under the live `$SPT_HOME`.\r\n    pub fn production() -> WriterPaths {\r\n        WriterPaths {\r\n            owlery: perch::owlery_dir(),\r\n            seed_git: contextstore::seed_git_dir(&perch::tracked_dir()),\r\n            index_path: projindex::index_path(),\r\n            stats_path: stats_path(),\r\n            invalidations_dir: projinval::invalidations_dir(),\r\n        }\r\n    }\r\n}\r\n\r\n/// One scanned branch: its tip at scan time + the endpoint ids holding\r\n/// `<id>/project-context.md` in that tip tree.\r\n#[derive(Debug, Clone)]\r\nstruct BranchScan {\r\n    tip: String,\r\n    members: BTreeSet<String>,\r\n}\r\n\r\n/// One cached cwd derivation, stamped with the repo-identity marker that\r\n/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\r\n/// for a linked worktree). Identity refreshes ONLY when the marker path or\r\n/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\r\n/// §3: commits do not alter project identity).\r\n#[derive(Debug, Clone)]\r\nstruct CwdDerivation {\r\n    id: String,\r\n    display: String,\r\n    marker: Option<PathBuf>,\r\n    stamp: Option<(u64, u64)>,\r\n}\r\n\r\n/// What one reconcile cycle did — the unit-gate surface.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct CycleReport {\r\n    pub counters: CycleCounters,\r\n    /// A new snapshot was written.\r\n    pub published: bool,\r\n    /// The cycle proved the store unchanged and exited without scanning,\r\n    /// deriving, or writing — the warm-start / no-op contract.\r\n    pub skipped_unchanged: bool,\r\n}\r\n\r\n/// The single-flight reconcile engine. ONE instance lives on ONE writer\r\n/// thread — single-flight is by construction, not by locking.\r\npub struct WriterEngine {\r\n    paths: WriterPaths,\r\n    /// The last published snapshot (in-memory copy; `None` until the first\r\n    /// load/publish).\r\n    current: Option<ProjectIndex>,\r\n    /// The fingerprint the published snapshot was derived from.\r\n    last_fingerprint: String,\r\n    /// branch → last scan (tips + membership), the ≤1-scan-per-changed-branch\r\n    /// cache. Brain-local: a brain restart re-scans (bounded by B) only when\r\n    /// the fingerprint moved or rows must be recomposed.\r\n    branch_cache: BTreeMap<String, BranchScan>,\r\n    /// `p-*` branches in recency order as of the last enumeration:\r\n    /// `(project_id, branch)`.\r\n    branch_order: Vec<(String, String)>,\r\n    /// normalized cwd → stamped derivation (the C term's cross-cycle cache).\r\n    cwd_cache: HashMap<String, CwdDerivation>,\r\n    /// Pending repair: the on-disk index was unreadable/foreign at load; the\r\n    /// next successful publish counts as a repair.\r\n    repair_pending: bool,\r\n    pub stats: IndexWriterStats,\r\n}\r\n\r\nimpl WriterEngine {\r\n    /// Build the engine and LOAD the persisted index (ADR-0037 §2: load at\r\n    /// startup, ready without warm — this constructor performs pure file I/O\r\n    /// and returns immediately; reconciling is the thread's job).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    pub fn new(paths: WriterPaths) -> WriterEngine {\r\n        let (current, repair_pending) = match projindex::read_index_at(&paths.index_path) {\r\n            IndexRead::Snapshot(idx) => (Some(idx), false),\r\n            IndexRead::Absent(AbsentReason::Missing) => (None, false),\r\n            // Torn/corrupt/foreign file present: readers are degrading to `-`;\r\n            // our next publish REPAIRS it (and is counted as such).\r\n            IndexRead::Absent(_) => (None, true),\r\n        };\r\n        let last_fingerprint = current\r\n            .as_ref()\r\n            .map(|i| i.source_generation.clone())\r\n            .unwrap_or_default();\r\n        let mut stats = IndexWriterStats {\r\n            generated_ms: current.as_ref().map(|i| i.generated_ms).unwrap_or(0),\r\n            source_generation: last_fingerprint.clone(),\r\n            pending_refresh: true, // the boot reconcile is queued by definition\r\n            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\r\n            ..IndexWriterStats::default()\r\n        };\r\n        stats.projects = current\r\n            .as_ref()\r\n            .map(|i| distinct_projects(i) as u64)\r\n            .unwrap_or(0);\r\n        WriterEngine {\r\n            paths,\r\n            current,\r\n            last_fingerprint,\r\n            branch_cache: BTreeMap::new(),\r\n            branch_order: Vec::new(),\r\n            cwd_cache: HashMap::new(),\r\n            repair_pending,\r\n            stats,\r\n        }\r\n    }\r\n\r\n    /// Run ONE reconcile cycle over the coalesced request. `force_full`\r\n    /// recomposes every endpoint even on an unchanged fingerprint (the\r\n    /// periodic backstop); a non-forced global request with an unchanged\r\n    /// fingerprint and no scoped work SKIPS everything — the warm-start\r\n    /// no-scan contract. Any error leaves the published index untouched\r\n    /// (last-known-good) and surfaces in `stats.last_error`.\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    pub fn reconcile(&mut self, req: &Coalesced, force_full: bool) -> Result<CycleReport, String> {\r\n        let started = std::time::Instant::now();\r\n        let now = now_ms();\r\n        let mut counters = CycleCounters::default();\r\n\r\n        // Explicit cwd invalidations (new session in a dir whose identity may\r\n        // have moved): drop those cache entries up front so the compose pass\r\n        // re-derives them exactly once.\r\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n        for cwd in &req.cwds {\r\n            self.cwd_cache.remove(&projderive::normalize_path(Path::new(cwd)));\r\n        }\r\n\r\n        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\r\n        let store = if self.paths.seed_git.exists() {\r\n            Some(\r\n                BranchStore::open_or_init(&self.paths.seed_git)\r\n                    .map_err(|e| format!(\"branch store open: {e}\"))?,\r\n            )\r\n        } else {\r\n            None // no context store yet — sessions/origin still index\r\n        };\r\n        let tips: Vec<(String, String)> = match &store {\r\n            Some(store) => {\r\n                counters.branch_enumerations += 1;\r\n                store\r\n                    .branch_tips_by_recency()\r\n                    .map_err(|e| format!(\"branch enumeration: {e}\"))?\r\n            }\r\n            None => Vec::new(),\r\n        };\r\n        let fingerprint = projindex::fingerprint_tips(&tips);\r\n        let fingerprint_changed = fingerprint != self.last_fingerprint;\r\n\r\n        // ── 2. The no-work exit: unchanged generation performs NO scan ───\r\n        if !fingerprint_changed\r\n            && !force_full\r\n            && req.endpoints.is_empty()\r\n            && req.cwds.is_empty()\r\n            && self.current.is_some()\r\n        {\r\n            let report = CycleReport { counters, published: false, skipped_unchanged: true };\r\n            self.finish_cycle(&report, now, started.elapsed());\r\n            return Ok(report);\r\n        }\r\n\r\n        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\r\n        self.branch_order = tips\r\n            .iter()\r\n            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\r\n            .collect();\r\n        if let (false, Some(store)) = (self.branch_order.is_empty(), &store) {\r\n            let live: BTreeSet<&str> =\r\n                self.branch_order.iter().map(|(_, b)| b.as_str()).collect();\r\n            self.branch_cache.retain(|b, _| live.contains(b.as_str()));\r\n            for (branch, tip) in tips.iter().filter(|(b, _)| b.starts_with(\"p-\")) {\r\n                let cached = self.branch_cache.get(branch);\r\n                if cached.map(|c| &c.tip) == Some(tip) {\r\n                    continue; // unchanged tip → cached membership, no scan\r\n                }\r\n                let files = store\r\n                    .ls_files_at(tip)\r\n                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\r\n                counters.tree_scans += 1;\r\n                let members = files\r\n                    .iter()\r\n                    .filter_map(|f| {\r\n                        let (id, rest) = f.split_once('/')?;\r\n                        (rest == PROJECT_CONTEXT_FILE).then(|| id.to_string())\r\n                    })\r\n                    .collect();\r\n                self.branch_cache\r\n                    .insert(branch.clone(), BranchScan { tip: tip.clone(), members });\r\n            }\r\n        } else {\r\n            self.branch_cache.clear();\r\n        }\r\n\r\n        // ── 4. Which endpoints recompose ──────────────────────────────────\r\n        // A membership change can touch ANY endpoint, so a changed fingerprint\r\n        // (or the backstop / a missing snapshot) recomposes the full roster;\r\n        // otherwise only the scoped rows are patched.\r\n        let full = force_full || fingerprint_changed || self.current.is_none();\r\n        let roster = enumerate_perches(&self.paths.owlery);\r\n        let targets: Vec<(String, PathBuf, Option<String>)> = if full {\r\n            roster\r\n        } else {\r\n            let by_id: HashMap<String, (PathBuf, Option<String>)> = roster\r\n                .into_iter()\r\n                .map(|(id, path, cwd)| (id, (path, cwd)))\r\n                .collect();\r\n            req.endpoints\r\n                .iter()\r\n                .filter_map(|id| {\r\n                    by_id.get(id).map(|(p, c)| (id.clone(), p.clone(), c.clone()))\r\n                })\r\n                .collect()\r\n        };\r\n\r\n        // ── 5. Compose rows through the SHARED precedence kernel ─────────\r\n        // (parity by construction — ADR-0037 §4). The derive closure is the\r\n        // cwd cache: one real derivation per distinct normalized cwd.\r\n        let cache = std::cell::RefCell::new(&mut self.cwd_cache);\r\n        let hits = std::cell::Cell::new(0u64);\r\n        let misses = std::cell::Cell::new(0u64);\r\n        let derivations = std::cell::Cell::new(0u64);\r\n        let derive = |dir: &Path| -> (String, String) {\r\n            let key = projderive::normalize_path(dir);\r\n            let marker = discover_git_marker(dir);\r\n            let stamp = marker.as_deref().and_then(stamp_of);\r\n            let mut cache = cache.borrow_mut();\r\n            if let Some(hit) = cache.get(&key) {\r\n                if hit.marker == marker && hit.stamp == stamp {\r\n                    hits.set(hits.get() + 1);\r\n                    return (hit.id.clone(), hit.display.clone());\r\n                }\r\n            }\r\n            misses.set(misses.get() + 1);\r\n            derivations.set(derivations.get() + 1);\r\n            let (id, display) = spt_store::project::project_id_and_display_for_dir(dir);\r\n            cache.insert(\r\n                key,\r\n                CwdDerivation { id: id.clone(), display: display.clone(), marker, stamp },\r\n            );\r\n            (id, display)\r\n        };\r\n\r\n        let mut endpoints: BTreeMap<String, EndpointProject> = if full {\r\n            BTreeMap::new()\r\n        } else {\r\n            self.current.as_ref().map(|i| i.endpoints.clone()).unwrap_or_default()\r\n        };\r\n        if !full {\r\n            // A scoped id whose perch vanished (purge) drops its row — the\r\n            // roster is the authority; a stale attribution never survives it.\r\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n            let live: BTreeSet<&String> = targets.iter().map(|(id, _, _)| id).collect();\r\n            for id in &req.endpoints {\r\n                if !live.contains(id) {\r\n                    endpoints.remove(id);\r\n                }\r\n            }\r\n        }\r\n        for (id, perch_path, origin_cwd) in &targets {\r\n            let entries = sessions::read_all(perch_path);\r\n            let store_ids: Vec<String> = self\r\n                .branch_order\r\n                .iter()\r\n                .filter(|(_, branch)| {\r\n                    self.branch_cache\r\n                        .get(branch)\r\n                        .is_some_and(|scan| scan.members.contains(id))\r\n                })\r\n                .map(|(project, _)| project.clone())\r\n                .collect();\r\n            let refs = projderive::project_refs_from(\r\n                &entries,\r\n                origin_cwd.as_deref(),\r\n                store_ids,\r\n                &self.paths.owlery,\r\n                derive, // captures only shared refs → Copy; each call reuses one closure\r\n            );\r\n            // The FULL precedence-ordered history is persisted (W3 additive\r\n            // `refs`, REQ-PICKER-PROJECT-HISTORY-TRUTH: the picker projects\r\n            // history, not head); the head fields stay beside it for W2-shaped\r\n            // consumers and the status surface.\r\n            // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\r\n            let head = refs.first().cloned();\r\n            let ref_entries: Vec<projindex::ProjectRefEntry> = refs\r\n                .into_iter()\r\n                .map(|r| projindex::ProjectRefEntry {\r\n                    id: r.id,\r\n                    dir: r.dir,\r\n                    display: r.display,\r\n                    source: Some(r.source.as_str().to_string()),\r\n                })\r\n                .collect();\r\n            endpoints.insert(\r\n                id.clone(),\r\n                EndpointProject {\r\n                    project_id: head.as_ref().map(|h| h.id.clone()),\r\n                    display: head.as_ref().map(|h| h.display.clone()),\r\n                    source: head.as_ref().map(|h| h.source.as_str().to_string()),\r\n                    updated_ms: now,\r\n                    refs: ref_entries,\r\n                },\r\n            );\r\n        }\r\n        counters.derivations += derivations.get();\r\n        self.stats.cwd_cache_hits += hits.get();\r\n        self.stats.cwd_cache_misses += misses.get();\r\n\r\n        // The persisted cwd map (W3 additive `cwds` — the resume pane's\r\n        // per-session-cwd source, REQ-RESUME-ROW-PER-PROJECT without git): a\r\n        // FULL cycle publishes exactly the post-compose cache (fresh + pruned\r\n        // of dead cwds); a scoped patch merges the cycle's cache over the\r\n        // previous snapshot so rows this cycle did not visit keep their\r\n        // entries (a restarted brain's cache starts empty).\r\n        // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\r\n        let mut cwds: BTreeMap<String, projindex::CwdProject> = if full {\r\n            BTreeMap::new()\r\n        } else {\r\n            self.current.as_ref().map(|i| i.cwds.clone()).unwrap_or_default()\r\n        };\r\n        for (norm, entry) in self.cwd_cache.iter() {\r\n            if !entry.id.is_empty() {\r\n                cwds.insert(\r\n                    norm.clone(),\r\n                    projindex::CwdProject { id: entry.id.clone(), display: entry.display.clone() },\r\n                );\r\n            }\r\n        }\r\n\r\n        // ── 6. Atomic publish; last-known-good on failure ─────────────────\r\n        let index = ProjectIndex {\r\n            schema: projindex::PROJECT_INDEX_SCHEMA,\r\n            generated_ms: now,\r\n            source_generation: fingerprint.clone(),\r\n            endpoints,\r\n            cwds,\r\n        };\r\n        projindex::write_index_at(&self.paths.index_path, &index)\r\n            .map_err(|e| format!(\"publish: {e}\"))?;\r\n        if self.repair_pending {\r\n            self.stats.repairs += 1;\r\n            self.repair_pending = false;\r\n        }\r\n        self.last_fingerprint = fingerprint;\r\n        self.stats.generated_ms = now;\r\n        self.stats.source_generation = index.source_generation.clone();\r\n        self.stats.endpoints = index.endpoints.len() as u64;\r\n        self.stats.projects = distinct_projects(&index) as u64;\r\n        self.current = Some(index);\r\n\r\n        let report = CycleReport { counters, published: true, skipped_unchanged: false };\r\n        self.finish_cycle(&report, now, started.elapsed());\r\n        Ok(report)\r\n    }\r\n\r\n    /// Book a FAILED cycle: the published snapshot is untouched\r\n    /// (last-known-good) and every read until the next success is a stale\r\n    /// read by contract.\r\n    pub fn record_failure(&mut self, error: String) {\r\n        self.stats.last_error = Some(error);\r\n        self.stats.stale_reads += 1;\r\n        self.stats.last_run_ms = now_ms();\r\n        self.stats.pending_refresh = true; // the work is still owed\r\n        self.write_stats();\r\n    }\r\n\r\n    fn finish_cycle(&mut self, report: &CycleReport, now: u64, took: Duration) {\r\n        report.counters.add_to(&mut self.stats.cumulative);\r\n        self.stats.last_cycle = report.counters.clone();\r\n        self.stats.last_run_ms = now;\r\n        self.stats.last_duration_ms = took.as_millis() as u64;\r\n        self.stats.last_error = None;\r\n        self.stats.pending_refresh = false;\r\n        self.stats.cwds = self.cwd_cache.len() as u64;\r\n        self.write_stats();\r\n    }\r\n\r\n    /// Publish the observability sidecar (atomic, best-effort — stats must\r\n    /// never fail the index itself).\r\n    pub fn write_stats(&self) {\r\n        if let Some(dir) = self.paths.stats_path.parent() {\r\n            let _ = std::fs::create_dir_all(dir);\r\n        }\r\n        if let Ok(body) = serde_json::to_string(&self.stats) {\r\n            let _ = spt_store::atomic::atomic_write_string(&self.paths.stats_path, &body);\r\n        }\r\n    }\r\n}\r\n\r\nfn distinct_projects(index: &ProjectIndex) -> usize {\r\n    index\r\n        .endpoints\r\n        .values()\r\n        .filter_map(|e| e.project_id.as_deref())\r\n        .collect::<BTreeSet<_>>()\r\n        .len()\r\n}\r\n\r\n/// Enumerate the flat perch roster: every top-level owlery dir with a\r\n/// PARSEABLE `info.json` → `(id, perch_path, origin_cwd)`. Mirrors the\r\n/// list/picker roster semantics (a corrupt record renders `-` there and has\r\n/// no derivable attribution here; nested psyche perches are not list rows).\r\nfn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\r\n    let Ok(entries) = std::fs::read_dir(owlery) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out = Vec::new();\r\n    for entry in entries.flatten() {\r\n        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {\r\n            continue;\r\n        }\r\n        let path = entry.path();\r\n        if let Some(rec) = info::read_info(&path) {\r\n            out.push((rec.id, path, rec.cwd));\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n/// The repo-identity marker for a dir: the nearest ancestor's `.git/config`\r\n/// (a normal checkout) or the `.git` gitfile itself (a linked worktree — the\r\n/// file's content moves when the repo pointer moves). Pure filesystem walk,\r\n/// NO git spawn — this is what lets cache validity be checked per cycle for\r\n/// free.\r\nfn discover_git_marker(dir: &Path) -> Option<PathBuf> {\r\n    for anc in dir.ancestors() {\r\n        let g = anc.join(\".git\");\r\n        if g.is_dir() {\r\n            return Some(g.join(\"config\"));\r\n        }\r\n        if g.is_file() {\r\n            return Some(g);\r\n        }\r\n    }\r\n    None\r\n}\r\n\r\n/// (mtime epoch-ms, len) of a marker file — the cheap identity stamp.\r\nfn stamp_of(p: &Path) -> Option<(u64, u64)> {\r\n    let meta = std::fs::metadata(p).ok()?;\r\n    let mtime = meta\r\n        .modified()\r\n        .ok()?\r\n        .duration_since(std::time::UNIX_EPOCH)\r\n        .ok()?\r\n        .as_millis() as u64;\r\n    Some((mtime, meta.len()))\r\n}\r\n\r\nfn env_ms(name: &str, default: u64) -> u64 {\r\n    std::env::var(name)\r\n        .ok()\r\n        .and_then(|v| v.parse().ok())\r\n        .unwrap_or(default)\r\n}\r\n\r\n/// Spawn the brain's index-writer host: ONE thread (single-flight by\r\n/// construction) that loads the persisted index, runs the boot reconcile\r\n/// (fingerprint-gated: a warm start with an unchanged generation scans\r\n/// nothing), then drains + debounces + coalesces the invalidation queue and\r\n/// runs the periodic full backstop until `stop`. Mirrors\r\n/// `livehost::spawn_live_host`; the thread dies with the brain and the next\r\n/// brain rehydrates from the persisted index (ready without warm).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\r\n        // untouched): rigs that need a writer-silent daemon set it — the\r\n        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\r\n        if std::env::var_os(\"SPT_PROJINDEX_DISABLE\").is_some() {\r\n            return;\r\n        }\r\n        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\r\n        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\r\n        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\r\n\r\n        let mut engine = WriterEngine::new(WriterPaths::production());\r\n        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\r\n\r\n        // Boot reconcile: global but NOT forced — a warm start over an\r\n        // unchanged store exits without a scan (the cold/warm start gate).\r\n        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n        let boot = Coalesced { global: true, ..boot };\r\n        if let Err(e) = engine.reconcile(&boot, false) {\r\n            engine.record_failure(e);\r\n        }\r\n\r\n        let mut since_periodic = Duration::ZERO;\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // Sleep one poll slice (sliced so a stop lands promptly).\r\n            let mut left = poll.as_millis() as u64;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n                left -= step;\r\n            }\r\n            if stop.load(Ordering::SeqCst) {\r\n                break;\r\n            }\r\n            since_periodic += poll;\r\n\r\n            let due_periodic = since_periodic >= periodic;\r\n            let has_events = projinval::pending_at(&engine.paths.invalidations_dir);\r\n            if !has_events && !due_periodic {\r\n                continue;\r\n            }\r\n\r\n            if has_events {\r\n                // Debounce: let the burst land, then drain it ALL — the\r\n                // coalescing window (multiple invalidations → one refresh).\r\n                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n                std::thread::sleep(debounce);\r\n                engine.stats.pending_refresh = true;\r\n                engine.write_stats();\r\n            }\r\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n            if due_periodic {\r\n                since_periodic = Duration::ZERO;\r\n            }\r\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\r\n            if req.is_empty() && !due_periodic {\r\n                continue; // a racer consumed it (never happens in production)\r\n            }\r\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\r\n                engine.record_failure(e);\r\n            }\r\n        }\r\n    })\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use spt_store::contextstore::ContextStore;\r\n    use spt_store::projinval::InvalScope;\r\n    use std::time::Instant;\r\n    use tempfile::TempDir;\r\n\r\n    /// A self-contained home-shaped fixture tree (NO env, NO real $SPT_HOME):\r\n    /// owlery + tracked context store + index paths, all under one TempDir.\r\n    struct Fixture {\r\n        _tmp: TempDir,\r\n        home: PathBuf,\r\n        paths: WriterPaths,\r\n    }\r\n\r\n    impl Fixture {\r\n        fn new() -> Fixture {\r\n            let tmp = TempDir::new().unwrap();\r\n            let home = tmp.path().to_path_buf();\r\n            let paths = WriterPaths {\r\n                owlery: home.join(\"owlery\"),\r\n                seed_git: contextstore::seed_git_dir(&home.join(\"tracked\")),\r\n                index_path: home.join(\"index\").join(\"project-index.json\"),\r\n                stats_path: home.join(\"index\").join(\"project-index-stats.json\"),\r\n                invalidations_dir: home.join(\"index\").join(\"invalidations\"),\r\n            };\r\n            std::fs::create_dir_all(&paths.owlery).unwrap();\r\n            Fixture { _tmp: tmp, home, paths }\r\n        }\r\n\r\n        fn store(&self) -> ContextStore {\r\n            ContextStore::open_or_init_in(&self.home.join(\"tracked\")).unwrap()\r\n        }\r\n\r\n        /// A perch with an optional origin cwd and optional session-ledger cwd.\r\n        fn perch(&self, id: &str, origin_cwd: Option<&Path>, session_cwd: Option<&Path>) {\r\n            let perch = self.paths.owlery.join(id);\r\n            std::fs::create_dir_all(&perch).unwrap();\r\n            let mut rec = info::InfoJson::new(id, \"t\", 0, &format!(\"sid-{id}\"), \"live_agent\");\r\n            rec.cwd = origin_cwd.map(|p| p.to_string_lossy().to_string());\r\n            info::write_info(&perch, &rec).unwrap();\r\n            if let Some(cwd) = session_cwd {\r\n                sessions::append(\r\n                    &perch,\r\n                    &sessions::SessionEntry {\r\n                        ts: \"2026-07-15T00:00:00Z\".to_string(),\r\n                        session_id: format!(\"s-{id}\"),\r\n                        trigger: sessions::SessionTrigger::Boot,\r\n                        cwd: Some(cwd.to_string_lossy().to_string()),\r\n                        ordinal: None,\r\n                        adapter: None,\r\n                    },\r\n                )\r\n                .unwrap();\r\n            }\r\n        }\r\n\r\n        /// Give `id` committed context in `project` (a p-* membership row).\r\n        fn membership(&self, project: &str, id: &str) {\r\n            let cs = self.store();\r\n            let file = cs.project_context_path(project, id).unwrap();\r\n            std::fs::write(&file, format!(\"{id} in {project}\")).unwrap();\r\n            cs.commit_project(project, &format!(\"{id} slice\")).unwrap().unwrap();\r\n        }\r\n\r\n        /// A plain (non-git) project dir whose folder name becomes the id.\r\n        fn project_dir(&self, name: &str) -> PathBuf {\r\n            let dir = self.home.join(\"workdirs\").join(name);\r\n            std::fs::create_dir_all(&dir).unwrap();\r\n            dir\r\n        }\r\n    }\r\n\r\n    fn global() -> Coalesced {\r\n        Coalesced { global: true, ..Coalesced::default() }\r","truncated":true,"truncatedBy":"lines","totalLines":1270,"totalBytes":33179,"outputLines":763,"outputBytes":33179,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! The materialized project-index WRITER — the daemon's derived-state\r\n//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\r\n//!\r\n//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\r\n//! `$SPT_HOME/index/project-index.json` (format + read path owned by\r\n//! `spt_store::projindex`, W1). One brain-hosted thread:\r\n//!\r\n//! - loads the persisted index at start and is READY WITHOUT WARM — the boot\r\n//!   reconcile runs in the background; a cold daemon never blocks on it;\r\n//! - reconciles with BATCHED work, `O(P + B + F + C)`: ONE branch enumeration\r\n//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\r\n//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\r\n//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\r\n//!   in a project repo touch neither the path nor `.git/config`, so they are\r\n//!   a NO-OP by construction — ADR-0037 §3);\r\n//! - publishes atomically via `projindex::write_index_at` and preserves\r\n//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\r\n//! - drains + coalesces the `projinval` event queue (multiple invalidations →\r\n//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\r\n//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\r\n//!   scan;\r\n//! - maintains an observability sidecar (`project-index-stats.json`) — index\r\n//!   presence alone is not health: generated time, source generation, pending\r\n//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\r\n//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\r\n//!   (wall-clock budgets are manual acceptance ONLY).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n\r\nuse std::collections::{BTreeMap, BTreeSet, HashMap};\r\nuse std::path::{Path, PathBuf};\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread::JoinHandle;\r\nuse std::time::Duration;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse spt_store::branchstore::BranchStore;\r\nuse spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};\r\nuse spt_store::info;\r\nuse spt_store::perch;\r\nuse spt_store::projderive;\r\nuse spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\r\nuse spt_store::projinval::{self, Coalesced};\r\nuse spt_store::sessions;\r\n\r\nuse crate::brain::now_ms;\r\n\r\n/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\r\n/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\r\nconst POLL_MS: u64 = 500;\r\n/// Debounce window after the first observed event — a burst (an adapter\r\n/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\r\nconst DEBOUNCE_MS: u64 = 250;\r\n/// The low-frequency periodic FULL reconcile — the backstop that catches a\r\n/// lost nudge (`SPT_PROJINDEX_PERIODIC_MS` override for int rigs).\r\nconst PERIODIC_MS: u64 = 300_000;\r\n\r\n/// The complexity counters — the deterministic CI gate surface (gate ruling:\r\n/// counters gate, wall-clock is manual acceptance only).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct CycleCounters {\r\n    /// Branch enumerations this cycle (contract: exactly 1 when the store exists).\r\n    pub branch_enumerations: u64,\r\n    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\r\n    pub tree_scans: u64,\r\n    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\r\n    pub derivations: u64,\r\n}\r\n\r\nimpl CycleCounters {\r\n    fn add_to(&self, total: &mut CycleCounters) {\r\n        total.branch_enumerations += self.branch_enumerations;\r\n        total.tree_scans += self.tree_scans;\r\n        total.derivations += self.derivations;\r\n    }\r\n}\r\n\r\n/// The observability sidecar (`$SPT_HOME/index/project-index-stats.json`) —\r\n/// written atomically by the writer after every cycle; read by\r\n/// `spt daemon status` and by the int gates. Additive serde-default shape\r\n/// (derived + regenerable, like the index itself).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct IndexWriterStats {\r\n    /// When the current snapshot was generated (epoch ms; 0 = never).\r\n    #[serde(default)]\r\n    pub generated_ms: u64,\r\n    /// The published snapshot's branch-tip fingerprint.\r\n    #[serde(default)]\r\n    pub source_generation: String,\r\n    /// Whether a refresh is queued/underway right now.\r\n    #[serde(default)]\r\n    pub pending_refresh: bool,\r\n    /// When the last reconcile cycle ran (epoch ms).\r\n    #[serde(default)]\r\n    pub last_run_ms: u64,\r\n    /// How long it took (ms).\r\n    #[serde(default)]\r\n    pub last_duration_ms: u64,\r\n    /// The last cycle failure, if the most recent cycle failed (`None` = last\r\n    /// cycle succeeded). While set, readers are on last-known-good.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub last_error: Option<String>,\r\n    /// Row counts of the published snapshot.\r\n    #[serde(default)]\r\n    pub endpoints: u64,\r\n    #[serde(default)]\r\n    pub projects: u64,\r\n    /// Distinct cwds in the derivation cache.\r\n    #[serde(default)]\r\n    pub cwds: u64,\r\n    /// Cumulative cwd-cache hit/miss census.\r\n    #[serde(default)]\r\n    pub cwd_cache_hits: u64,\r\n    #[serde(default)]\r\n    pub cwd_cache_misses: u64,\r\n    /// Cycles that FAILED to publish — every read until the next success is a\r\n    /// stale (last-known-good) read by contract.\r\n    #[serde(default)]\r\n    pub stale_reads: u64,\r\n    /// Publishes that REPLACED an unreadable / schema-mismatched index file\r\n    /// (the writer repairing a torn or foreign snapshot).\r\n    #[serde(default)]\r\n    pub repairs: u64,\r\n    /// The most recent cycle's complexity counters (the CI-gate surface).\r\n    #[serde(default)]\r\n    pub last_cycle: CycleCounters,\r\n    /// Lifetime totals (this brain generation).\r\n    #[serde(default)]\r\n    pub cumulative: CycleCounters,\r\n}\r\n\r\n/// The stats sidecar path, beside the index inside `projindex::index_dir()`.\r\npub fn stats_path() -> PathBuf {\r\n    projindex::index_dir().join(\"project-index-stats.json\")\r\n}\r\n\r\n/// Read the stats sidecar (tolerant: absent/corrupt → `None`) — the\r\n/// `spt daemon status` consumer half.\r\npub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\r\n    let raw = std::fs::read_to_string(path).ok()?;\r\n    serde_json::from_str(&raw).ok()\r\n}\r\n\r\n/// Every filesystem root the engine touches, injected so units run against\r\n/// plain temp dirs with zero env games.\r\n#[derive(Debug, Clone)]\r\npub struct WriterPaths {\r\n    pub owlery: PathBuf,\r\n    /// The context store's bare seed repo (`tracked/.seed.git`).\r\n    pub seed_git: PathBuf,\r\n    pub index_path: PathBuf,\r\n    pub stats_path: PathBuf,\r\n    pub invalidations_dir: PathBuf,\r\n}\r\n\r\nimpl WriterPaths {\r\n    /// The production layout under the live `$SPT_HOME`.\r\n    pub fn production() -> WriterPaths {\r\n        WriterPaths {\r\n            owlery: perch::owlery_dir(),\r\n            seed_git: contextstore::seed_git_dir(&perch::tracked_dir()),\r\n            index_path: projindex::index_path(),\r\n            stats_path: stats_path(),\r\n            invalidations_dir: projinval::invalidations_dir(),\r\n        }\r\n    }\r\n}\r\n\r\n/// One scanned branch: its tip at scan time + the endpoint ids holding\r\n/// `<id>/project-context.md` in that tip tree.\r\n#[derive(Debug, Clone)]\r\nstruct BranchScan {\r\n    tip: String,\r\n    members: BTreeSet<String>,\r\n}\r\n\r\n/// One cached cwd derivation, stamped with the repo-identity marker that\r\n/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\r\n/// for a linked worktree). Identity refreshes ONLY when the marker path or\r\n/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\r\n/// §3: commits do not alter project identity).\r\n#[derive(Debug, Clone)]\r\nstruct CwdDerivation {\r\n    id: String,\r\n    display: String,\r\n    marker: Option<PathBuf>,\r\n    stamp: Option<(u64, u64)>,\r\n}\r\n\r\n/// What one reconcile cycle did — the unit-gate surface.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct CycleReport {\r\n    pub counters: CycleCounters,\r\n    /// A new snapshot was written.\r\n    pub published: bool,\r\n    /// The cycle proved the store unchanged and exited without scanning,\r\n    /// deriving, or writing — the warm-start / no-op contract.\r\n    pub skipped_unchanged: bool,\r\n}\r\n\r\n/// The single-flight reconcile engine. ONE instance lives on ONE writer\r\n/// thread — single-flight is by construction, not by locking.\r\npub struct WriterEngine {\r\n    paths: WriterPaths,\r\n    /// The last published snapshot (in-memory copy; `None` until the first\r\n    /// load/publish).\r\n    current: Option<ProjectIndex>,\r\n    /// The fingerprint the published snapshot was derived from.\r\n    last_fingerprint: String,\r\n    /// branch → last scan (tips + membership), the ≤1-scan-per-changed-branch\r\n    /// cache. Brain-local: a brain restart re-scans (bounded by B) only when\r\n    /// the fingerprint moved or rows must be recomposed.\r\n    branch_cache: BTreeMap<String, BranchScan>,\r\n    /// `p-*` branches in recency order as of the last enumeration:\r\n    /// `(project_id, branch)`.\r\n    branch_order: Vec<(String, String)>,\r\n    /// normalized cwd → stamped derivation (the C term's cross-cycle cache).\r\n    cwd_cache: HashMap<String, CwdDerivation>,\r\n    /// Pending repair: the on-disk index was unreadable/foreign at load; the\r\n    /// next successful publish counts as a repair.\r\n    repair_pending: bool,\r\n    pub stats: IndexWriterStats,\r\n}\r\n\r\nimpl WriterEngine {\r\n    /// Build the engine and LOAD the persisted index (ADR-0037 §2: load at\r\n    /// startup, ready without warm — this constructor performs pure file I/O\r\n    /// and returns immediately; reconciling is the thread's job).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    pub fn new(paths: WriterPaths) -> WriterEngine {\r\n        let (current, repair_pending) = match projindex::read_index_at(&paths.index_path) {\r\n            IndexRead::Snapshot(idx) => (Some(idx), false),\r\n            IndexRead::Absent(AbsentReason::Missing) => (None, false),\r\n            // Torn/corrupt/foreign file present: readers are degrading to `-`;\r\n            // our next publish REPAIRS it (and is counted as such).\r\n            IndexRead::Absent(_) => (None, true),\r\n        };\r\n        let last_fingerprint = current\r\n            .as_ref()\r\n            .map(|i| i.source_generation.clone())\r\n            .unwrap_or_default();\r\n        let mut stats = IndexWriterStats {\r\n            generated_ms: current.as_ref().map(|i| i.generated_ms).unwrap_or(0),\r\n            source_generation: last_fingerprint.clone(),\r\n            pending_refresh: true, // the boot reconcile is queued by definition\r\n            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\r\n            ..IndexWriterStats::default()\r\n        };\r\n        stats.projects = current\r\n            .as_ref()\r\n            .map(|i| distinct_projects(i) as u64)\r\n            .unwrap_or(0);\r\n        WriterEngine {\r\n            paths,\r\n            current,\r\n            last_fingerprint,\r\n            branch_cache: BTreeMap::new(),\r\n            branch_order: Vec::new(),\r\n            cwd_cache: HashMap::new(),\r\n            repair_pending,\r\n            stats,\r\n        }\r\n    }\r\n\r\n    /// Run ONE reconcile cycle over the coalesced request. `force_full`\r\n    /// recomposes every endpoint even on an unchanged fingerprint (the\r\n    /// periodic backstop); a non-forced global request with an unchanged\r\n    /// fingerprint and no scoped work SKIPS everything — the warm-start\r\n    /// no-scan contract. Any error leaves the published index untouched\r\n    /// (last-known-good) and surfaces in `stats.last_error`.\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    pub fn reconcile(&mut self, req: &Coalesced, force_full: bool) -> Result<CycleReport, String> {\r\n        let started = std::time::Instant::now();\r\n        let now = now_ms();\r\n        let mut counters = CycleCounters::default();\r\n\r\n        // Explicit cwd invalidations (new session in a dir whose identity may\r\n        // have moved): drop those cache entries up front so the compose pass\r\n        // re-derives them exactly once.\r\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n        for cwd in &req.cwds {\r\n            self.cwd_cache.remove(&projderive::normalize_path(Path::new(cwd)));\r\n        }\r\n\r\n        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\r\n        let store = if self.paths.seed_git.exists() {\r\n            Some(\r\n                BranchStore::open_or_init(&self.paths.seed_git)\r\n                    .map_err(|e| format!(\"branch store open: {e}\"))?,\r\n            )\r\n        } else {\r\n            None // no context store yet — sessions/origin still index\r\n        };\r\n        let tips: Vec<(String, String)> = match &store {\r\n            Some(store) => {\r\n                counters.branch_enumerations += 1;\r\n                store\r\n                    .branch_tips_by_recency()\r\n                    .map_err(|e| format!(\"branch enumeration: {e}\"))?\r\n            }\r\n            None => Vec::new(),\r\n        };\r\n        let fingerprint = projindex::fingerprint_tips(&tips);\r\n        let fingerprint_changed = fingerprint != self.last_fingerprint;\r\n\r\n        // ── 2. The no-work exit: unchanged generation performs NO scan ───\r\n        if !fingerprint_changed\r\n            && !force_full\r\n            && req.endpoints.is_empty()\r\n            && req.cwds.is_empty()\r\n            && self.current.is_some()\r\n        {\r\n            let report = CycleReport { counters, published: false, skipped_unchanged: true };\r\n            self.finish_cycle(&report, now, started.elapsed());\r\n            return Ok(report);\r\n        }\r\n\r\n        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\r\n        self.branch_order = tips\r\n            .iter()\r\n            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\r\n            .collect();\r\n        if let (false, Some(store)) = (self.branch_order.is_empty(), &store) {\r\n            let live: BTreeSet<&str> =\r\n                self.branch_order.iter().map(|(_, b)| b.as_str()).collect();\r\n            self.branch_cache.retain(|b, _| live.contains(b.as_str()));\r\n            for (branch, tip) in tips.iter().filter(|(b, _)| b.starts_with(\"p-\")) {\r\n                let cached = self.branch_cache.get(branch);\r\n                if cached.map(|c| &c.tip) == Some(tip) {\r\n                    continue; // unchanged tip → cached membership, no scan\r\n                }\r\n                let files = store\r\n                    .ls_files_at(tip)\r\n                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\r\n                counters.tree_scans += 1;\r\n                let members = files\r\n                    .iter()\r\n                    .filter_map(|f| {\r\n                        let (id, rest) = f.split_once('/')?;\r\n                        (rest == PROJECT_CONTEXT_FILE).then(|| id.to_string())\r\n                    })\r\n                    .collect();\r\n                self.branch_cache\r\n                    .insert(branch.clone(), BranchScan { tip: tip.clone(), members });\r\n            }\r\n        } else {\r\n            self.branch_cache.clear();\r\n        }\r\n\r\n        // ── 4. Which endpoints recompose ──────────────────────────────────\r\n        // A membership change can touch ANY endpoint, so a changed fingerprint\r\n        // (or the backstop / a missing snapshot) recomposes the full roster;\r\n        // otherwise only the scoped rows are patched.\r\n        let full = force_full || fingerprint_changed || self.current.is_none();\r\n        let roster = enumerate_perches(&self.paths.owlery);\r\n        let targets: Vec<(String, PathBuf, Option<String>)> = if full {\r\n            roster\r\n        } else {\r\n            let by_id: HashMap<String, (PathBuf, Option<String>)> = roster\r\n                .into_iter()\r\n                .map(|(id, path, cwd)| (id, (path, cwd)))\r\n                .collect();\r\n            req.endpoints\r\n                .iter()\r\n                .filter_map(|id| {\r\n                    by_id.get(id).map(|(p, c)| (id.clone(), p.clone(), c.clone()))\r\n                })\r\n                .collect()\r\n        };\r\n\r\n        // ── 5. Compose rows through the SHARED precedence kernel ─────────\r\n        // (parity by construction — ADR-0037 §4). The derive closure is the\r\n        // cwd cache: one real derivation per distinct normalized cwd.\r\n        let cache = std::cell::RefCell::new(&mut self.cwd_cache);\r\n        let hits = std::cell::Cell::new(0u64);\r\n        let misses = std::cell::Cell::new(0u64);\r\n        let derivations = std::cell::Cell::new(0u64);\r\n        let derive = |dir: &Path| -> (String, String) {\r\n            let key = projderive::normalize_path(dir);\r\n            let marker = discover_git_marker(dir);\r\n            let stamp = marker.as_deref().and_then(stamp_of);\r\n            let mut cache = cache.borrow_mut();\r\n            if let Some(hit) = cache.get(&key) {\r\n                if hit.marker == marker && hit.stamp == stamp {\r\n                    hits.set(hits.get() + 1);\r\n                    return (hit.id.clone(), hit.display.clone());\r\n                }\r\n            }\r\n            misses.set(misses.get() + 1);\r\n            derivations.set(derivations.get() + 1);\r\n            let (id, display) = spt_store::project::project_id_and_display_for_dir(dir);\r\n            cache.insert(\r\n                key,\r\n                CwdDerivation { id: id.clone(), display: display.clone(), marker, stamp },\r\n            );\r\n            (id, display)\r\n        };\r\n\r\n        let mut endpoints: BTreeMap<String, EndpointProject> = if full {\r\n            BTreeMap::new()\r\n        } else {\r\n            self.current.as_ref().map(|i| i.endpoints.clone()).unwrap_or_default()\r\n        };\r\n        if !full {\r\n            // A scoped id whose perch vanished (purge) drops its row — the\r\n            // roster is the authority; a stale attribution never survives it.\r\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n            let live: BTreeSet<&String> = targets.iter().map(|(id, _, _)| id).collect();\r\n            for id in &req.endpoints {\r\n                if !live.contains(id) {\r\n                    endpoints.remove(id);\r\n                }\r\n            }\r\n        }\r\n        for (id, perch_path, origin_cwd) in &targets {\r\n            let entries = sessions::read_all(perch_path);\r\n            let store_ids: Vec<String> = self\r\n                .branch_order\r\n                .iter()\r\n                .filter(|(_, branch)| {\r\n                    self.branch_cache\r\n                        .get(branch)\r\n                        .is_some_and(|scan| scan.members.contains(id))\r\n                })\r\n                .map(|(project, _)| project.clone())\r\n                .collect();\r\n            let refs = projderive::project_refs_from(\r\n                &entries,\r\n                origin_cwd.as_deref(),\r\n                store_ids,\r\n                &self.paths.owlery,\r\n                derive, // captures only shared refs → Copy; each call reuses one closure\r\n            );\r\n            // The FULL precedence-ordered history is persisted (W3 additive\r\n            // `refs`, REQ-PICKER-PROJECT-HISTORY-TRUTH: the picker projects\r\n            // history, not head); the head fields stay beside it for W2-shaped\r\n            // consumers and the status surface.\r\n            // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\r\n            let head = refs.first().cloned();\r\n            let ref_entries: Vec<projindex::ProjectRefEntry> = refs\r\n                .into_iter()\r\n                .map(|r| projindex::ProjectRefEntry {\r\n                    id: r.id,\r\n                    dir: r.dir,\r\n                    display: r.display,\r\n                    source: Some(r.source.as_str().to_string()),\r\n                })\r\n                .collect();\r\n            endpoints.insert(\r\n                id.clone(),\r\n                EndpointProject {\r\n                    project_id: head.as_ref().map(|h| h.id.clone()),\r\n                    display: head.as_ref().map(|h| h.display.clone()),\r\n                    source: head.as_ref().map(|h| h.source.as_str().to_string()),\r\n                    updated_ms: now,\r\n                    refs: ref_entries,\r\n                },\r\n            );\r\n        }\r\n        counters.derivations += derivations.get();\r\n        self.stats.cwd_cache_hits += hits.get();\r\n        self.stats.cwd_cache_misses += misses.get();\r\n\r\n        // The persisted cwd map (W3 additive `cwds` — the resume pane's\r\n        // per-session-cwd source, REQ-RESUME-ROW-PER-PROJECT without git): a\r\n        // FULL cycle publishes exactly the post-compose cache (fresh + pruned\r\n        // of dead cwds); a scoped patch merges the cycle's cache over the\r\n        // previous snapshot so rows this cycle did not visit keep their\r\n        // entries (a restarted brain's cache starts empty).\r\n        // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\r\n        let mut cwds: BTreeMap<String, projindex::CwdProject> = if full {\r\n            BTreeMap::new()\r\n        } else {\r\n            self.current.as_ref().map(|i| i.cwds.clone()).unwrap_or_default()\r\n        };\r\n        for (norm, entry) in self.cwd_cache.iter() {\r\n            if !entry.id.is_empty() {\r\n                cwds.insert(\r\n                    norm.clone(),\r\n                    projindex::CwdProject { id: entry.id.clone(), display: entry.display.clone() },\r\n                );\r\n            }\r\n        }\r\n\r\n        // ── 6. Atomic publish; last-known-good on failure ─────────────────\r\n        let index = ProjectIndex {\r\n            schema: projindex::PROJECT_INDEX_SCHEMA,\r\n            generated_ms: now,\r\n            source_generation: fingerprint.clone(),\r\n            endpoints,\r\n            cwds,\r\n        };\r\n        projindex::write_index_at(&self.paths.index_path, &index)\r\n            .map_err(|e| format!(\"publish: {e}\"))?;\r\n        if self.repair_pending {\r\n            self.stats.repairs += 1;\r\n            self.repair_pending = false;\r\n        }\r\n        self.last_fingerprint = fingerprint;\r\n        self.stats.generated_ms = now;\r\n        self.stats.source_generation = index.source_generation.clone();\r\n        self.stats.endpoints = index.endpoints.len() as u64;\r\n        self.stats.projects = distinct_projects(&index) as u64;\r\n        self.current = Some(index);\r\n\r\n        let report = CycleReport { counters, published: true, skipped_unchanged: false };\r\n        self.finish_cycle(&report, now, started.elapsed());\r\n        Ok(report)\r\n    }\r\n\r\n    /// Book a FAILED cycle: the published snapshot is untouched\r\n    /// (last-known-good) and every read until the next success is a stale\r\n    /// read by contract.\r\n    pub fn record_failure(&mut self, error: String) {\r\n        self.stats.last_error = Some(error);\r\n        self.stats.stale_reads += 1;\r\n        self.stats.last_run_ms = now_ms();\r\n        self.stats.pending_refresh = true; // the work is still owed\r\n        self.write_stats();\r\n    }\r\n\r\n    fn finish_cycle(&mut self, report: &CycleReport, now: u64, took: Duration) {\r\n        report.counters.add_to(&mut self.stats.cumulative);\r\n        self.stats.last_cycle = report.counters.clone();\r\n        self.stats.last_run_ms = now;\r\n        self.stats.last_duration_ms = took.as_millis() as u64;\r\n        self.stats.last_error = None;\r\n        self.stats.pending_refresh = false;\r\n        self.stats.cwds = self.cwd_cache.len() as u64;\r\n        self.write_stats();\r\n    }\r\n\r\n    /// Publish the observability sidecar (atomic, best-effort — stats must\r\n    /// never fail the index itself).\r\n    pub fn write_stats(&self) {\r\n        if let Some(dir) = self.paths.stats_path.parent() {\r\n            let _ = std::fs::create_dir_all(dir);\r\n        }\r\n        if let Ok(body) = serde_json::to_string(&self.stats) {\r\n            let _ = spt_store::atomic::atomic_write_string(&self.paths.stats_path, &body);\r\n        }\r\n    }\r\n}\r\n\r\nfn distinct_projects(index: &ProjectIndex) -> usize {\r\n    index\r\n        .endpoints\r\n        .values()\r\n        .filter_map(|e| e.project_id.as_deref())\r\n        .collect::<BTreeSet<_>>()\r\n        .len()\r\n}\r\n\r\n/// Enumerate the flat perch roster: every top-level owlery dir with a\r\n/// PARSEABLE `info.json` → `(id, perch_path, origin_cwd)`. Mirrors the\r\n/// list/picker roster semantics (a corrupt record renders `-` there and has\r\n/// no derivable attribution here; nested psyche perches are not list rows).\r\nfn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\r\n    let Ok(entries) = std::fs::read_dir(owlery) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out = Vec::new();\r\n    for entry in entries.flatten() {\r\n        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {\r\n            continue;\r\n        }\r\n        let path = entry.path();\r\n        if let Some(rec) = info::read_info(&path) {\r\n            out.push((rec.id, path, rec.cwd));\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n/// The repo-identity marker for a dir: the nearest ancestor's `.git/config`\r\n/// (a normal checkout) or the `.git` gitfile itself (a linked worktree — the\r\n/// file's content moves when the repo pointer moves). Pure filesystem walk,\r\n/// NO git spawn — this is what lets cache validity be checked per cycle for\r\n/// free.\r\nfn discover_git_marker(dir: &Path) -> Option<PathBuf> {\r\n    for anc in dir.ancestors() {\r\n        let g = anc.join(\".git\");\r\n        if g.is_dir() {\r\n            return Some(g.join(\"config\"));\r\n        }\r\n        if g.is_file() {\r\n            return Some(g);\r\n        }\r\n    }\r\n    None\r\n}\r\n\r\n/// (mtime epoch-ms, len) of a marker file — the cheap identity stamp.\r\nfn stamp_of(p: &Path) -> Option<(u64, u64)> {\r\n    let meta = std::fs::metadata(p).ok()?;\r\n    let mtime = meta\r\n        .modified()\r\n        .ok()?\r\n        .duration_since(std::time::UNIX_EPOCH)\r\n        .ok()?\r\n        .as_millis() as u64;\r\n    Some((mtime, meta.len()))\r\n}\r\n\r\nfn env_ms(name: &str, default: u64) -> u64 {\r\n    std::env::var(name)\r\n        .ok()\r\n        .and_then(|v| v.parse().ok())\r\n        .unwrap_or(default)\r\n}\r\n\r\n/// Spawn the brain's index-writer host: ONE thread (single-flight by\r\n/// construction) that loads the persisted index, runs the boot reconcile\r\n/// (fingerprint-gated: a warm start with an unchanged generation scans\r\n/// nothing), then drains + debounces + coalesces the invalidation queue and\r\n/// runs the periodic full backstop until `stop`. Mirrors\r\n/// `livehost::spawn_live_host`; the thread dies with the brain and the next\r\n/// brain rehydrates from the persisted index (ready without warm).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\r\n        // untouched): rigs that need a writer-silent daemon set it — the\r\n        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\r\n        if std::env::var_os(\"SPT_PROJINDEX_DISABLE\").is_some() {\r\n            return;\r\n        }\r\n        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\r\n        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\r\n        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\r\n\r\n        let mut engine = WriterEngine::new(WriterPaths::production());\r\n        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\r\n\r\n        // Boot reconcile: global but NOT forced — a warm start over an\r\n        // unchanged store exits without a scan (the cold/warm start gate).\r\n        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n        let boot = Coalesced { global: true, ..boot };\r\n        if let Err(e) = engine.reconcile(&boot, false) {\r\n            engine.record_failure(e);\r\n        }\r\n\r\n        let mut since_periodic = Duration::ZERO;\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // Sleep one poll slice (sliced so a stop lands promptly).\r\n            let mut left = poll.as_millis() as u64;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n                left -= step;\r\n            }\r\n            if stop.load(Ordering::SeqCst) {\r\n                break;\r\n            }\r\n            since_periodic += poll;\r\n\r\n            let due_periodic = since_periodic >= periodic;\r\n            let has_events = projinval::pending_at(&engine.paths.invalidations_dir);\r\n            if !has_events && !due_periodic {\r\n                continue;\r\n            }\r\n\r\n            if has_events {\r\n                // Debounce: let the burst land, then drain it ALL — the\r\n                // coalescing window (multiple invalidations → one refresh).\r\n                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n                std::thread::sleep(debounce);\r\n                engine.stats.pending_refresh = true;\r\n                engine.write_stats();\r\n            }\r\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n            if due_periodic {\r\n                since_periodic = Duration::ZERO;\r\n            }\r\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\r\n            if req.is_empty() && !due_periodic {\r\n                continue; // a racer consumed it (never happens in production)\r\n            }\r\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\r\n                engine.record_failure(e);\r\n            }\r\n        }\r\n    })\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use spt_store::contextstore::ContextStore;\r\n    use spt_store::projinval::InvalScope;\r\n    use std::time::Instant;\r\n    use tempfile::TempDir;\r\n\r\n    /// A self-contained home-shaped fixture tree (NO env, NO real $SPT_HOME):\r\n    /// owlery + tracked context store + index paths, all under one TempDir.\r\n    struct Fixture {\r\n        _tmp: TempDir,\r\n        home: PathBuf,\r\n        paths: WriterPaths,\r\n    }\r\n\r\n    impl Fixture {\r\n        fn new() -> Fixture {\r\n            let tmp = TempDir::new().unwrap();\r\n            let home = tmp.path().to_path_buf();\r\n            let paths = WriterPaths {\r\n                owlery: home.join(\"owlery\"),\r\n                seed_git: contextstore::seed_git_dir(&home.join(\"tracked\")),\r\n                index_path: home.join(\"index\").join(\"project-index.json\"),\r\n                stats_path: home.join(\"index\").join(\"project-index-stats.json\"),\r\n                invalidations_dir: home.join(\"index\").join(\"invalidations\"),\r\n            };\r\n            std::fs::create_dir_all(&paths.owlery).unwrap();\r\n            Fixture { _tmp: tmp, home, paths }\r\n        }\r\n\r\n        fn store(&self) -> ContextStore {\r\n            ContextStore::open_or_init_in(&self.home.join(\"tracked\")).unwrap()\r\n        }\r\n\r\n        /// A perch with an optional origin cwd and optional session-ledger cwd.\r\n        fn perch(&self, id: &str, origin_cwd: Option<&Path>, session_cwd: Option<&Path>) {\r\n            let perch = self.paths.owlery.join(id);\r\n            std::fs::create_dir_all(&perch).unwrap();\r\n            let mut rec = info::InfoJson::new(id, \"t\", 0, &format!(\"sid-{id}\"), \"live_agent\");\r\n            rec.cwd = origin_cwd.map(|p| p.to_string_lossy().to_string());\r\n            info::write_info(&perch, &rec).unwrap();\r\n            if let Some(cwd) = session_cwd {\r\n                sessions::append(\r\n                    &perch,\r\n                    &sessions::SessionEntry {\r\n                        ts: \"2026-07-15T00:00:00Z\".to_string(),\r\n                        session_id: format!(\"s-{id}\"),\r\n                        trigger: sessions::SessionTrigger::Boot,\r\n                        cwd: Some(cwd.to_string_lossy().to_string()),\r\n                        ordinal: None,\r\n                        adapter: None,\r\n                    },\r\n                )\r\n                .unwrap();\r\n            }\r\n        }\r\n\r\n        /// Give `id` committed context in `project` (a p-* membership row).\r\n        fn membership(&self, project: &str, id: &str) {\r\n            let cs = self.store();\r\n            let file = cs.project_context_path(project, id).unwrap();\r\n            std::fs::write(&file, format!(\"{id} in {project}\")).unwrap();\r\n            cs.commit_project(project, &format!(\"{id} slice\")).unwrap().unwrap();\r\n        }\r\n\r\n        /// A plain (non-git) project dir whose folder name becomes the id.\r\n        fn project_dir(&self, name: &str) -> PathBuf {\r\n            let dir = self.home.join(\"workdirs\").join(name);\r\n            std::fs::create_dir_all(&dir).unwrap();\r\n            dir\r\n        }\r\n    }\r\n\r\n    fn global() -> Coalesced {\r\n        Coalesced { global: true, ..Coalesced::default() }\r\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,null,1269]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\projwriter.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1270,"totalBytes":33179,"outputLines":763,"outputBytes":33179,"shownRange":{"start":1,"end":763},"nextOffset":764}}},"isError":false,"timestamp":1784271471854}}
{"type":"message","id":"79a8d47c","parentId":"f9b55e17","timestamp":"2026-07-17T06:57:51.875Z","message":{"role":"toolResult","toolCallId":"call_a35oJTi3WN1ZNuKkwMbMlJcX|fc_038f28d3e93bbfd4016a59d26fb9308196b94fd57c01646b06","toolName":"read","content":[{"type":"text","text":"\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\n/// holding the connection and probing broker liveness each heartbeat. A dropped\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\n/// from the (possibly swapped) binary.\n///\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\n/// and start them exactly once, on the first `enabled` reply. That single gate\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\n/// NetHost bind in the background and `net-status` flips `enabled` when it\n/// attaches, at which point the brain starts the consumers — no extra signal.\n///\n/// It inherits the broker's already-unelevated token as a plain child, so it\n/// runs no de-elevation guard of its own.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\n    let _ = crate::stderrlog::install(\"brain\", generation);\n    let name = broker_socket_name();\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\n    // *counter* is rehydrated, never the wall-clock stamp.\n    eprintln!(\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\n        std::process::id(),\n        reason.as_arg(),\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\n/// the brain child (restoration D2-1):** both are standalone IPC clients\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\n/// restartable brain. Each mints its own registry + stop flag and connects\n/// independently; when the brain process exits these in-process threads die with\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\n/// `net-status` reply rather than the broker's identity load.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n// [impl->REQ-DAEMON-9]\nfn spawn_net_consumers(node_hex: &str) {\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\n    let stop = Arc::new(AtomicBool::new(false));\n    let _ = crate::dispatch::spawn_dispatcher(\n        broker_socket_name(),\n        Arc::clone(&registry),\n        Arc::clone(&stop),\n    );\n    let cfg = crate::config::DaemonConfig::load();\n    if cfg.peer_pump {\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n    }\n}\n\n/// Connect to the broker, retrying briefly while it finishes binding.\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n…\n}\n\n[Showing lines 137-336 of 2116. Use :337 to continue]"}],"details":{"truncation":{"content":"\r\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\r\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\r\n/// holding the connection and probing broker liveness each heartbeat. A dropped\r\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\r\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\r\n/// from the (possibly swapped) binary.\r\n///\r\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\r\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\r\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\r\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\r\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\r\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\r\n/// and start them exactly once, on the first `enabled` reply. That single gate\r\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\r\n/// NetHost bind in the background and `net-status` flips `enabled` when it\r\n/// attaches, at which point the brain starts the consumers — no extra signal.\r\n///\r\n/// It inherits the broker's already-unelevated token as a plain child, so it\r\n/// runs no de-elevation guard of its own.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\r\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\r\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\r\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\r\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\r\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\r\n    let _ = crate::stderrlog::install(\"brain\", generation);\r\n    let name = broker_socket_name();\r\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\r\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\r\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\r\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\r\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\r\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\r\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\r\n    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r\n\r\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\r\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\r\n/// the brain child (restoration D2-1):** both are standalone IPC clients\r\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\r\n/// restartable brain. Each mints its own registry + stop flag and connects\r\n/// independently; when the brain process exits these in-process threads die with\r\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\r\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\r\n/// `net-status` reply rather than the broker's identity load.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [impl->REQ-DAEMON-9]\r\nfn spawn_net_consumers(node_hex: &str) {\r\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let _ = crate::dispatch::spawn_dispatcher(\r\n        broker_socket_name(),\r\n        Arc::clone(&registry),\r\n        Arc::clone(&stop),\r\n    );\r\n    let cfg = crate::config::DaemonConfig::load();\r\n    if cfg.peer_pump {\r\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\r\n    }\r\n}\r\n\r\n/// Connect to the broker, retrying briefly while it finishes binding.\r\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\r","truncated":true,"truncatedBy":"lines","totalLines":1980,"totalBytes":12291,"outputLines":200,"outputBytes":12291,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n/// The `spt daemon brain` child entry. Connect to the broker over the existing\r\n/// socket IPC, signal readiness, host the brain-side logic loops, and idle —\r\n/// holding the connection and probing broker liveness each heartbeat. A dropped\r\n/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)\r\n/// returns `Err`, exiting the process so the broker-side supervisor respawns it\r\n/// from the (possibly swapped) binary.\r\n///\r\n/// **D2-1 — net consumers run here.** The inbound dispatcher + outbound peer\r\n/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no\r\n/// shared `Arc<Broker>`), so they belong with the restartable brain, not the\r\n/// broker — a brain respawn re-spawns them from the swapped binary. They are\r\n/// meaningful only once the broker-owned NetHost is up, so we gate on the\r\n/// existing `net-status` verb (which also serves as the broker-liveness probe)\r\n/// and start them exactly once, on the first `enabled` reply. That single gate\r\n/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the\r\n/// NetHost bind in the background and `net-status` flips `enabled` when it\r\n/// attaches, at which point the brain starts the consumers — no extra signal.\r\n///\r\n/// It inherits the broker's already-unelevated token as a plain child, so it\r\n/// runs no de-elevation guard of its own.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\r\n    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\r\n    // (REQ-DAEMON-STDERR-PERSIST) — a detached brain's stdio is /dev/null, so every\r\n    // diagnostic below would otherwise vanish (the incident-night RCA-blind gap). Runs\r\n    // FIRST so even the BRAIN_UP line and a connect failure are captured. Best-effort.\r\n    // [impl->REQ-DAEMON-STDERR-PERSIST]\r\n    let _ = crate::stderrlog::install(\"brain\", generation);\r\n    let name = broker_socket_name();\r\n    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;\r\n    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the\r\n    // generation is broker-owned custody of the KH-2.4 counter (no longer the\r\n    // brain→brain BrainState.generation, which retires in D4), and the reason is\r\n    // Q4's update-vs-crash discriminator that D5 will use to decide whether to\r\n    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5\r\n    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the\r\n    // *counter* is rehydrated, never the wall-clock stamp.\r\n    eprintln!(\r\n        \"BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})\",\r\n        std::process::id(),\r\n        reason.as_arg(),\r\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r\n\r\n/// Spawn the net consumers — the inbound dispatcher + the outbound peer pump\r\n/// (D9-1) — against the broker's socket. **Relocated from the broker process to\r\n/// the brain child (restoration D2-1):** both are standalone IPC clients\r\n/// (`Brain::cold_start`, no shared `Arc<Broker>`), so they belong with the\r\n/// restartable brain. Each mints its own registry + stop flag and connects\r\n/// independently; when the brain process exits these in-process threads die with\r\n/// it, and the respawned brain re-spawns them from the current binary. Mirrors\r\n/// the wiring the broker boot used to do, `node_hex` now sourced from the\r\n/// `net-status` reply rather than the broker's identity load.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n// [impl->REQ-DAEMON-9]\r\nfn spawn_net_consumers(node_hex: &str) {\r\n    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));\r\n    let stop = Arc::new(AtomicBool::new(false));\r\n    let _ = crate::dispatch::spawn_dispatcher(\r\n        broker_socket_name(),\r\n        Arc::clone(&registry),\r\n        Arc::clone(&stop),\r\n    );\r\n    let cfg = crate::config::DaemonConfig::load();\r\n    if cfg.peer_pump {\r\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\r\n    }\r\n}\r\n\r\n/// Connect to the broker, retrying briefly while it finishes binding.\r\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\r\n…\n}","startLine":137,"lineNumbers":[137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,null,348]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":12291,"outputLines":200,"outputBytes":12291,"shownRange":{"start":137,"end":336},"nextOffset":337}}},"isError":false,"timestamp":1784271471874}}
{"type":"message","id":"5c008982","parentId":"79a8d47c","timestamp":"2026-07-17T06:57:52.662Z","message":{"role":"toolResult","toolCallId":"call_asm4vxNyBXv17hpiuthIHEyB|fc_038f28d3e93bbfd4016a59d26fb95c81968de472c46d8100f6","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n\n### src/\n#### brainproc.rs\n 247|\n*248|    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n 249|    // brain hosts the SOLE single-flight writer — load persisted index, boot\n 250|    // reconcile in the background (ready-without-warm is preserved because\n 251|    // write_ready already fired above), then drain/debounce/coalesce the\n...\n 253|    // held for symmetry, the thread dies with the brain, and the next brain\n*254|    // rehydrates from the persisted index (warm start, fingerprint-gated).\n 255|    // [impl->REQ-PROJECT-INDEX-WRITER]\n 256|    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n 257|\n#### broker.rs\n 5328|    // payload's HEAD prefix must appear in the PTY echo. A swallowed head (only a tail\n*5329|    // echoes) FAILS verify — the exact head-loss fingerprint. An empty payload (a\n 5330|    // keys-only sequence, nothing echoable) is vacuously verified. RED-FIRST: make\n 5331|    // `echo_verified` always return true → the swallowed-head assert flips.\n 5332|    #[test]\n#### projwriter.rs\n*1|//! The materialized project-index WRITER — the daemon's derived-state\n 2|//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\n 3|//!\n 4|//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\n...\n 18|//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n*19|//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n*20|//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n 21|//!   scan;\n 22|//! - maintains an observability sidecar (`project-index-stats.json`) — index\n 23|//!   presence alone is not health: generated time, source generation, pending\n...\n 42|use spt_store::projderive;\n*43|use spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\n 44|use spt_store::projinval::{self, Coalesced};\n 45|use spt_store::sessions;\n 46|\n...\n 83|#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n*84|pub struct IndexWriterStats {\n 85|    /// When the current snapshot was generated (epoch ms; 0 = never).\n 86|    #[serde(default)]\n 87|    pub generated_ms: u64,\n*88|    /// The published snapshot's branch-tip fingerprint.\n 89|    #[serde(default)]\n 90|    pub source_generation: String,\n 91|    /// Whether a refresh is queued/underway right now.\n...\n 139|/// `spt daemon status` consumer half.\n*140|pub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n 141|    let raw = std::fs::read_to_string(path).ok()?;\n 142|    serde_json::from_str(&raw).ok()\n 143|}\n...\n 207|    /// load/publish).\n*208|    current: Option<ProjectIndex>,\n*209|    /// The fingerprint the published snapshot was derived from.\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 216|    /// `(project_id, branch)`.\n...\n 222|    repair_pending: bool,\n*223|    pub stats: IndexWriterStats,\n 224|}\n 225|\n 226|impl WriterEngine {\n...\n 238|        };\n*239|        let last_fingerprint = current\n 240|            .as_ref()\n 241|            .map(|i| i.source_generation.clone())\n 242|            .unwrap_or_default();\n*243|        let mut stats = IndexWriterStats {\n 244|            generated_ms: current.as_ref().map(|i| i.generated_ms).unwrap_or(0),\n*245|            source_generation: last_fingerprint.clone(),\n 246|            pending_refresh: true, // the boot reconcile is queued by definition\n 247|            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n*248|            ..IndexWriterStats::default()\n 249|        };\n 250|        stats.projects = current\n 251|            .as_ref()\n...\n 256|            current,\n*257|            last_fingerprint,\n 258|            branch_cache: BTreeMap::new(),\n 259|            branch_order: Vec::new(),\n 260|            cwd_cache: HashMap::new(),\n...\n 266|    /// Run ONE reconcile cycle over the coalesced request. `force_full`\n*267|    /// recomposes every endpoint even on an unchanged fingerprint (the\n 268|    /// periodic backstop); a non-forced global request with an unchanged\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...\n 285|\n*286|        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\n 287|        let store = if self.paths.seed_git.exists() {\n 288|            Some(\n 289|                BranchStore::open_or_init(&self.paths.seed_git)\n#### sync.rs\n 447|                            // Nudge ONE global refresh per pull that applied any\n*448|                            // p-* branch; the fingerprint gates what actually\n 449|                            // rescans, and the debounce coalesces a multi-peer\n 450|                            // pull burst. Without this, readers (index-only\n 451|                            // since W3) would lag a sync by up to PERIODIC_MS.\n\n#### pump/\n##### health.rs\n 5|//! membership). This file is the milestone's acceptance surface: the incident\n*6|//! fingerprint (all dials failing + heartbeat fresh + net_up true) MUST\n 7|//! render degraded, and health returns to green only on REAL peer progress —\n 8|//! never on heartbeat, never on elapsed time.\n 9|//!\n...\n 36|    /// Epoch ms since when EVERY fan-out target has been failing (the\n*37|    /// incident fingerprint). Cleared by any real peer progress.\n 38|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 39|    pub all_fail_since_ms: Option<u64>,\n 40|    /// Currently-failing peers → the STAGE their last failure died in\n...\n 60|    Connecting,\n*61|    /// EVERY target failing — the sequester fingerprint. Names the failing\n 62|    /// count and the dominant stage so the operator reads WHERE it dies.\n 63|    Degraded {\n 64|        failing: usize,\n...\n 225|    // [unit->REQ-PUMP-STAGE-TRUTH] the health state machine renders the\n*226|    // INCIDENT FINGERPRINT degraded: every target failing, nothing live —\n 227|    // exactly the state the 2026-07-10 sequester sat in while net_up,\n 228|    // heartbeat, and counts all stayed green. And it becomes healthy ONLY on\n 229|    // real peer progress: more failures, elapsed time, or a heartbeat never\n...\n 237|        assert!(h.note_failed(\"a\", \"quic-connect\", 0, 2_000), \"new failure logs\");\n*238|        assert_eq!(h.verdict(), HealthVerdict::Connecting, \"one of two failing: not yet the fingerprint\");\n 239|\n 240|        assert!(h.note_failed(\"b\", \"quic-connect\", 0, 3_000));\n 241|        match h.verdict() {\n\n### tests/\n#### mesh_recovery.rs\n 342|// stage — WHILE the pump heartbeat stays fresh (the exact incident\n*343|// fingerprint: green liveness, dead reachability); when discovery restores a\n 344|// real route and a real connect lands, health flips healthy — on progress,\n 345|// never on time.\n 346|#[test]\n...\n 378|    // Degraded, naming the stage — while the heartbeat is FRESH (the\n*379|    // fingerprint that made the field incident invisible).\n 380|    converge(\"health degrades during the outage\", || {\n 381|        let h = PumpHealth::load_from(&health_path);\n 382|        matches!(\n\n## spt-store/src/\n### branchstore.rs\n 258|    /// Every local branch with its tip commit id, ordered **newest→oldest by\n*259|    /// tip committer date** — the index writer's ONE branch enumeration\n*260|    /// (ADR-0037 §2: recency order AND the tip fingerprint substrate in a\n 261|    /// single `for-each-ref`, so \"enumerate branches once\" is one git spawn,\n 262|    /// not one per branch).\n 263|    // [impl->REQ-PROJECT-INDEX-WRITER]\n...\n 922|    // branch_tips_by_recency returns the SAME order as branches_by_recency and\n*923|    // each entry's tip matches tip() — recency + fingerprint substrate in one\n 924|    // for-each-ref call.\n 925|    #[test]\n 926|    fn branch_tips_by_recency_matches_order_and_tips() {\n### projderive.rs\n 6|//! Lifted verbatim from the picker's `project_refs_from` (PROJECT-INDEX W2) so\n*7|//! the daemon's materialized-index writer and the legacy synchronous reader\n 8|//! (until the W3 cutover) share one kernel — parity by construction, not by\n 9|//! parallel maintenance. The picker wraps [`project_refs_from`] and drops the\n 10|//! [`SourceLeg`]; the writer persists it as `EndpointProject.source`\n### projindex.rs\n 45|/// so discarding a mismatched one loses nothing durable.)\n*46|pub const PROJECT_INDEX_SCHEMA: u32 = 1;\n 47|\n 48|/// `$SPT_HOME/index/` — derived, node-local, regenerable materialized indexes.\n 49|/// Never synced, never tracked; safe to delete wholesale (the daemon writer\n...\n 129|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n*130|pub struct ProjectIndex {\n*131|    /// [`PROJECT_INDEX_SCHEMA`] at write time — the read gate.\n 132|    pub schema: u32,\n 133|    /// When the writer generated this snapshot (epoch ms).\n 134|    #[serde(default)]\n 135|    pub generated_ms: u64,\n*136|    /// Branch-tip fingerprint of the source context store at generation time\n 137|    /// (ADR-0037 §3 — no authoritative commit path exists, so freshness keys\n*138|    /// on fingerprints; an unchanged fingerprint means no rescan).\n 139|    #[serde(default)]\n 140|    pub source_generation: String,\n 141|    /// endpoint id → attribution.\n...\n 152|\n*153|impl ProjectIndex {\n 154|    /// The materialized project for a working directory, looked up on the\n 155|    /// normalized spelling — the resume pane's per-row source (never a\n 156|    /// derivation).\n...\n 162|\n*163|impl ProjectIndex {\n 164|    /// An empty schema-current index (the writer's cold-start seed).\n 165|    pub fn empty(generated_ms: u64) -> Self {\n*166|        ProjectIndex {\n*167|            schema: PROJECT_INDEX_SCHEMA,\n 168|            generated_ms,\n 169|            source_generation: String::new(),\n 170|            endpoints: BTreeMap::new(),\n...\n 175|\n*176|/// Fold a branch-tip enumeration into the `source_generation` fingerprint\n 177|/// (ADR-0037 §3: no authoritative context-store commit path exists, so\n*178|/// freshness keys on branch-tip fingerprints — a writer-maintained generation\n 179|/// counter is REJECTED because nothing maintains it). Order-insensitive\n 180|/// (sorted before hashing) so two enumerations of one store state — whatever\n*181|/// their recency order — fingerprint identically; an unchanged fingerprint\n 182|/// means NO rescan. Only `p-*` branches participate: project membership is\n 183|/// what the index derives, and an `a-*` live-context commit must not force a\n 184|/// membership rescan.\n 185|// [impl->REQ-PROJECT-INDEX-WRITER]\n*186|pub fn fingerprint_tips(tips: &[(String, String)]) -> String {\n 187|    use sha2::{Digest, Sha256};\n 188|    let mut lines: Vec<String> = tips\n 189|        .iter()\n...\n 221|pub enum IndexRead {\n*222|    Snapshot(ProjectIndex),\n 223|    Absent(AbsentReason),\n 224|}\n 225|\n...\n 258|    match found {\n*259|        Some(s) if s == u64::from(PROJECT_INDEX_SCHEMA) => {}\n 260|        Some(s) => {\n 261|            return IndexRead::Absent(AbsentReason::SchemaMismatch { found: s as u32 });\n 262|        }\n...\n 268|    }\n*269|    match serde_json::from_value::<ProjectIndex>(value) {\n 270|        Ok(idx) => IndexRead::Snapshot(idx),\n 271|        Err(e) => IndexRead::Absent(AbsentReason::Unreadable(e.to_string())),\n 272|    }\n...\n 285|// [impl->REQ-PROJECT-INDEX-STORE]\n*286|pub fn write_index_at(path: &Path, index: &ProjectIndex) -> std::io::Result<()> {\n 287|    if let Some(dir) = path.parent() {\n 288|        std::fs::create_dir_all(dir)?;\n 289|    }\n...\n 295|/// [`write_index_at`] against the canonical [`index_path`].\n*296|pub fn write_index(index: &ProjectIndex) -> std::io::Result<()> {\n 297|    write_index_at(&index_path(), index)\n 298|}\n 299|\n...\n 329|\n*330|    fn sample_index() -> ProjectIndex {\n 331|        let mut endpoints = BTreeMap::new();\n 332|        endpoints.insert(\n 333|            \"todlando\".to_string(),\n...\n 372|        );\n*373|        ProjectIndex {\n### projinval.rs\n 1|//! Project-index invalidation events — the store-anchored nudge queue between\n*2|//! mutation sites and the daemon's single-flight index writer (ADR-0037 §3,\n 3|//! REQ-PROJECT-INDEX-INVALIDATION).\n 4|//!\n 5|//! Mutation seams (session append, context-store commit, rename, fork, purge)\n...\n 33|    /// A context-store mutation (any `p-*`/`a-*` commit, rename, fork): ONE\n*34|    /// debounced global membership refresh — branch-tip fingerprints decide\n 35|    /// what actually rescans.\n 36|    Global,\n 37|    /// A per-endpoint event (new session/cwd, bind/start, purge): refresh that\n...\n 125|    /// Any [`InvalScope::Global`] event was present → membership refresh\n*126|    /// (fingerprint-gated).\n 127|    pub global: bool,\n 128|    /// Endpoint ids needing a row refresh (deduped, sorted).\n 129|    pub endpoints: std::collections::BTreeSet<String>,\n\n## spt/\n\n### src/\n#### cli.rs\n 4170|    stall_evict_last_ms: Option<u64>,\n*4171|    /// PROJECT-INDEX W2 (ADR-0037): the index writer's observability sidecar —\n 4172|    /// generated time, source generation, pending refresh, last duration/error,\n 4173|    /// counts, cache hits/misses, stale reads, repairs, and the per-cycle\n 4174|    /// complexity counters (the CI-gate surface). `None` = no writer has ever\n...\n 4179|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*4180|    project_index: Option<spt_daemon::projwriter::IndexWriterStats>,\n 4181|    /// MESH-RECOVERY (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): the pump's\n 4182|    /// reachability snapshot — live peer count, last successful dial, last\n 4183|    /// admitted registry update, all-peer-failure window, per-peer failing\n...\n 4239|/// dials die in; healthy requires a live peer connection (real progress), so\n*4240|/// the incident fingerprint (all dials failing, everything else green) renders\n 4241|/// DEGRADED here. `None` = no health snapshot (older daemon / no pump yet) or\n 4242|/// a solo node with nobody to reach — silent, not alarming. Pure for tests.\n 4243|// [impl->REQ-PUMP-STAGE-TRUTH]\n...\n 4319|// [impl->REQ-PROJECT-INDEX-WRITER]\n*4320|fn render_project_index_line(\n*4321|    stats: Option<&spt_daemon::projwriter::IndexWriterStats>,\n 4322|) -> Option<String> {\n 4323|    let s = stats?;\n 4324|    let health = match (&s.last_error, s.pending_refresh) {\n...\n 4454|            stall_evict_last_ms,\n*4455|            project_index: spt_daemon::projwriter::read_stats_at(\n 4456|                &spt_daemon::projwriter::stats_path(),\n 4457|            ),\n 4458|            peer_health,\n...\n 4520|        // project index's writer health (index presence alone is not health).\n*4521|        if let Some(line) = render_project_index_line(\n 4522|            spt_daemon::projwriter::read_stats_at(&spt_daemon::projwriter::stats_path())\n 4523|                .as_ref(),\n 4524|        ) {\n...\n 17999|    // [unit->REQ-PUMP-STAGE-TRUTH] the peer-reachability status line: no\n*18000|    // snapshot or a solo node (idle) shows NOTHING; the incident fingerprint\n 18001|    // (every target failing) renders DEGRADED naming the stage, the window\n 18002|    // duration, and the last-good stamps; a live peer renders healthy. Public\n 18003|    // wording, no internal codes; verdict tokens mirror the same states.\n...\n 18015|\n*18016|        // The sequester fingerprint: all targets failing, nothing live.\n 18017|        let mut h = PumpHealth::default();\n 18018|        h.set_targets([\"a\".to_string(), \"b\".to_string()], 1_700_000_000_000);\n 18019|        h.note_failed(\"a\", \"quic-connect\", 0, 1_700_000_050_000);\n...\n 18044|    #[test]\n*18045|    fn project_index_line_states_health_not_presence() {\n*18046|        assert_eq!(render_project_index_line(None), None, \"no stats → no line\");\n 18047|\n*18048|        let mut stats = spt_daemon::projwriter::IndexWriterStats {\n 18049|            generated_ms: 1_700_000_000_000,\n 18050|            endpoints: 3,\n 18051|            projects: 2,\n...\n 18056|        stats.last_cycle.tree_scans = 2;\n*18057|        let fresh = render_project_index_line(Some(&stats)).expect(\"stats render\");\n 18058|        assert!(fresh.contains(\"fresh\"), \"got {fresh}\");\n 18059|        assert!(fresh.contains(\"3 endpoint(s)\") && fresh.contains(\"2 project(s)\"), \"got {fresh}\");\n 18060|        assert!(fresh.contains(\"1 branch enum\") && fresh.contains(\"2 tree scan\"), \"got {fresh}\");\n...\n 18062|        stats.last_error = Some(\"branch enumeration: boom\".to_string());\n*18063|        let failed = render_project_index_line(Some(&stats)).expect(\"failure renders\");\n 18064|        assert!(failed.contains(\"last-known-good\"), \"degradation is stated: {failed}\");\n 18065|\n 18066|        for line in [&fresh, &failed] {\n\n#### picker/\n##### data.rs\n 395|/// test (REQ-PICKER-PROJECT-HISTORY-TRUTH 1c). The kernel moved to\n*396|/// `spt_store::projderive` (PROJECT-INDEX W2) so the daemon's index writer\n 397|/// shares it; this is the picker-local name for the same test.\n 398|fn path_under(path: &Path, base: &Path) -> bool {\n 399|    spt_store::projderive::path_under(path, base)\n...\n 1074|        use spt_store::projindex::{\n*1075|            AbsentReason, CwdProject, EndpointProject, IndexRead, ProjectIndex, ProjectRefEntry,\n*1076|            PROJECT_INDEX_SCHEMA,\n 1077|        };\n 1078|        let spawns_before = spt_store::gitrun::git_spawn_count();\n 1079|\n...\n 1123|        );\n*1124|        let read = IndexRead::Snapshot(ProjectIndex {\n*1125|            schema: PROJECT_INDEX_SCHEMA,\n 1126|            generated_ms: 1,\n 1127|            source_generation: \"fp\".into(),\n 1128|            endpoints,\n...\n 1177|    fn resume_rows_title_from_index_cwd_map_with_pure_fallback() {\n*1178|        use spt_store::projindex::{AbsentReason, CwdProject, IndexRead, ProjectIndex};\n 1179|        let _home = crate::testutil::isolated_home();\n 1180|        let spawns_before = spt_store::gitrun::git_spawn_count();\n 1181|\n...\n 1199|\n*1200|        let mut idx = ProjectIndex::empty(1);\n 1201|        idx.cwds.insert(\n 1202|            \"c:/p/indexed-proj\".to_string(),\n 1203|            CwdProject { id: \"the-slug\".into(), display: \"TheRealDisplay\".into() },\n...\n 1232|        use spt_store::projindex::{\n*1233|            CwdProject, EndpointProject, ProjectIndex, ProjectRefEntry,\n 1234|        };\n 1235|        let _home = crate::testutil::isolated_home();\n 1236|\n...\n 1258|        // The daemon-shaped snapshot on disk (what a writer would publish).\n*1259|        let mut idx = ProjectIndex::empty(1);\n 1260|        idx.source_generation = \"fp\".into();\n 1261|        idx.endpoints.insert(\n 1262|            \"ep-idx\".to_string(),\n\n### tests/\n#### projindex_writer_e2e.rs\n*1|//! PROJECT-INDEX W2 — the daemon's materialized-index writer + event-driven\n 2|//! invalidation against a REAL live daemon (REQ-PROJECT-INDEX-WRITER int gate:\n 3|//! cold + warm start against a real store; REQ-PROJECT-INDEX-INVALIDATION int\n 4|//! gate: session / context / rename / fork / purge invalidation).\n...\n 28|use spt_store::contextstore::ContextStore;\n*29|use spt_store::projindex::{self, IndexRead, ProjectIndex};\n 30|\n 31|/// Scoped pid-tree kill (cleanup) — never machine-wide.\n 32|fn kill_pid(pid: u32) {\n...\n 77|    what: &str,\n*78|    pred: impl Fn(&ProjectIndex) -> bool,\n*79|) -> ProjectIndex {\n 80|    let deadline = Instant::now() + budget;\n 81|    loop {\n 82|        if let IndexRead::Snapshot(idx) = projindex::read_index_at(path) {\n...\n 186|    );\n*187|    assert!(!cold.source_generation.is_empty(), \"fingerprint stamped\");\n 188|\n 189|    // The observability surface rides `daemon status --json` (index presence\n 190|    // alone is not health), and the batched counters are on it.\n...\n 200|    let pi = status_json\n*201|        .get(\"project_index\")\n*202|        .expect(\"status carries the project_index observability block\");\n 203|    assert_eq!(pi[\"source_generation\"], cold.source_generation.as_str());\n 204|    assert_eq!(pi[\"endpoints\"], 3);\n 205|    assert_eq!(\n...\n 303|    // (4b) CONTEXT: a committed slice gives ep2 membership in `beta` — the\n*304|    // store's own commit funnel fires the Global nudge; the fingerprint moves.\n 305|    let slice = cs.project_context_path(\"beta\", \"ep2\").unwrap();\n 306|    std::fs::write(&slice, \"ep2 in beta\").unwrap();\n 307|    cs.commit_project(\"beta\", \"ep2 slice\").unwrap().unwrap();"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":89,"fileCount":13,"files":["crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/pump/health.rs","crates/spt-daemon/src/sync.rs","crates/spt-daemon/tests/mesh_recovery.rs","crates/spt-store/src/branchstore.rs","crates/spt-store/src/projderive.rs","crates/spt-store/src/projindex.rs","crates/spt-store/src/projinval.rs","crates/spt/src/cli.rs","crates/spt/src/picker/data.rs","crates/spt/tests/projindex_writer_e2e.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":2},{"path":"crates/spt-daemon/src/broker.rs","count":1},{"path":"crates/spt-daemon/src/projwriter.rs","count":20},{"path":"crates/spt-daemon/src/pump/health.rs","count":5},{"path":"crates/spt-daemon/src/sync.rs","count":1},{"path":"crates/spt-daemon/tests/mesh_recovery.rs","count":2},{"path":"crates/spt-store/src/branchstore.rs","count":3},{"path":"crates/spt-store/src/projderive.rs","count":1},{"path":"crates/spt-store/src/projindex.rs","count":20},{"path":"crates/spt-store/src/projinval.rs","count":3},{"path":"crates/spt/src/cli.rs","count":14},{"path":"crates/spt/src/picker/data.rs","count":9},{"path":"crates/spt/tests/projindex_writer_e2e.rs","count":8}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/\n\n### src/\n#### brainproc.rs\n 247│\n*248│    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n 249│    // brain hosts the SOLE single-flight writer — load persisted index, boot\n 250│    // reconcile in the background (ready-without-warm is preserved because\n 251│    // write_ready already fired above), then drain/debounce/coalesce the\n    │...\n 253│    // held for symmetry, the thread dies with the brain, and the next brain\n*254│    // rehydrates from the persisted index (warm start, fingerprint-gated).\n 255│    // [impl->REQ-PROJECT-INDEX-WRITER]\n 256│    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n 257│\n#### broker.rs\n 5328│    // payload's HEAD prefix must appear in the PTY echo. A swallowed head (only a tail\n*5329│    // echoes) FAILS verify — the exact head-loss fingerprint. An empty payload (a\n 5330│    // keys-only sequence, nothing echoable) is vacuously verified. RED-FIRST: make\n 5331│    // `echo_verified` always return true → the swallowed-head assert flips.\n 5332│    #[test]\n#### projwriter.rs\n  *1│//! The materialized project-index WRITER — the daemon's derived-state\n   2│//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\n   3│//!\n   4│//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\n    │...\n  18│//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n *19│//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n *20│//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n  21│//!   scan;\n  22│//! - maintains an observability sidecar (`project-index-stats.json`) — index\n  23│//!   presence alone is not health: generated time, source generation, pending\n    │...\n  42│use spt_store::projderive;\n *43│use spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\n  44│use spt_store::projinval::{self, Coalesced};\n  45│use spt_store::sessions;\n  46│\n    │...\n  83│#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n *84│pub struct IndexWriterStats {\n  85│    /// When the current snapshot was generated (epoch ms; 0 = never).\n  86│    #[serde(default)]\n  87│    pub generated_ms: u64,\n *88│    /// The published snapshot's branch-tip fingerprint.\n  89│    #[serde(default)]\n  90│    pub source_generation: String,\n  91│    /// Whether a refresh is queued/underway right now.\n    │...\n 139│/// `spt daemon status` consumer half.\n*140│pub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n 141│    let raw = std::fs::read_to_string(path).ok()?;\n 142│    serde_json::from_str(&raw).ok()\n 143│}\n    │...\n 207│    /// load/publish).\n*208│    current: Option<ProjectIndex>,\n*209│    /// The fingerprint the published snapshot was derived from.\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 216│    /// `(project_id, branch)`.\n    │...\n 222│    repair_pending: bool,\n*223│    pub stats: IndexWriterStats,\n 224│}\n 225│\n 226│impl WriterEngine {\n    │...\n 238│        };\n*239│        let last_fingerprint = current\n 240│            .as_ref()\n 241│            .map(|i| i.source_generation.clone())\n 242│            .unwrap_or_default();\n*243│        let mut stats = IndexWriterStats {\n 244│            generated_ms: current.as_ref().map(|i| i.generated_ms).unwrap_or(0),\n*245│            source_generation: last_fingerprint.clone(),\n 246│            pending_refresh: true, // the boot reconcile is queued by definition\n 247│            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n*248│            ..IndexWriterStats::default()\n 249│        };\n 250│        stats.projects = current\n 251│            .as_ref()\n    │...\n 256│            current,\n*257│            last_fingerprint,\n 258│            branch_cache: BTreeMap::new(),\n 259│            branch_order: Vec::new(),\n 260│            cwd_cache: HashMap::new(),\n    │...\n 266│    /// Run ONE reconcile cycle over the coalesced request. `force_full`\n*267│    /// recomposes every endpoint even on an unchanged fingerprint (the\n 268│    /// periodic backstop); a non-forced global request with an unchanged\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    │...\n 285│\n*286│        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\n 287│        let store = if self.paths.seed_git.exists() {\n 288│            Some(\n 289│                BranchStore::open_or_init(&self.paths.seed_git)\n#### sync.rs\n 447│                            // Nudge ONE global refresh per pull that applied any\n*448│                            // p-* branch; the fingerprint gates what actually\n 449│                            // rescans, and the debounce coalesces a multi-peer\n 450│                            // pull burst. Without this, readers (index-only\n 451│                            // since W3) would lag a sync by up to PERIODIC_MS.\n\n#### pump/\n##### health.rs\n   5│//! membership). This file is the milestone's acceptance surface: the incident\n  *6│//! fingerprint (all dials failing + heartbeat fresh + net_up true) MUST\n   7│//! render degraded, and health returns to green only on REAL peer progress —\n   8│//! never on heartbeat, never on elapsed time.\n   9│//!\n    │...\n  36│    /// Epoch ms since when EVERY fan-out target has been failing (the\n *37│    /// incident fingerprint). Cleared by any real peer progress.\n  38│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n  39│    pub all_fail_since_ms: Option<u64>,\n  40│    /// Currently-failing peers → the STAGE their last failure died in\n    │...\n  60│    Connecting,\n *61│    /// EVERY target failing — the sequester fingerprint. Names the failing\n  62│    /// count and the dominant stage so the operator reads WHERE it dies.\n  63│    Degraded {\n  64│        failing: usize,\n    │...\n 225│    // [unit->REQ-PUMP-STAGE-TRUTH] the health state machine renders the\n*226│    // INCIDENT FINGERPRINT degraded: every target failing, nothing live —\n 227│    // exactly the state the 2026-07-10 sequester sat in while net_up,\n 228│    // heartbeat, and counts all stayed green. And it becomes healthy ONLY on\n 229│    // real peer progress: more failures, elapsed time, or a heartbeat never\n    │...\n 237│        assert!(h.note_failed(\"a\", \"quic-connect\", 0, 2_000), \"new failure logs\");\n*238│        assert_eq!(h.verdict(), HealthVerdict::Connecting, \"one of two failing: not yet the fingerprint\");\n 239│\n 240│        assert!(h.note_failed(\"b\", \"quic-connect\", 0, 3_000));\n 241│        match h.verdict() {\n\n### tests/\n#### mesh_recovery.rs\n 342│// stage — WHILE the pump heartbeat stays fresh (the exact incident\n*343│// fingerprint: green liveness, dead reachability); when discovery restores a\n 344│// real route and a real connect lands, health flips healthy — on progress,\n 345│// never on time.\n 346│#[test]\n    │...\n 378│    // Degraded, naming the stage — while the heartbeat is FRESH (the\n*379│    // fingerprint that made the field incident invisible).\n 380│    converge(\"health degrades during the outage\", || {\n 381│        let h = PumpHealth::load_from(&health_path);\n 382│        matches!(\n\n## spt-store/src/\n### branchstore.rs\n 258│    /// Every local branch with its tip commit id, ordered **newest→oldest by\n*259│    /// tip committer date** — the index writer's ONE branch enumeration\n*260│    /// (ADR-0037 §2: recency order AND the tip fingerprint substrate in a\n 261│    /// single `for-each-ref`, so \"enumerate branches once\" is one git spawn,\n 262│    /// not one per branch).\n 263│    // [impl->REQ-PROJECT-INDEX-WRITER]\n    │...\n 922│    // branch_tips_by_recency returns the SAME order as branches_by_recency and\n*923│    // each entry's tip matches tip() — recency + fingerprint substrate in one\n 924│    // for-each-ref call.\n 925│    #[test]\n 926│    fn branch_tips_by_recency_matches_order_and_tips() {\n### projderive.rs\n  6│//! Lifted verbatim from the picker's `project_refs_from` (PROJECT-INDEX W2) so\n *7│//! the daemon's materialized-index writer and the legacy synchronous reader\n  8│//! (until the W3 cutover) share one kernel — parity by construction, not by\n  9│//! parallel maintenance. The picker wraps [`project_refs_from`] and drops the\n 10│//! [`SourceLeg`]; the writer persists it as `EndpointProject.source`\n### projindex.rs\n  45│/// so discarding a mismatched one loses nothing durable.)\n *46│pub const PROJECT_INDEX_SCHEMA: u32 = 1;\n  47│\n  48│/// `$SPT_HOME/index/` — derived, node-local, regenerable materialized indexes.\n  49│/// Never synced, never tracked; safe to delete wholesale (the daemon writer\n    │...\n 129│#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n*130│pub struct ProjectIndex {\n*131│    /// [`PROJECT_INDEX_SCHEMA`] at write time — the read gate.\n 132│    pub schema: u32,\n 133│    /// When the writer generated this snapshot (epoch ms).\n 134│    #[serde(default)]\n 135│    pub generated_ms: u64,\n*136│    /// Branch-tip fingerprint of the source context store at generation time\n 137│    /// (ADR-0037 §3 — no authoritative commit path exists, so freshness keys\n*138│    /// on fingerprints; an unchanged fingerprint means no rescan).\n 139│    #[serde(default)]\n 140│    pub source_generation: String,\n 141│    /// endpoint id → attribution.\n    │...\n 152│\n*153│impl ProjectIndex {\n 154│    /// The materialized project for a working directory, looked up on the\n 155│    /// normalized spelling — the resume pane's per-row source (never a\n 156│    /// derivation).\n    │...\n 162│\n*163│impl ProjectIndex {\n 164│    /// An empty schema-current index (the writer's cold-start seed).\n 165│    pub fn empty(generated_ms: u64) -> Self {\n*166│        ProjectIndex {\n*167│            schema: PROJECT_INDEX_SCHEMA,\n 168│            generated_ms,\n 169│            source_generation: String::new(),\n 170│            endpoints: BTreeMap::new(),\n    │...\n 175│\n*176│/// Fold a branch-tip enumeration into the `source_generation` fingerprint\n 177│/// (ADR-0037 §3: no authoritative context-store commit path exists, so\n*178│/// freshness keys on branch-tip fingerprints — a writer-maintained generation\n 179│/// counter is REJECTED because nothing maintains it). Order-insensitive\n 180│/// (sorted before hashing) so two enumerations of one store state — whatever\n*181│/// their recency order — fingerprint identically; an unchanged fingerprint\n 182│/// means NO rescan. Only `p-*` branches participate: project membership is\n 183│/// what the index derives, and an `a-*` live-context commit must not force a\n 184│/// membership rescan.\n 185│// [impl->REQ-PROJECT-INDEX-WRITER]\n*186│pub fn fingerprint_tips(tips: &[(String, String)]) -> String {\n 187│    use sha2::{Digest, Sha256};\n 188│    let mut lines: Vec<String> = tips\n 189│        .iter()\n    │...\n 221│pub enum IndexRead {\n*222│    Snapshot(ProjectIndex),\n 223│    Absent(AbsentReason),\n 224│}\n 225│\n    │...\n 258│    match found {\n*259│        Some(s) if s == u64::from(PROJECT_INDEX_SCHEMA) => {}\n 260│        Some(s) => {\n 261│            return IndexRead::Absent(AbsentReason::SchemaMismatch { found: s as u32 });\n 262│        }\n    │...\n 268│    }\n*269│    match serde_json::from_value::<ProjectIndex>(value) {\n 270│        Ok(idx) => IndexRead::Snapshot(idx),\n 271│        Err(e) => IndexRead::Absent(AbsentReason::Unreadable(e.to_string())),\n 272│    }\n    │...\n 285│// [impl->REQ-PROJECT-INDEX-STORE]\n*286│pub fn write_index_at(path: &Path, index: &ProjectIndex) -> std::io::Result<()> {\n 287│    if let Some(dir) = path.parent() {\n 288│        std::fs::create_dir_all(dir)?;\n 289│    }\n    │...\n 295│/// [`write_index_at`] against the canonical [`index_path`].\n*296│pub fn write_index(index: &ProjectIndex) -> std::io::Result<()> {\n 297│    write_index_at(&index_path(), index)\n 298│}\n 299│\n    │...\n 329│\n*330│    fn sample_index() -> ProjectIndex {\n 331│        let mut endpoints = BTreeMap::new();\n 332│        endpoints.insert(\n 333│            \"todlando\".to_string(),\n    │...\n 372│        );\n*373│        ProjectIndex {\n### projinval.rs\n   1│//! Project-index invalidation events — the store-anchored nudge queue between\n  *2│//! mutation sites and the daemon's single-flight index writer (ADR-0037 §3,\n   3│//! REQ-PROJECT-INDEX-INVALIDATION).\n   4│//!\n   5│//! Mutation seams (session append, context-store commit, rename, fork, purge)\n    │...\n  33│    /// A context-store mutation (any `p-*`/`a-*` commit, rename, fork): ONE\n *34│    /// debounced global membership refresh — branch-tip fingerprints decide\n  35│    /// what actually rescans.\n  36│    Global,\n  37│    /// A per-endpoint event (new session/cwd, bind/start, purge): refresh that\n    │...\n 125│    /// Any [`InvalScope::Global`] event was present → membership refresh\n*126│    /// (fingerprint-gated).\n 127│    pub global: bool,\n 128│    /// Endpoint ids needing a row refresh (deduped, sorted).\n 129│    pub endpoints: std::collections::BTreeSet<String>,\n\n## spt/\n\n### src/\n#### cli.rs\n  4170│    stall_evict_last_ms: Option<u64>,\n *4171│    /// PROJECT-INDEX W2 (ADR-0037): the index writer's observability sidecar —\n  4172│    /// generated time, source generation, pending refresh, last duration/error,\n  4173│    /// counts, cache hits/misses, stale reads, repairs, and the per-cycle\n  4174│    /// complexity counters (the CI-gate surface). `None` = no writer has ever\n      │...\n  4179│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n *4180│    project_index: Option<spt_daemon::projwriter::IndexWriterStats>,\n  4181│    /// MESH-RECOVERY (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): the pump's\n  4182│    /// reachability snapshot — live peer count, last successful dial, last\n  4183│    /// admitted registry update, all-peer-failure window, per-peer failing\n      │...\n  4239│/// dials die in; healthy requires a live peer connection (real progress), so\n *4240│/// the incident fingerprint (all dials failing, everything else green) renders\n  4241│/// DEGRADED here. `None` = no health snapshot (older daemon / no pump yet) or\n  4242│/// a solo node with nobody to reach — silent, not alarming. Pure for tests.\n  4243│// [impl->REQ-PUMP-STAGE-TRUTH]\n      │...\n  4319│// [impl->REQ-PROJECT-INDEX-WRITER]\n *4320│fn render_project_index_line(\n *4321│    stats: Option<&spt_daemon::projwriter::IndexWriterStats>,\n  4322│) -> Option<String> {\n  4323│    let s = stats?;\n  4324│    let health = match (&s.last_error, s.pending_refresh) {\n      │...\n  4454│            stall_evict_last_ms,\n *4455│            project_index: spt_daemon::projwriter::read_stats_at(\n  4456│                &spt_daemon::projwriter::stats_path(),\n  4457│            ),\n  4458│            peer_health,\n      │...\n  4520│        // project index's writer health (index presence alone is not health).\n *4521│        if let Some(line) = render_project_index_line(\n  4522│            spt_daemon::projwriter::read_stats_at(&spt_daemon::projwriter::stats_path())\n  4523│                .as_ref(),\n  4524│        ) {\n      │...\n 17999│    // [unit->REQ-PUMP-STAGE-TRUTH] the peer-reachability status line: no\n*18000│    // snapshot or a solo node (idle) shows NOTHING; the incident fingerprint\n 18001│    // (every target failing) renders DEGRADED naming the stage, the window\n 18002│    // duration, and the last-good stamps; a live peer renders healthy. Public\n 18003│    // wording, no internal codes; verdict tokens mirror the same states.\n      │...\n 18015│\n*18016│        // The sequester fingerprint: all targets failing, nothing live.\n 18017│        let mut h = PumpHealth::default();\n 18018│        h.set_targets([\"a\".to_string(), \"b\".to_string()], 1_700_000_000_000);\n 18019│        h.note_failed(\"a\", \"quic-connect\", 0, 1_700_000_050_000);\n      │...\n 18044│    #[test]\n*18045│    fn project_index_line_states_health_not_presence() {\n*18046│        assert_eq!(render_project_index_line(None), None, \"no stats → no line\");\n 18047│\n*18048│        let mut stats = spt_daemon::projwriter::IndexWriterStats {\n 18049│            generated_ms: 1_700_000_000_000,\n 18050│            endpoints: 3,\n 18051│            projects: 2,\n      │...\n 18056│        stats.last_cycle.tree_scans = 2;\n*18057│        let fresh = render_project_index_line(Some(&stats)).expect(\"stats render\");\n 18058│        assert!(fresh.contains(\"fresh\"), \"got {fresh}\");\n 18059│        assert!(fresh.contains(\"3 endpoint(s)\") && fresh.contains(\"2 project(s)\"), \"got {fresh}\");\n 18060│        assert!(fresh.contains(\"1 branch enum\") && fresh.contains(\"2 tree scan\"), \"got {fresh}\");\n      │...\n 18062│        stats.last_error = Some(\"branch enumeration: boom\".to_string());\n*18063│        let failed = render_project_index_line(Some(&stats)).expect(\"failure renders\");\n 18064│        assert!(failed.contains(\"last-known-good\"), \"degradation is stated: {failed}\");\n 18065│\n 18066│        for line in [&fresh, &failed] {\n\n#### picker/\n##### data.rs\n  395│/// test (REQ-PICKER-PROJECT-HISTORY-TRUTH 1c). The kernel moved to\n *396│/// `spt_store::projderive` (PROJECT-INDEX W2) so the daemon's index writer\n  397│/// shares it; this is the picker-local name for the same test.\n  398│fn path_under(path: &Path, base: &Path) -> bool {\n  399│    spt_store::projderive::path_under(path, base)\n     │...\n 1074│        use spt_store::projindex::{\n*1075│            AbsentReason, CwdProject, EndpointProject, IndexRead, ProjectIndex, ProjectRefEntry,\n*1076│            PROJECT_INDEX_SCHEMA,\n 1077│        };\n 1078│        let spawns_before = spt_store::gitrun::git_spawn_count();\n 1079│\n     │...\n 1123│        );\n*1124│        let read = IndexRead::Snapshot(ProjectIndex {\n*1125│            schema: PROJECT_INDEX_SCHEMA,\n 1126│            generated_ms: 1,\n 1127│            source_generation: \"fp\".into(),\n 1128│            endpoints,\n     │...\n 1177│    fn resume_rows_title_from_index_cwd_map_with_pure_fallback() {\n*1178│        use spt_store::projindex::{AbsentReason, CwdProject, IndexRead, ProjectIndex};\n 1179│        let _home = crate::testutil::isolated_home();\n 1180│        let spawns_before = spt_store::gitrun::git_spawn_count();\n 1181│\n     │...\n 1199│\n*1200│        let mut idx = ProjectIndex::empty(1);\n 1201│        idx.cwds.insert(\n 1202│            \"c:/p/indexed-proj\".to_string(),\n 1203│            CwdProject { id: \"the-slug\".into(), display: \"TheRealDisplay\".into() },\n     │...\n 1232│        use spt_store::projindex::{\n*1233│            CwdProject, EndpointProject, ProjectIndex, ProjectRefEntry,\n 1234│        };\n 1235│        let _home = crate::testutil::isolated_home();\n 1236│\n     │...\n 1258│        // The daemon-shaped snapshot on disk (what a writer would publish).\n*1259│        let mut idx = ProjectIndex::empty(1);\n 1260│        idx.source_generation = \"fp\".into();\n 1261│        idx.endpoints.insert(\n 1262│            \"ep-idx\".to_string(),\n\n### tests/\n#### projindex_writer_e2e.rs\n  *1│//! PROJECT-INDEX W2 — the daemon's materialized-index writer + event-driven\n   2│//! invalidation against a REAL live daemon (REQ-PROJECT-INDEX-WRITER int gate:\n   3│//! cold + warm start against a real store; REQ-PROJECT-INDEX-INVALIDATION int\n   4│//! gate: session / context / rename / fork / purge invalidation).\n    │...\n  28│use spt_store::contextstore::ContextStore;\n *29│use spt_store::projindex::{self, IndexRead, ProjectIndex};\n  30│\n  31│/// Scoped pid-tree kill (cleanup) — never machine-wide.\n  32│fn kill_pid(pid: u32) {\n    │...\n  77│    what: &str,\n *78│    pred: impl Fn(&ProjectIndex) -> bool,\n *79│) -> ProjectIndex {\n  80│    let deadline = Instant::now() + budget;\n  81│    loop {\n  82│        if let IndexRead::Snapshot(idx) = projindex::read_index_at(path) {\n    │...\n 186│    );\n*187│    assert!(!cold.source_generation.is_empty(), \"fingerprint stamped\");\n 188│\n 189│    // The observability surface rides `daemon status --json` (index presence\n 190│    // alone is not health), and the batched counters are on it.\n    │...\n 200│    let pi = status_json\n*201│        .get(\"project_index\")\n*202│        .expect(\"status carries the project_index observability block\");\n 203│    assert_eq!(pi[\"source_generation\"], cold.source_generation.as_str());\n 204│    assert_eq!(pi[\"endpoints\"], 3);\n 205│    assert_eq!(\n    │...\n 303│    // (4b) CONTEXT: a committed slice gives ep2 membership in `beta` — the\n*304│    // store's own commit funnel fires the Global nudge; the fingerprint moves.\n 305│    let slice = cs.project_context_path(\"beta\", \"ep2\").unwrap();\n 306│    std::fs::write(&slice, \"ep2 in beta\").unwrap();\n 307│    cs.commit_project(\"beta\", \"ep2 slice\").unwrap().unwrap();"},"isError":false,"timestamp":1784271472662}}
{"type":"message","id":"1fc8bf56","parentId":"5c008982","timestamp":"2026-07-17T06:58:06.350Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating pulse_tick and lifecycle scanning**\n\n**Examining echo_commune and event projections**"},{"type":"toolCall","id":"call_OqWlCtSTggNGrTEFG7K4lsje|fc_038f28d3e93bbfd4016a59d27e4610819684ddf9f5d02fe7d9","name":"grep","arguments":{"i":"Locating pulse internals","pattern":"fn run_pulse_loop|fn pulse_tick|echo|commune|digest|history|read_to_string|read\\(","path":"crates/spt-daemon/src/lifecycle.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Y5QqpDwxlKtuE7zl6Pp5zlsM|fc_038f28d3e93bbfd4016a59d27e46288196a2473b4842718336","name":"grep","arguments":{"i":"Locating brain heartbeat","pattern":"BRAIN_HEARTBEAT|DEFAULT_PULSE_PERIOD|pulse_period","path":"crates/spt-daemon/src; crates/spt-live/src; crates/spt-store/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_a9hUTAXLPdoTjgNwqrFwvScY|fc_038f28d3e93bbfd4016a59d27e463481968011b9ae3552ec48","name":"read","arguments":{"i":"Reading invalidation queue","path":"crates/spt-store/src/projinval.rs","selector":"1-220"}},{"type":"toolCall","id":"call_SjdJBzFdRxjII5dbJY1LUwqW|fc_038f28d3e93bbfd4016a59d27e463c8196a5d9e7731b90749a","name":"read","arguments":{"i":"Reading branch git scans","path":"crates/spt-store/src/branchstore.rs","selector":"230-310,480-540"}},{"type":"toolCall","id":"call_Lw7LT5LujvwmToCVE5peEmme|fc_038f28d3e93bbfd4016a59d27e4644819686354d2a85d6e859","name":"grep","arguments":{"i":"Locating session ledger reads","pattern":"pub fn read_all|SESSIONS|sessions.json|SessionEntry","path":"crates/spt-store/src/sessions.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_YKXf1jdVZShj0K61jrp0p4oL|fc_038f28d3e93bbfd4016a59d27e464c8196968b3203fc212d7b","name":"grep","arguments":{"i":"Finding repeated daemon scans","pattern":"while !stop|loop \\{|thread::sleep|recv_timeout|read_dir|read_to_string|std::fs::read\\(","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":20}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":32322,"output":574,"cacheRead":60928,"cacheWrite":0,"totalTokens":93824,"reasoningTokens":191,"cost":{"input":0.16161,"output":0.01722,"cacheRead":0.030463999999999998,"cacheWrite":0,"total":0.20929399999999998}},"stopReason":"toolUse","timestamp":1784271472701,"responseId":"resp_038f28d3e93bbfd4016a59d2724fa48196a0ad85b312ab6328","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d273cc7c8196a7481c76a3454da1","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdJ-xrQQW30woJyJ6GfQ1gipFGVytS_LT-2JnkgpbeUgoBU57Tv6vdKi2RJ3lWx1oiLWLiAQor45S9LMWzur8SHTvBJoaCIbli0RSuFa2So3Y-c4s8wZ-n8JBgPSy1Ut51O1_T5EK0b4a-oNz9ZMjvBAhCmBkR88St9brNQwjxbA31nryGng33Jm3heKAPYRD9btWl0pZhY5v4MbLoBJqHqA_MluKjJxCLEWdipSJ6Jx_J4gOnl_iMhMjArtdiJ3qa7fuWPS5ZyKspqatoezY-Dgpx3Ai-CayHVRa3aQHpyoOfHW3LzMuRfM-Pa4fQz2u0DGD715cRrMWqC5zgjjkQY1mtzfFZqM0e-qV9WsNeUiXHxhSU6gazRM-GDr_ggmX_ei5roYzqQ9_uolZeSuUZOiKNaOXjpQB5WwHXPAfkKQPeu--t9o58c6njwM0Hj0veckNW0IvBMTt3i5qXxC5xXn7GKuUGIYKs8QUwnAFrK9cSDPWEb-bWblkaNaR2h-NIn8N6qN3zFKKjRSv5iENWvCi2zi82ls1b8MuKGMakzXXhFnE6GJRLfCNn60ozxDB0jcCckEVpuOxbWagnr2NhlEULwK-oYuRtCYOYGqUM6gG3faKlKMISs19orObiY1d4PQaMPbF3BahtI4YQgA4YBGSVwQPAGe11gpeXT32ZYxV9rpg0ef076tsf-1CkQXFX1uln0swsMQXoY-75uS0Nqf7BRw5dYe-wiEZrtBwQf-zgkJvV7JsDDBO2pCAayiG0rd93_35OJag2Nzvp5J9mjAf283s_mQORZdYi4T3jbn1M0PCCG_WmkWkwEdhmRLXBbkmftI16HbDrFpyZ1Sn0h2MDJWg85no_d9wF5ZMcwbChJ6eECoIyLirQUnB6dFBgTPaHC2zF9zclbVubd_M023PAqEfGowvoQhvTFnjcY7IM_THsAIGPkpNlWNVHQ_79QMeNbsMJy63JuARTpq_33wSK0Vwcezd27_-G4dHHxfCkkkfnlUa9D45gvT9E8ax-WsquEkJO7mhmwqJCgFHA-AKwoTWqoJG-_WUv-90IyB1_z-7DVNQRGZ9P9O1Vl_KsMnpjD7QyWP5sPl-siTsnYUo4xgSiAZQ827udq85IJhiYnZe19AaUKxTsQMEZGpyxB4S8BdCFTVtDstc72uUJufofiCPw0SWQyirnOJYyZjN3NN83aiNWDYMd1b1mcBxXcCf8VeSKDEkh4LAA9E-jhHzwAje-vAetrl1kueFio7qslcaxj6nZnh7vBmNttUA0bqbziEqaPL7u9ug5xktxzgKiwyJf0Au-IJAPLxU2YKUcLVzZ73zT92o4jN72Lva5jxV9RVk5e-h8Mf0A-zdNDwB5Wde_gAWqxszOwR52yj7NqAQ0tBlKrFg3I6bCLVX9VxdQMQ-JiftOZkml0HmHIU5ZBk0dBj0kY2v4NULH6WRwvKMlFEOURGxp9N8wRTXRR_OVwrlm3Ve2vyhDU3MyrfOImYe27sLf9hLWuOb1HNOow3N4OtWd5hL7VPjIQ3ND0AukUdPde8LArgx36E27TXZlMZ-jEa_kNCRmZzX_9bH6CUS1q0fQ6MEIi4Uq3Gt04UAlApPzKyHLl0sf3VJeT9Auzoj_-A2l8dFkVgNj0vPfuR9fI8V0kgb1Ln09wpGEHdvMoVTuUH6mdPx8ha4TrmRmW2KEPOMdQeMuYbn9Xl8XCtu9iZfLN_smv7v6STeoF2FyurblgUFtDvCDxZqQBZYSlGrdge3sjGnjVkNT1GwmGTFCOL0G2tGLS62FhrtKXoV-iTMqazLoPVA_b5hqgKm5dv-fAm_f5KydwFafswHafk1Ixk9qj9lycUMw81QDkoghFV8e040TMgg0NlSy2I2neLrDTJpNz8iovCK_TeA6W0bA7qqMo6eK_LxAdGXxHMS-1PMhw2x0yl_nU8g7Pr2r0eC29q1mG8GNniUXpHT79b5Wct2sJU8UtoDYis9jRvWdvjCfEcfW2XpMfZk1_h-IXUvSIHm8RYyqQ8eCWeQEOCci8fnH1ascNHlN7fvRh4zx82aUMlH6esNp4R3xam9FcQjZvfT_PsMn0p1tdRIr25ZKPjrHU_4BDE-ROmLrZ7DPmuEeuqdSZp7Sg0c4uh6zalkn_skctRHWNopmoSUbxR1tBERz2hfpJLwktSOMUY","summary":[{"type":"summary_text","text":"**Investigating pulse_tick and lifecycle scanning**"},{"type":"summary_text","text":"**Examining echo_commune and event projections**"}]},{"id":"fc_038f28d3e93bbfd4016a59d27e4610819684ddf9f5d02fe7d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating pulse internals\",\"pattern\":\"fn run_pulse_loop|fn pulse_tick|echo|commune|digest|history|read_to_string|read\\\\(\",\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_OqWlCtSTggNGrTEFG7K4lsje","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d27e46288196a2473b4842718336","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating brain heartbeat\",\"pattern\":\"BRAIN_HEARTBEAT|DEFAULT_PULSE_PERIOD|pulse_period\",\"path\":\"crates/spt-daemon/src; crates/spt-live/src; crates/spt-store/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Y5QqpDwxlKtuE7zl6Pp5zlsM","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d27e463481968011b9ae3552ec48","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading invalidation queue\",\"path\":\"crates/spt-store/src/projinval.rs\",\"selector\":\"1-220\"}","call_id":"call_a9hUTAXLPdoTjgNwqrFwvScY","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d27e463c8196a5d9e7731b90749a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading branch git scans\",\"path\":\"crates/spt-store/src/branchstore.rs\",\"selector\":\"230-310,480-540\"}","call_id":"call_SjdJBzFdRxjII5dbJY1LUwqW","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d27e4644819686354d2a85d6e859","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating session ledger reads\",\"pattern\":\"pub fn read_all|SESSIONS|sessions.json|SessionEntry\",\"path\":\"crates/spt-store/src/sessions.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Lw7LT5LujvwmToCVE5peEmme","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d27e464c8196968b3203fc212d7b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding repeated daemon scans\",\"pattern\":\"while !stop|loop \\\\{|thread::sleep|recv_timeout|read_dir|read_to_string|std::fs::read\\\\(\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":20}","call_id":"call_YKXf1jdVZShj0K61jrp0p4oL","name":"grep"}]},"duration":13646.277500003576,"ttft":3112.086899995804,"contextSnapshot":{"promptTokens":93250,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OqWlCtSTggNGrTEFG7K4lsje|fc_038f28d3e93bbfd4016a59d27e4610819684ddf9f5d02fe7d9","toolName":"grep","startedAt":"2026-07-17T06:58:06.351Z","args":{"path":"crates/spt-daemon/src/lifecycle.rs"},"intent":"Locating pulse internals"},"id":"aca153ad","parentId":"1fc8bf56","timestamp":"2026-07-17T06:58:06.351Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Y5QqpDwxlKtuE7zl6Pp5zlsM|fc_038f28d3e93bbfd4016a59d27e46288196a2473b4842718336","toolName":"grep","startedAt":"2026-07-17T06:58:06.351Z","args":{"path":"crates/spt-daemon/src; crates/spt-live/src; crates/spt-store/src"},"intent":"Locating brain heartbeat"},"id":"40ed9fc3","parentId":"aca153ad","timestamp":"2026-07-17T06:58:06.351Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_a9hUTAXLPdoTjgNwqrFwvScY|fc_038f28d3e93bbfd4016a59d27e463481968011b9ae3552ec48","toolName":"read","startedAt":"2026-07-17T06:58:06.351Z","args":{"path":"crates/spt-store/src/projinval.rs"},"intent":"Reading invalidation queue"},"id":"f5481c7b","parentId":"40ed9fc3","timestamp":"2026-07-17T06:58:06.351Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_SjdJBzFdRxjII5dbJY1LUwqW|fc_038f28d3e93bbfd4016a59d27e463c8196a5d9e7731b90749a","toolName":"read","startedAt":"2026-07-17T06:58:06.351Z","args":{"path":"crates/spt-store/src/branchstore.rs"},"intent":"Reading branch git scans"},"id":"bfcc7410","parentId":"f5481c7b","timestamp":"2026-07-17T06:58:06.351Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Lw7LT5LujvwmToCVE5peEmme|fc_038f28d3e93bbfd4016a59d27e4644819686354d2a85d6e859","toolName":"grep","startedAt":"2026-07-17T06:58:06.352Z","args":{"path":"crates/spt-store/src/sessions.rs"},"intent":"Locating session ledger reads"},"id":"98d6558f","parentId":"bfcc7410","timestamp":"2026-07-17T06:58:06.352Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YKXf1jdVZShj0K61jrp0p4oL|fc_038f28d3e93bbfd4016a59d27e464c8196968b3203fc212d7b","toolName":"grep","startedAt":"2026-07-17T06:58:06.352Z","args":{"path":"crates/spt-daemon/src"},"intent":"Finding repeated daemon scans"},"id":"61570193","parentId":"98d6558f","timestamp":"2026-07-17T06:58:06.352Z"}
{"type":"message","id":"fb1d3b43","parentId":"61570193","timestamp":"2026-07-17T06:58:06.365Z","message":{"role":"toolResult","toolCallId":"call_SjdJBzFdRxjII5dbJY1LUwqW|fc_038f28d3e93bbfd4016a59d27e463c8196a5d9e7731b90749a","toolName":"read","content":[{"type":"text","text":"    pub fn branches(&self) -> std::io::Result<Vec<String>> {\n…\n    }\n\n    /// Every local branch name ordered **newest→oldest by tip committer date** —\n    /// the recency order a reader projects history in (REQ-PICKER-2). Same source\n    /// as [`branches`](Self::branches), just `--sort=-committerdate`; ties hold\n    /// git's stable secondary order.\n    // [impl->REQ-PICKER-2]\n    pub fn branches_by_recency(&self) -> std::io::Result<Vec<String>> {\n        let out = run_git_ok(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"for-each-ref\",\n                \"--sort=-committerdate\",\n                \"--format=%(refname:short)\",\n                \"refs/heads/\",\n            ],\n            None,\n            None,\n        )?;\n        Ok(String::from_utf8_lossy(&out.stdout)\n            .lines()\n            .map(str::trim)\n            .filter(|l| !l.is_empty())\n            .map(str::to_string)\n            .collect())\n    }\n\n    /// Every local branch with its tip commit id, ordered **newest→oldest by\n    /// tip committer date** — the index writer's ONE branch enumeration\n    /// (ADR-0037 §2: recency order AND the tip fingerprint substrate in a\n    /// single `for-each-ref`, so \"enumerate branches once\" is one git spawn,\n    /// not one per branch).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    pub fn branch_tips_by_recency(&self) -> std::io::Result<Vec<(String, String)>> {\n        let out = run_git_ok(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"for-each-ref\",\n                \"--sort=-committerdate\",\n                \"--format=%(refname:short) %(objectname)\",\n                \"refs/heads/\",\n            ],\n            None,\n            None,\n        )?;\n        Ok(String::from_utf8_lossy(&out.stdout)\n            .lines()\n            .filter_map(|l| {\n                let (name, tip) = l.trim().rsplit_once(' ')?;\n                let (name, tip) = (name.trim(), tip.trim());\n                if name.is_empty() || tip.is_empty() {\n                    None\n                } else {\n                    Some((name.to_string(), tip.to_string()))\n                }\n            })\n            .collect())\n    }\n\n    /// The branch tip commit id, `None` for an absent branch — **tip = resume**\n    /// (the recovery read; a crash mid-write never advanced this).\n    // [impl->REQ-STORE-1]\n    pub fn tip(&self, branch: &str) -> std::io::Result<Option<String>> {\n        let out = run_git(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"rev-parse\",\n                \"--verify\",\n                \"--quiet\",\n                &format!(\"refs/heads/{branch}\"),\n            ],\n            None,\n            None,\n            GIT_TIMEOUT,\n        )?;\n        Ok(if out.success() {\n            Some(out.stdout_trimmed())\n        } else {\n…\n        })\n    }\n…\n    pub fn fetch_bundle(&self, bundle: &Path) -> std::io::Result<BTreeMap<String, String>> {\n…\n        for b in &branches {\n            run_git_ok(\n                &[\n                    \"-C\",\n                    &dir,\n                    \"fetch\",\n                    \"--force\",\n                    &bundle_arg,\n                    &format!(\"refs/heads/{b}:{SYNC_REF_PREFIX}/{b}\"),\n                ],\n                None,\n                None,\n            )?;\n            let tip = run_git_ok(\n                &[\"-C\", &dir, \"rev-parse\", &format!(\"{SYNC_REF_PREFIX}/{b}\")],\n                None,\n                None,\n            )?\n            .stdout_trimmed();\n            tips.insert(b.clone(), tip);\n        }\n        Ok(tips)\n    }\n\n    /// Fast-forward `worktree`'s branch to `commit` (ref + index + files).\n    /// Only legal when the current tip is an ancestor — the caller's\n    /// all-accepted propagation arm; git refuses otherwise.\n    // [impl->REQ-NET-3]\n    pub fn fast_forward_worktree(&self, worktree: &Path, commit: &str) -> std::io::Result<()> {\n        run_git_ok(&[\"merge\", \"--ff-only\", commit], Some(worktree), None)?;\n        Ok(())\n    }\n\n    /// Commit the worktree's current state as a **two-parent merge commit**\n    /// (`HEAD` + `other_parent`) — the sync join that keeps the context DAG\n    /// shared (ADR-0013: one merge commit per sync, unrelated seed histories\n    /// joined at first contact; `commit-tree` needs no\n    /// `--allow-unrelated-histories` escape hatch). The tree is whatever the\n    /// driver left in the worktree — never a git line-merge. Returns the merge\n    /// commit id.\n    // [impl->REQ-NET-3]\n    pub fn merge_commit_in_worktree(\n        &self,\n        worktree: &Path,\n        other_parent: &str,\n        message: &str,\n    ) -> std::io::Result<String> {\n        run_git_ok(&[\"add\", \"-A\", \"--\", \".\"], Some(worktree), None)?;\n        let tree = run_git_ok(&[\"write-tree\"], Some(worktree), None)?.stdout_trimmed();\n        let head = run_git_ok(&[\"rev-parse\", \"HEAD\"], Some(worktree), None)?.stdout_trimmed();\n        let merge = run_git_ok(\n            &[\n                \"commit-tree\",\n                &tree,\n                \"-p\",\n                &head,\n                \"-p\",\n                other_parent,\n                \"-m\",\n                message,\n            ],\n            Some(worktree),\n            None,\n        )?\n        .stdout_trimmed();\n…\n    }"}],"details":{"displayContent":{"text":"    pub fn branches(&self) -> std::io::Result<Vec<String>> {\n…\n    }\n\n    /// Every local branch name ordered **newest→oldest by tip committer date** —\n    /// the recency order a reader projects history in (REQ-PICKER-2). Same source\n    /// as [`branches`](Self::branches), just `--sort=-committerdate`; ties hold\n    /// git's stable secondary order.\n    // [impl->REQ-PICKER-2]\n    pub fn branches_by_recency(&self) -> std::io::Result<Vec<String>> {\n        let out = run_git_ok(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"for-each-ref\",\n                \"--sort=-committerdate\",\n                \"--format=%(refname:short)\",\n                \"refs/heads/\",\n            ],\n            None,\n            None,\n        )?;\n        Ok(String::from_utf8_lossy(&out.stdout)\n            .lines()\n            .map(str::trim)\n            .filter(|l| !l.is_empty())\n            .map(str::to_string)\n            .collect())\n    }\n\n    /// Every local branch with its tip commit id, ordered **newest→oldest by\n    /// tip committer date** — the index writer's ONE branch enumeration\n    /// (ADR-0037 §2: recency order AND the tip fingerprint substrate in a\n    /// single `for-each-ref`, so \"enumerate branches once\" is one git spawn,\n    /// not one per branch).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    pub fn branch_tips_by_recency(&self) -> std::io::Result<Vec<(String, String)>> {\n        let out = run_git_ok(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"for-each-ref\",\n                \"--sort=-committerdate\",\n                \"--format=%(refname:short) %(objectname)\",\n                \"refs/heads/\",\n            ],\n            None,\n            None,\n        )?;\n        Ok(String::from_utf8_lossy(&out.stdout)\n            .lines()\n            .filter_map(|l| {\n                let (name, tip) = l.trim().rsplit_once(' ')?;\n                let (name, tip) = (name.trim(), tip.trim());\n                if name.is_empty() || tip.is_empty() {\n                    None\n                } else {\n                    Some((name.to_string(), tip.to_string()))\n                }\n            })\n            .collect())\n    }\n\n    /// The branch tip commit id, `None` for an absent branch — **tip = resume**\n    /// (the recovery read; a crash mid-write never advanced this).\n    // [impl->REQ-STORE-1]\n    pub fn tip(&self, branch: &str) -> std::io::Result<Option<String>> {\n        let out = run_git(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"rev-parse\",\n                \"--verify\",\n                \"--quiet\",\n                &format!(\"refs/heads/{branch}\"),\n            ],\n            None,\n            None,\n            GIT_TIMEOUT,\n        )?;\n        Ok(if out.success() {\n            Some(out.stdout_trimmed())\n        } else {\n…\n        })\n    }\n…\n    pub fn fetch_bundle(&self, bundle: &Path) -> std::io::Result<BTreeMap<String, String>> {\n…\n        for b in &branches {\n            run_git_ok(\n                &[\n                    \"-C\",\n                    &dir,\n                    \"fetch\",\n                    \"--force\",\n                    &bundle_arg,\n                    &format!(\"refs/heads/{b}:{SYNC_REF_PREFIX}/{b}\"),\n                ],\n                None,\n                None,\n            )?;\n            let tip = run_git_ok(\n                &[\"-C\", &dir, \"rev-parse\", &format!(\"{SYNC_REF_PREFIX}/{b}\")],\n                None,\n                None,\n            )?\n            .stdout_trimmed();\n            tips.insert(b.clone(), tip);\n        }\n        Ok(tips)\n    }\n\n    /// Fast-forward `worktree`'s branch to `commit` (ref + index + files).\n    /// Only legal when the current tip is an ancestor — the caller's\n    /// all-accepted propagation arm; git refuses otherwise.\n    // [impl->REQ-NET-3]\n    pub fn fast_forward_worktree(&self, worktree: &Path, commit: &str) -> std::io::Result<()> {\n        run_git_ok(&[\"merge\", \"--ff-only\", commit], Some(worktree), None)?;\n        Ok(())\n    }\n\n    /// Commit the worktree's current state as a **two-parent merge commit**\n    /// (`HEAD` + `other_parent`) — the sync join that keeps the context DAG\n    /// shared (ADR-0013: one merge commit per sync, unrelated seed histories\n    /// joined at first contact; `commit-tree` needs no\n    /// `--allow-unrelated-histories` escape hatch). The tree is whatever the\n    /// driver left in the worktree — never a git line-merge. Returns the merge\n    /// commit id.\n    // [impl->REQ-NET-3]\n    pub fn merge_commit_in_worktree(\n        &self,\n        worktree: &Path,\n        other_parent: &str,\n        message: &str,\n    ) -> std::io::Result<String> {\n        run_git_ok(&[\"add\", \"-A\", \"--\", \".\"], Some(worktree), None)?;\n        let tree = run_git_ok(&[\"write-tree\"], Some(worktree), None)?.stdout_trimmed();\n        let head = run_git_ok(&[\"rev-parse\", \"HEAD\"], Some(worktree), None)?.stdout_trimmed();\n        let merge = run_git_ok(\n            &[\n                \"commit-tree\",\n                &tree,\n                \"-p\",\n                &head,\n                \"-p\",\n                other_parent,\n                \"-m\",\n                message,\n            ],\n            Some(worktree),\n            None,\n        )?\n        .stdout_trimmed();\n…\n    }","startLine":212,"lineNumbers":[212,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,297,298,299,300,301,302,303,304,305,306,307,308,309,310,null,312,313,null,461,null,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,null,547]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\branchstore.rs"}}},"isError":false,"timestamp":1784271486365}}
{"type":"message","id":"e6cfa62c","parentId":"fb1d3b43","timestamp":"2026-07-17T06:58:06.365Z","message":{"role":"toolResult","toolCallId":"call_Lw7LT5LujvwmToCVE5peEmme|fc_038f28d3e93bbfd4016a59d27e4644819686354d2a85d6e859","toolName":"grep","content":[{"type":"text","text":" 58|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n*59|pub struct SessionEntry {\n 60|    /// RFC3339-UTC instant (see [`crate::timefmt`]).\n 61|    pub ts: String,\n 62|    /// The harness session id that became active.\n...\n 98|/// line is skipped, the rest still load (the degrade-don't-crash store stance).\n*99|pub fn read_all(perch: &Path) -> Vec<SessionEntry> {\n 100|    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\n 101|        return Vec::new();\n 102|    };\n...\n 109|/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\n*110|pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\n 111|    if k == 0 {\n 112|        return Vec::new();\n 113|    }\n...\n 118|\n*119|/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\n 120|/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\n 121|/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\n 122|/// `read_all` index (not the slice index) so it stays comparable across sessions.\n...\n 124|// [impl->REQ-DIGEST-CURSOR]\n*125|pub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\n 126|    if k == 0 {\n 127|        return Vec::new();\n 128|    }\n...\n 149|// [impl->REQ-TERM-6]\n*150|pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {\n 151|    let mut entries = read_all(perch);\n 152|    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {\n 153|        return Ok(false); // same session re-bound — not a new boundary\n...\n 188|\n*189|    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {\n*190|        SessionEntry {\n 191|            ts: crate::timefmt::rfc3339_utc(1_700_000_000),\n 192|            session_id: sid.to_string(),\n 193|            trigger,\n...\n 255|        let perch = dir.path().join(\"perch\");\n*256|        let with_cwd = SessionEntry {\n 257|            ts: crate::timefmt::rfc3339_utc(1_700_000_000),\n 258|            session_id: \"s-cwd\".to_string(),\n 259|            trigger: SessionTrigger::Boot,\n...\n 308|    fn none_cwd_emits_no_field_and_roundtrips() {\n*309|        let row = SessionEntry {\n 310|            ts: \"t\".to_string(),\n 311|            session_id: \"s\".to_string(),\n 312|            trigger: SessionTrigger::Boot,\n...\n 321|        );\n*322|        let back: SessionEntry = serde_json::from_str(&json).unwrap();\n 323|        assert_eq!(row, back, \"a no-cwd row round-trips identically\");\n 324|    }\n 325|\n...\n 354|    fn adapter_field_roundtrips_and_survives_append() {\n*355|        let none_row = SessionEntry {\n 356|            ts: \"t\".to_string(),\n 357|            session_id: \"s\".to_string(),\n 358|            trigger: SessionTrigger::Boot,\n...\n 374|            &perch,\n*375|            &SessionEntry {\n 376|                ts: \"t2\".to_string(),\n 377|                session_id: \"s2\".to_string(),\n 378|                trigger: SessionTrigger::Clear,"}],"details":{"scopePath":"crates/spt-store/src/sessions.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\sessions.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":13,"fileCount":1,"files":["crates/spt-store/src/sessions.rs"],"fileMatches":[{"path":"crates/spt-store/src/sessions.rs","count":13}],"truncated":false,"displayContent":"  58│#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n *59│pub struct SessionEntry {\n  60│    /// RFC3339-UTC instant (see [`crate::timefmt`]).\n  61│    pub ts: String,\n  62│    /// The harness session id that became active.\n    │...\n  98│/// line is skipped, the rest still load (the degrade-don't-crash store stance).\n *99│pub fn read_all(perch: &Path) -> Vec<SessionEntry> {\n 100│    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\n 101│        return Vec::new();\n 102│    };\n    │...\n 109│/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\n*110│pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\n 111│    if k == 0 {\n 112│        return Vec::new();\n 113│    }\n    │...\n 118│\n*119│/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\n 120│/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\n 121│/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\n 122│/// `read_all` index (not the slice index) so it stays comparable across sessions.\n    │...\n 124│// [impl->REQ-DIGEST-CURSOR]\n*125│pub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\n 126│    if k == 0 {\n 127│        return Vec::new();\n 128│    }\n    │...\n 149│// [impl->REQ-TERM-6]\n*150│pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {\n 151│    let mut entries = read_all(perch);\n 152│    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {\n 153│        return Ok(false); // same session re-bound — not a new boundary\n    │...\n 188│\n*189│    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {\n*190│        SessionEntry {\n 191│            ts: crate::timefmt::rfc3339_utc(1_700_000_000),\n 192│            session_id: sid.to_string(),\n 193│            trigger,\n    │...\n 255│        let perch = dir.path().join(\"perch\");\n*256│        let with_cwd = SessionEntry {\n 257│            ts: crate::timefmt::rfc3339_utc(1_700_000_000),\n 258│            session_id: \"s-cwd\".to_string(),\n 259│            trigger: SessionTrigger::Boot,\n    │...\n 308│    fn none_cwd_emits_no_field_and_roundtrips() {\n*309│        let row = SessionEntry {\n 310│            ts: \"t\".to_string(),\n 311│            session_id: \"s\".to_string(),\n 312│            trigger: SessionTrigger::Boot,\n    │...\n 321│        );\n*322│        let back: SessionEntry = serde_json::from_str(&json).unwrap();\n 323│        assert_eq!(row, back, \"a no-cwd row round-trips identically\");\n 324│    }\n 325│\n    │...\n 354│    fn adapter_field_roundtrips_and_survives_append() {\n*355│        let none_row = SessionEntry {\n 356│            ts: \"t\".to_string(),\n 357│            session_id: \"s\".to_string(),\n 358│            trigger: SessionTrigger::Boot,\n    │...\n 374│            &perch,\n*375│            &SessionEntry {\n 376│                ts: \"t2\".to_string(),\n 377│                session_id: \"s2\".to_string(),\n 378│                trigger: SessionTrigger::Clear,"},"isError":false,"timestamp":1784271486365}}
{"type":"message","id":"c38bf416","parentId":"e6cfa62c","timestamp":"2026-07-17T06:58:06.374Z","message":{"role":"toolResult","toolCallId":"call_YKXf1jdVZShj0K61jrp0p4oL|fc_038f28d3e93bbfd4016a59d27e464c8196968b3203fc212d7b","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## linkhost.rs\n 455|    let mut decoder = ShellLinkDecoder::new();\n*456|    loop {\n 457|        match brain.read_event()? {\n 458|            BrokerEvent::NetStreamData {\n 459|                stream_id: sid,\n...\n 684|    let mut decoder = ShellLinkDecoder::new();\n*685|    loop {\n 686|        match brain.read_event()? {\n 687|            BrokerEvent::NetStreamData {\n 688|                stream_id: sid,\n## livehost.rs\n 297|fn read_resume_pid(perch: &Path) -> Option<u32> {\n*298|    std::fs::read_to_string(perch.join(RESUME_PID_FILE))\n 299|        .ok()\n 300|        .and_then(|s| s.trim().parse::<u32>().ok())\n 301|        .filter(|&p| p != 0)\n...\n 918|        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n*919|        let Ok(entries) = std::fs::read_dir(&perch) else {\n 920|            continue;\n 921|        };\n 922|        for entry in entries.flatten() {\n...\n 1004|        }\n*1005|        while !stop.load(Ordering::SeqCst) {\n 1006|            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n 1007|            // unset = normal production, untouched): skip the reconcile poll+body so an\n 1008|            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n...\n 1019|            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n*1020|                std::thread::sleep(Duration::from_millis(100));\n 1021|                continue;\n 1022|            }\n 1023|            let owlery = perch::owlery_dir();\n...\n 1050|                let step = left.min(100);\n*1051|                std::thread::sleep(Duration::from_millis(step));\n 1052|                left -= step;\n 1053|            }\n 1054|        }\n...\n 1116|            }\n*1117|            std::thread::sleep(Duration::from_millis(20));\n 1118|        }\n 1119|        pred()\n 1120|    }\n...\n 1491|                    }\n*1492|                    std::thread::sleep(Duration::from_millis(10));\n 1493|                }\n 1494|                b.expect(\"connect to test broker\")\n 1495|            };\n...\n 1781|                }\n*1782|                std::thread::sleep(std::time::Duration::from_millis(20));\n 1783|            }\n 1784|            assert!(removed, \"could not remove the perch dir for the torn-down case\");\n 1785|            run();\n...\n 1889|        );\n*1890|        let displaced: Vec<_> = std::fs::read_dir(&perch)\n 1891|            .unwrap()\n 1892|            .flatten()\n 1893|            .filter(|e| e.file_name().to_string_lossy().starts_with(\".live-bin.old\"))\n## machineid.rs\n 44|    for path in [\"/etc/machine-id\", \"/var/lib/dbus/machine-id\"] {\n*45|        if let Ok(s) = std::fs::read_to_string(path) {\n 46|            let s = s.trim().to_string();\n 47|            if !s.is_empty() {\n 48|                return Some(s);\n...\n 109|    let path = spt_store::perch::identity_dir().join(\"machine-id\");\n*110|    if let Ok(s) = std::fs::read_to_string(&path) {\n 111|        let s = s.trim().to_string();\n 112|        if !s.is_empty() {\n 113|            return Some(s);\n## nethost.rs\n 1037|        match recv {\n*1038|            RecvHalf::Quic(mut recv) => loop {\n 1039|                await_ring_room(&log, &room).await;\n 1040|                match recv.read_chunk(STREAM_READ_CHUNK).await {\n 1041|                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n...\n 1049|                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n*1050|                loop {\n 1051|                    await_ring_room(&log, &room).await;\n 1052|                    match recv.read(&mut buf).await {\n 1053|                        Ok(0) | Err(_) => {\n...\n 1071|async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n*1072|    loop {\n 1073|        {\n 1074|            let l = log.lock().unwrap();\n 1075|            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n...\n 1289|            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n*1290|            loop {\n 1291|                ivl.tick().await;\n 1292|                canary.store(epoch_ms(), Ordering::Relaxed);\n 1293|            }\n...\n 2130|            }\n*2131|            std::thread::sleep(std::time::Duration::from_millis(5));\n 2132|        }\n 2133|        assert_eq!(b.conn_count(), 1, \"accept loop registered the inbound conn\");\n 2134|        assert_eq!(\n...\n 2196|            }\n*2197|            std::thread::sleep(std::time::Duration::from_millis(5));\n 2198|        }\n 2199|        assert!(\n 2200|            crossed,\n...\n 2273|            }\n*2274|            std::thread::sleep(std::time::Duration::from_millis(5));\n 2275|            if sent.load(AtOrd::Relaxed) >= N {\n 2276|                break; // (shouldn't reach N while we never drain — backpressure)\n 2277|            }\n...\n 2299|            }\n*2300|            std::thread::sleep(std::time::Duration::from_millis(2));\n 2301|        }\n 2302|        writer.join().expect(\"writer joins\");\n 2303|        assert!(finished, \"the tunnel saw EOF after the owner finished\");\n...\n 2405|            }\n*2406|            std::thread::sleep(Duration::from_millis(5));\n 2407|        }\n 2408|        assert_eq!(\n 2409|            pinned.as_deref(),\n...\n 2461|            }\n*2462|            std::thread::sleep(Duration::from_millis(5));\n 2463|        }\n 2464|        assert!(\n 2465|            String::from_utf8_lossy(&got).contains(\"\\\"outcome\\\":\\\"edge\\\"\"),\n...\n 2488|            }\n*2489|            std::thread::sleep(Duration::from_millis(5));\n 2490|        }\n 2491|        assert!(saw, \"the peer stream row registered on b\");\n 2492|\n...\n 2500|            }\n*2501|            std::thread::sleep(Duration::from_millis(10));\n 2502|        }\n 2503|        assert!(swept, \"the closed-watcher swept the dead conn's stream rows\");\n 2504|    }\n...\n 2548|            }\n*2549|            std::thread::sleep(Duration::from_millis(5));\n 2550|        }\n 2551|        panic!(\"seat never poisoned — the dead conn's write should have failed\");\n 2552|    }\n...\n 2789|            }\n*2790|            std::thread::sleep(Duration::from_millis(5));\n 2791|        }\n 2792|        assert!(\n 2793|            lease_a.is_canceled(),\n## notif.rs\n 1169|            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);\n*1170|            let content = loop {\n*1171|                if let Ok(c) = std::fs::read_to_string(&marker) {\n 1172|                    if !c.trim().is_empty() {\n 1173|                        break c;\n 1174|                    }\n...\n 1179|                );\n*1180|                std::thread::sleep(std::time::Duration::from_millis(50));\n 1181|            };\n 1182|            assert_eq!(\n 1183|                content.trim(),\n## pairhost.rs\n 186|        let mut prev_tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();\n*187|        loop {\n 188|            let step = time_step(now_secs());\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...\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...\n 430|            let start = std::time::Instant::now();\n*431|            loop {\n 432|                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {\n 433|                    Ok(conn) => return Ok(conn),\n 434|                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {\n...\n 509|{\n*510|    loop {\n 511|        match probe().await {\n 512|            Ok(a) => return Ok(a),\n 513|            Err(e) if start.elapsed() >= deadline => {\n## presence.rs\n 59|    let mut out = BTreeMap::new();\n*60|    let Ok(entries) = std::fs::read_dir(dir) else {\n 61|        return out;\n 62|    };\n 63|    for entry in entries.flatten() {\n...\n 70|        };\n*71|        let Ok(raw) = std::fs::read_to_string(&path) else {\n 72|            continue;\n 73|        };\n 74|        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else {\n## projwriter.rs\n 48|\n*49|/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n 50|/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\n 51|const POLL_MS: u64 = 500;\n 52|/// Debounce window after the first observed event — a burst (an adapter\n...\n 140|pub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n*141|    let raw = std::fs::read_to_string(path).ok()?;\n 142|    serde_json::from_str(&raw).ok()\n 143|}\n 144|\n...\n 559|fn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\n*560|    let Ok(entries) = std::fs::read_dir(owlery) else {\n 561|        return Vec::new();\n 562|    };\n 563|    let mut out = Vec::new();\n...\n 645|        let mut since_periodic = Duration::ZERO;\n*646|        while !stop.load(Ordering::SeqCst) {\n 647|            // Sleep one poll slice (sliced so a stop lands promptly).\n 648|            let mut left = poll.as_millis() as u64;\n 649|            while left > 0 && !stop.load(Ordering::SeqCst) {\n 650|                let step = left.min(100);\n*651|                std::thread::sleep(Duration::from_millis(step));\n 652|                left -= step;\n 653|            }\n 654|            if stop.load(Ordering::SeqCst) {\n...\n 668|                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*669|                std::thread::sleep(debounce);\n 670|                engine.stats.pending_refresh = true;\n 671|                engine.write_stats();\n 672|            }\n...\n 845|        first.reconcile(&global(), false).unwrap();\n*846|        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n 847|\n 848|        // The restarted brain: fresh engine, warm disk.\n 849|        let mut warm = WriterEngine::new(fx.paths.clone());\n...\n 857|        assert_eq!(\n*858|            std::fs::read(&fx.paths.index_path).unwrap(),\n 859|            bytes_before,\n 860|            \"the snapshot is byte-untouched — no rewrite, no generated_ms churn\"\n 861|        );\n...\n 874|        engine.reconcile(&global(), false).unwrap();\n*875|        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n 876|\n 877|        // Wreck the store: the seed git DIR becomes a plain FILE — it still\n 878|        // `exists()`, but opening it as a repo fails loudly (and cannot be\n...\n 886|        assert_eq!(\n*887|            std::fs::read(&fx.paths.index_path).unwrap(),\n 888|            bytes_before,\n 889|            \"a failed cycle never touches the published snapshot\"\n 890|        );\n...\n 1004|        // and the attribution follows the new identity.\n*1005|        std::thread::sleep(Duration::from_millis(30)); // distinct mtime/len either way\n 1006|        spt_store::gitrun::run_git_ok(\n 1007|            &[\n 1008|                \"-C\",\n...\n 1082|                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n*1083|                std::thread::sleep(Duration::from_millis(25));\n 1084|            }\n 1085|            let enums_after_boot = stats().unwrap().cumulative.branch_enumerations;\n 1086|\n...\n 1098|                assert!(Instant::now() < consumed, \"burst never consumed\");\n*1099|                std::thread::sleep(Duration::from_millis(25));\n 1100|            }\n 1101|            let s = stats().unwrap();\n 1102|            stop.store(true, Ordering::SeqCst);\n## propagate.rs\n 119|\n*120|    loop {\n 121|        match brain.read_event()? {\n 122|            BrokerEvent::NetStreamData {\n 123|                stream_id: sid,\n...\n 378|    let mut deadline = brain.reply_read_deadline();\n*379|    loop {\n 380|        let ev = brain.read_peer_reply_until(deadline)?;\n 381|        // Re-arm the budget on each chunk on OUR stream: a whole reply-read budget\n 382|        // of NO progress is a wedge, a streaming pull is healthy (REQ-HAZARD-PUMP-IPC-DEADLINE).\n...\n 473|                            drop(file.take()); // flush before reading back\n*474|                            let artifact = std::fs::read(&artifact_path).unwrap_or_default();\n 475|                            let _ = std::fs::remove_file(&artifact_path);\n 476|                            let offer = signed.take().ok_or_else(|| {\n 477|                                io::Error::other(\"peer sent Done before any Offer\")\n...\n 567|    let mut decoder = UpdDecoder::new();\n*568|    loop {\n 569|        match brain.read_event()? {\n 570|            BrokerEvent::NetStreamData {\n 571|                stream_id: sid,\n## reap.rs\n 231|            }\n*232|            std::thread::sleep(Duration::from_millis(20));\n 233|        }\n 234|        false\n 235|    }\n...\n 245|            }\n*246|            std::thread::sleep(Duration::from_millis(20));\n 247|        }\n 248|        false\n 249|    }\n...\n 258|        for _ in 0..250 {\n*259|            if let Ok(s) = std::fs::read_to_string(p) {\n 260|                if let Ok(pid) = s.trim().parse::<u32>() {\n 261|                    if pid != 0 {\n 262|                        return Some(pid);\n...\n 265|            }\n*266|            std::thread::sleep(Duration::from_millis(20));\n 267|        }\n 268|        None\n 269|    }\n## registryhost.rs\n 792|        let path = Self::repair_evict_path(&self.snapshot_dir);\n*793|        let Ok(raw) = std::fs::read_to_string(&path) else {\n 794|            return 0;\n 795|        };\n 796|        let _ = std::fs::remove_file(&path);\n...\n 986|    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);\n*987|    let Some(mut reg) = std::fs::read_to_string(&snap_path)\n 988|        .ok()\n 989|        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())\n 990|    else {\n...\n 1025|    let marker = RegistryHost::repair_evict_path(snap_dir);\n*1026|    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)\n 1027|        .ok()\n 1028|        .and_then(|s| serde_json::from_str(&s).ok())\n 1029|        .unwrap_or_default();\n...\n 1084|            Err(_) if attempt < 4 => {\n*1085|                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));\n 1086|            }\n 1087|            Err(e) => return Err(e),\n 1088|        }\n...\n 1363|        let on_disk: SubnetRegistry = serde_json::from_str(\n*1364|            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, \"home\")).unwrap(),\n 1365|        )\n 1366|        .unwrap();\n 1367|        assert!(\n...\n 1695|        let reg: SubnetRegistry =\n*1696|            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 1697|        assert_eq!(reg.node_labels().collect::<Vec<_>>(), vec![(\"bb22\", \"RENAMED\")]);\n 1698|        assert!(reg.endpoint_ids().next().is_none(), \"no phantom endpoint row\");\n 1699|    }\n...\n 1986|            let snap = RegistryHost::snapshot_path(&home.join(\"registry\"), \"adv\");\n*1987|            let bytes = std::fs::read(&snap).expect(\"snapshot written\");\n 1988|            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect(\"parses\");\n 1989|            assert!(!parsed.instances(\"vis-ag\").is_empty());\n 1990|        });\n...\n 2010|        let heard: BTreeMap<String, u64> =\n*2011|            serde_json::from_slice(&std::fs::read(&heard_file).expect(\"mirror written\")).unwrap();\n 2012|        let first = *heard.get(\"bb22\").expect(\"origin stamped\");\n 2013|        assert!(first > 0);\n 2014|        assert!(\n...\n 2019|        // A Stale verdict still refreshes the stamp (the sender is alive).\n*2020|        std::thread::sleep(std::time::Duration::from_millis(15));\n 2021|        let v = h.apply_feed(\"bb22\", &[upd(\"home\", \"ling\", \"bb22\", 5)], &p);\n 2022|        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);\n 2023|        let heard: BTreeMap<String, u64> =\n*2024|            serde_json::from_slice(&std::fs::read(&heard_file).unwrap()).unwrap();\n 2025|        assert!(\n 2026|            *heard.get(\"bb22\").unwrap() > first,\n 2027|            \"stale merge still refreshes recency\"\n...\n 2045|            let heard: BTreeMap<String, u64> = serde_json::from_slice(\n*2046|                &std::fs::read(RegistryHost::heard_path(&dir.path().join(\"registry\"))).unwrap(),\n 2047|            )\n 2048|            .unwrap();\n 2049|            *heard.get(\"bb22\").unwrap()\n...\n 2075|        let parsed: SubnetRegistry =\n*2076|            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2077|        assert!(\n 2078|            parsed.instances(\"sergey\").is_empty(),\n 2079|            \"snapshot rewritten without the ghost\"\n...\n 2124|        let parsed: SubnetRegistry =\n*2125|            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2126|        assert!(\n 2127|            parsed.instances(\"ghost\").is_empty(),\n 2128|            \"snapshot rewritten without the aged Offline ghost\"\n## relay.rs\n 90|        self.drain_backlog(&mut sink);\n*91|        while !stop.load(Ordering::Relaxed) {\n 92|            self.pump_once(&mut sink);\n 93|        }\n 94|    }\n## relcache.rs\n 253|    pub fn staged_platform(&self) -> Option<String> {\n*254|        let s = std::fs::read_to_string(self.platform_path()).ok()?;\n 255|        serde_json::from_str::<serde_json::Value>(&s)\n 256|            .ok()?\n 257|            .get(\"platform\")?\n...\n 305|    pub fn staged_docs(&self) -> Option<Vec<u8>> {\n*306|        std::fs::read(self.docs_bundle_path()).ok()\n 307|    }\n 308|\n 309|    /// Drop the staged docs bundle (after a successful apply landed it, or to\n...\n 331|        let signed: SignedRelease =\n*332|            serde_json::from_str(&std::fs::read_to_string(self.release_path()).ok()?).ok()?;\n 333|        if let Ok(set_meta) = serde_json::from_str::<UpdateSetMetadata>(&signed.metadata_json) {\n 334|            if set_meta\n 335|                .artifacts\n...\n 354|    pub fn load_artifact(&self) -> io::Result<Vec<u8>> {\n*355|        std::fs::read(self.artifact_path())\n 356|    }\n 357|\n 358|    /// Load the bytes for `platform` from the staged update. Legacy releases\n...\n 363|            Some(StagedUpdate::Single(_)) => self.load_artifact(),\n*364|            Some(StagedUpdate::Set(_)) => std::fs::read(self.artifact_path_for_platform(platform)),\n 365|            None => Err(io::Error::new(\n 366|                io::ErrorKind::NotFound,\n 367|                \"no complete staged update\",\n...\n 436|    pub fn applied_version(&self) -> Option<u64> {\n*437|        let s = std::fs::read_to_string(self.applied_path()).ok()?;\n 438|        serde_json::from_str::<serde_json::Value>(&s)\n 439|            .ok()?\n 440|            .get(\"version\")?\n...\n 458|    pub fn last_outcome(&self) -> Option<String> {\n*459|        let s = std::fs::read_to_string(self.last_outcome_path()).ok()?;\n 460|        Some(\n 461|            serde_json::from_str::<serde_json::Value>(&s)\n 462|                .ok()?\n...\n 487|    pub fn applied_state(&self) -> Option<AppliedRecord> {\n*488|        let s = std::fs::read_to_string(self.applied_state_path()).ok()?;\n 489|        serde_json::from_str(&s).ok()\n 490|    }\n 491|}\n## release.rs\n 377|    pub fn load_from(path: &std::path::Path, current_version: u64, now_ms: u64) -> VerifyPolicy {\n*378|        let file: ReleaseKeysFile = std::fs::read_to_string(path)\n 379|            .ok()\n 380|            .and_then(|raw| serde_json::from_str(&raw).ok())\n 381|            .unwrap_or_default();\n## resthost.rs\n 83|    let mut decoder = RestDecoder::new();\n*84|    loop {\n 85|        match brain.read_event()? {\n 86|            BrokerEvent::NetStreamData {\n 87|                stream_id: sid,\n...\n 203|    let mut decoder = RestDecoder::new();\n*204|    loop {\n 205|        match brain.read_event()? {\n 206|            BrokerEvent::NetStreamData {\n 207|                stream_id: sid,\n## seedmap.rs\n 125|    let stop = Arc::new(AtomicBool::new(false));\n*126|    loop {\n 127|        let conn = listener.accept()?;\n 128|        if stop.load(Ordering::SeqCst) {\n 129|            // (A) REQ-HAZARD-DAEMON-STOP-BARRIER: release the throwaway wake-conn AND\n...\n 159|    recv_hello(&mut conn, Role::Brain)?;\n*160|    loop {\n 161|        let env = match read_frame(&mut conn) {\n 162|            Ok(e) => e,\n 163|            Err(_) => return Ok(()), // peer hung up — connection done\n...\n 282|    });\n*283|    match rx.recv_timeout(bound) {\n 284|        Ok(res) => res,\n 285|        Err(_) => Err(io::Error::new(\n 286|            io::ErrorKind::TimedOut,\n...\n 296|/// already-dying name wedged the whole convoy). Every dial on the stop path must be\n*297|/// bounded, not just the barrier ping. Same worker-thread + recv_timeout shape as\n 298|/// [`ping_bounded`]: a busy / pinned name degrades to a DISTINCT [`io::ErrorKind::TimedOut`],\n 299|/// a gone name still returns its real NotFound / connection-refused. Scope: the stop\n 300|/// path only — put / take / is_running stay unbounded pending REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED.\n...\n 310|    });\n*311|    match rx.recv_timeout(bound) {\n 312|        Ok(res) => res,\n 313|        Err(_) => Err(io::Error::new(\n 314|            io::ErrorKind::TimedOut,\n...\n 363|    let deadline = std::time::Instant::now() + STOP_BARRIER_TIMEOUT;\n*364|    loop {\n 365|        match ping_bounded(name, PING_BARRIER_BOUND) {\n 366|            Err(e) if e.kind() != io::ErrorKind::TimedOut => return Ok(()),\n 367|            _ => {} // PONG (still serving) or TimedOut (still tearing down) — keep waiting\n...\n 374|        }\n*375|        std::thread::sleep(STOP_BARRIER_POLL);\n 376|    }\n 377|}\n 378|\n...\n 451|            }\n*452|            std::thread::sleep(Duration::from_millis(5));\n 453|        }\n 454|        panic!(\"seed daemon did not come up\");\n 455|    }\n...\n 481|                }\n*482|                std::thread::sleep(Duration::from_millis(5));\n 483|            }\n 484|            panic!(\"seed daemon still answering after stop\");\n 485|        });\n## serveprobe.rs\n 75|    let mut decoder = ServeProbeDecoder::new();\n*76|    loop {\n 77|        match brain.read_event()? {\n 78|            BrokerEvent::NetStreamData {\n 79|                stream_id: sid,\n...\n 122|    let mut decoder = ServeProbeDecoder::new();\n*123|    loop {\n 124|        match brain.read_event()? {\n 125|            BrokerEvent::NetStreamData {\n 126|                stream_id: sid,\n## shellchan.rs\n 211|        // Drain interleaved output until this row's exactly-once ack.\n*212|        loop {\n 213|            match brain.read_event().map_err(|e| format!(\"read: {e}\"))? {\n 214|                BrokerEvent::Applied { op_id, .. } if op_id == row_id as u64 => break,\n 215|                BrokerEvent::Exit { .. } => return Err(\"shell binary exited mid-delivery\".into()),\n\n## pump/\n### health.rs\n 79|    pub fn load_from(path: &Path) -> PumpHealth {\n*80|        match std::fs::read_to_string(path) {\n 81|            Ok(s) => serde_json::from_str(&s).unwrap_or_default(),\n 82|            Err(_) => PumpHealth::default(),\n 83|        }\n### mod.rs\n 593|\n*594|    while !stop.load(Ordering::Relaxed) {\n 595|        let now = Instant::now();\n 596|        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\n 597|        // while the loop is ALIVE — including idle ticks — never per work round,\n...\n 611|        if !any_due {\n*612|            thread::sleep(TICK);\n 613|            continue;\n 614|        }\n 615|\n...\n 704|        let round_deadline = round_start + PEER_REPLY_READ_BUDGET + ROUND_DRAIN_MARGIN;\n*705|        loop {\n 706|            let read_deadline = if pending.is_empty() {\n 707|                Instant::now() // no outstanding submits — sweep only what is queued\n 708|            } else {\n...\n 1136|        }\n*1137|        thread::sleep(Duration::from_millis(10));\n 1138|    }\n 1139|    Err(last)\n 1140|}\n...\n 1205|    let mut backoff = base;\n*1206|    while !stop.load(Ordering::Relaxed) {\n 1207|        let started = Instant::now();\n 1208|        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut body));\n 1209|        if stop.load(Ordering::Relaxed) {\n...\n 1233|        while slept < backoff && !stop.load(Ordering::Relaxed) {\n*1234|            thread::sleep(TICK.min(backoff - slept));\n 1235|            slept += TICK;\n 1236|        }\n 1237|    }\n...\n 1809|        write_heartbeat(&path);\n*1810|        let first: u64 = std::fs::read_to_string(&path)\n 1811|            .expect(\"heartbeat written\")\n 1812|            .trim()\n 1813|            .parse()\n...\n 1815|        assert!(first > 0, \"a real wall-clock stamp\");\n*1816|        thread::sleep(Duration::from_millis(15));\n 1817|        write_heartbeat(&path);\n*1818|        let second: u64 = std::fs::read_to_string(&path)\n 1819|            .unwrap()\n 1820|            .trim()\n 1821|            .parse()\n\nShowing files 21-40 of 48+. Use skip=40 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":104,"fileCount":20,"files":["crates/spt-daemon/src/linkhost.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/machineid.rs","crates/spt-daemon/src/nethost.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/propagate.rs","crates/spt-daemon/src/pump/health.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/reap.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/relay.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/release.rs","crates/spt-daemon/src/resthost.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/serveprobe.rs","crates/spt-daemon/src/shellchan.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/linkhost.rs","count":2},{"path":"crates/spt-daemon/src/livehost.rs","count":9},{"path":"crates/spt-daemon/src/machineid.rs","count":2},{"path":"crates/spt-daemon/src/nethost.rs","count":14},{"path":"crates/spt-daemon/src/notif.rs","count":3},{"path":"crates/spt-daemon/src/pairhost.rs","count":4},{"path":"crates/spt-daemon/src/presence.rs","count":2},{"path":"crates/spt-daemon/src/projwriter.rs","count":13},{"path":"crates/spt-daemon/src/propagate.rs","count":4},{"path":"crates/spt-daemon/src/pump/health.rs","count":1},{"path":"crates/spt-daemon/src/pump/mod.rs","count":9},{"path":"crates/spt-daemon/src/reap.rs","count":4},{"path":"crates/spt-daemon/src/registryhost.rs","count":13},{"path":"crates/spt-daemon/src/relay.rs","count":1},{"path":"crates/spt-daemon/src/relcache.rs","count":8},{"path":"crates/spt-daemon/src/release.rs","count":1},{"path":"crates/spt-daemon/src/resthost.rs","count":2},{"path":"crates/spt-daemon/src/seedmap.rs","count":9},{"path":"crates/spt-daemon/src/serveprobe.rs","count":2},{"path":"crates/spt-daemon/src/shellchan.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## linkhost.rs\n 455│    let mut decoder = ShellLinkDecoder::new();\n*456│    loop {\n 457│        match brain.read_event()? {\n 458│            BrokerEvent::NetStreamData {\n 459│                stream_id: sid,\n    │...\n 684│    let mut decoder = ShellLinkDecoder::new();\n*685│    loop {\n 686│        match brain.read_event()? {\n 687│            BrokerEvent::NetStreamData {\n 688│                stream_id: sid,\n## livehost.rs\n  297│fn read_resume_pid(perch: &Path) -> Option<u32> {\n *298│    std::fs::read_to_string(perch.join(RESUME_PID_FILE))\n  299│        .ok()\n  300│        .and_then(|s| s.trim().parse::<u32>().ok())\n  301│        .filter(|&p| p != 0)\n     │...\n  918│        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n *919│        let Ok(entries) = std::fs::read_dir(&perch) else {\n  920│            continue;\n  921│        };\n  922│        for entry in entries.flatten() {\n     │...\n 1004│        }\n*1005│        while !stop.load(Ordering::SeqCst) {\n 1006│            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n 1007│            // unset = normal production, untouched): skip the reconcile poll+body so an\n 1008│            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n     │...\n 1019│            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n*1020│                std::thread::sleep(Duration::from_millis(100));\n 1021│                continue;\n 1022│            }\n 1023│            let owlery = perch::owlery_dir();\n     │...\n 1050│                let step = left.min(100);\n*1051│                std::thread::sleep(Duration::from_millis(step));\n 1052│                left -= step;\n 1053│            }\n 1054│        }\n     │...\n 1116│            }\n*1117│            std::thread::sleep(Duration::from_millis(20));\n 1118│        }\n 1119│        pred()\n 1120│    }\n     │...\n 1491│                    }\n*1492│                    std::thread::sleep(Duration::from_millis(10));\n 1493│                }\n 1494│                b.expect(\"connect to test broker\")\n 1495│            };\n     │...\n 1781│                }\n*1782│                std::thread::sleep(std::time::Duration::from_millis(20));\n 1783│            }\n 1784│            assert!(removed, \"could not remove the perch dir for the torn-down case\");\n 1785│            run();\n     │...\n 1889│        );\n*1890│        let displaced: Vec<_> = std::fs::read_dir(&perch)\n 1891│            .unwrap()\n 1892│            .flatten()\n 1893│            .filter(|e| e.file_name().to_string_lossy().starts_with(\".live-bin.old\"))\n## machineid.rs\n  44│    for path in [\"/etc/machine-id\", \"/var/lib/dbus/machine-id\"] {\n *45│        if let Ok(s) = std::fs::read_to_string(path) {\n  46│            let s = s.trim().to_string();\n  47│            if !s.is_empty() {\n  48│                return Some(s);\n    │...\n 109│    let path = spt_store::perch::identity_dir().join(\"machine-id\");\n*110│    if let Ok(s) = std::fs::read_to_string(&path) {\n 111│        let s = s.trim().to_string();\n 112│        if !s.is_empty() {\n 113│            return Some(s);\n## nethost.rs\n 1037│        match recv {\n*1038│            RecvHalf::Quic(mut recv) => loop {\n 1039│                await_ring_room(&log, &room).await;\n 1040│                match recv.read_chunk(STREAM_READ_CHUNK).await {\n 1041│                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n     │...\n 1049│                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n*1050│                loop {\n 1051│                    await_ring_room(&log, &room).await;\n 1052│                    match recv.read(&mut buf).await {\n 1053│                        Ok(0) | Err(_) => {\n     │...\n 1071│async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n*1072│    loop {\n 1073│        {\n 1074│            let l = log.lock().unwrap();\n 1075│            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n     │...\n 1289│            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n*1290│            loop {\n 1291│                ivl.tick().await;\n 1292│                canary.store(epoch_ms(), Ordering::Relaxed);\n 1293│            }\n     │...\n 2130│            }\n*2131│            std::thread::sleep(std::time::Duration::from_millis(5));\n 2132│        }\n 2133│        assert_eq!(b.conn_count(), 1, \"accept loop registered the inbound conn\");\n 2134│        assert_eq!(\n     │...\n 2196│            }\n*2197│            std::thread::sleep(std::time::Duration::from_millis(5));\n 2198│        }\n 2199│        assert!(\n 2200│            crossed,\n     │...\n 2273│            }\n*2274│            std::thread::sleep(std::time::Duration::from_millis(5));\n 2275│            if sent.load(AtOrd::Relaxed) >= N {\n 2276│                break; // (shouldn't reach N while we never drain — backpressure)\n 2277│            }\n     │...\n 2299│            }\n*2300│            std::thread::sleep(std::time::Duration::from_millis(2));\n 2301│        }\n 2302│        writer.join().expect(\"writer joins\");\n 2303│        assert!(finished, \"the tunnel saw EOF after the owner finished\");\n     │...\n 2405│            }\n*2406│            std::thread::sleep(Duration::from_millis(5));\n 2407│        }\n 2408│        assert_eq!(\n 2409│            pinned.as_deref(),\n     │...\n 2461│            }\n*2462│            std::thread::sleep(Duration::from_millis(5));\n 2463│        }\n 2464│        assert!(\n 2465│            String::from_utf8_lossy(&got).contains(\"\\\"outcome\\\":\\\"edge\\\"\"),\n     │...\n 2488│            }\n*2489│            std::thread::sleep(Duration::from_millis(5));\n 2490│        }\n 2491│        assert!(saw, \"the peer stream row registered on b\");\n 2492│\n     │...\n 2500│            }\n*2501│            std::thread::sleep(Duration::from_millis(10));\n 2502│        }\n 2503│        assert!(swept, \"the closed-watcher swept the dead conn's stream rows\");\n 2504│    }\n     │...\n 2548│            }\n*2549│            std::thread::sleep(Duration::from_millis(5));\n 2550│        }\n 2551│        panic!(\"seat never poisoned — the dead conn's write should have failed\");\n 2552│    }\n     │...\n 2789│            }\n*2790│            std::thread::sleep(Duration::from_millis(5));\n 2791│        }\n 2792│        assert!(\n 2793│            lease_a.is_canceled(),\n## notif.rs\n 1169│            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);\n*1170│            let content = loop {\n*1171│                if let Ok(c) = std::fs::read_to_string(&marker) {\n 1172│                    if !c.trim().is_empty() {\n 1173│                        break c;\n 1174│                    }\n     │...\n 1179│                );\n*1180│                std::thread::sleep(std::time::Duration::from_millis(50));\n 1181│            };\n 1182│            assert_eq!(\n 1183│                content.trim(),\n## pairhost.rs\n 186│        let mut prev_tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();\n*187│        loop {\n 188│            let step = time_step(now_secs());\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    │...\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    │...\n 430│            let start = std::time::Instant::now();\n*431│            loop {\n 432│                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {\n 433│                    Ok(conn) => return Ok(conn),\n 434│                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {\n    │...\n 509│{\n*510│    loop {\n 511│        match probe().await {\n 512│            Ok(a) => return Ok(a),\n 513│            Err(e) if start.elapsed() >= deadline => {\n## presence.rs\n 59│    let mut out = BTreeMap::new();\n*60│    let Ok(entries) = std::fs::read_dir(dir) else {\n 61│        return out;\n 62│    };\n 63│    for entry in entries.flatten() {\n   │...\n 70│        };\n*71│        let Ok(raw) = std::fs::read_to_string(&path) else {\n 72│            continue;\n 73│        };\n 74│        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else {\n## projwriter.rs\n   48│\n  *49│/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n   50│/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\n   51│const POLL_MS: u64 = 500;\n   52│/// Debounce window after the first observed event — a burst (an adapter\n     │...\n  140│pub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n *141│    let raw = std::fs::read_to_string(path).ok()?;\n  142│    serde_json::from_str(&raw).ok()\n  143│}\n  144│\n     │...\n  559│fn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\n *560│    let Ok(entries) = std::fs::read_dir(owlery) else {\n  561│        return Vec::new();\n  562│    };\n  563│    let mut out = Vec::new();\n     │...\n  645│        let mut since_periodic = Duration::ZERO;\n *646│        while !stop.load(Ordering::SeqCst) {\n  647│            // Sleep one poll slice (sliced so a stop lands promptly).\n  648│            let mut left = poll.as_millis() as u64;\n  649│            while left > 0 && !stop.load(Ordering::SeqCst) {\n  650│                let step = left.min(100);\n *651│                std::thread::sleep(Duration::from_millis(step));\n  652│                left -= step;\n  653│            }\n  654│            if stop.load(Ordering::SeqCst) {\n     │...\n  668│                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n *669│                std::thread::sleep(debounce);\n  670│                engine.stats.pending_refresh = true;\n  671│                engine.write_stats();\n  672│            }\n     │...\n  845│        first.reconcile(&global(), false).unwrap();\n *846│        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n  847│\n  848│        // The restarted brain: fresh engine, warm disk.\n  849│        let mut warm = WriterEngine::new(fx.paths.clone());\n     │...\n  857│        assert_eq!(\n *858│            std::fs::read(&fx.paths.index_path).unwrap(),\n  859│            bytes_before,\n  860│            \"the snapshot is byte-untouched — no rewrite, no generated_ms churn\"\n  861│        );\n     │...\n  874│        engine.reconcile(&global(), false).unwrap();\n *875│        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n  876│\n  877│        // Wreck the store: the seed git DIR becomes a plain FILE — it still\n  878│        // `exists()`, but opening it as a repo fails loudly (and cannot be\n     │...\n  886│        assert_eq!(\n *887│            std::fs::read(&fx.paths.index_path).unwrap(),\n  888│            bytes_before,\n  889│            \"a failed cycle never touches the published snapshot\"\n  890│        );\n     │...\n 1004│        // and the attribution follows the new identity.\n*1005│        std::thread::sleep(Duration::from_millis(30)); // distinct mtime/len either way\n 1006│        spt_store::gitrun::run_git_ok(\n 1007│            &[\n 1008│                \"-C\",\n     │...\n 1082│                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n*1083│                std::thread::sleep(Duration::from_millis(25));\n 1084│            }\n 1085│            let enums_after_boot = stats().unwrap().cumulative.branch_enumerations;\n 1086│\n     │...\n 1098│                assert!(Instant::now() < consumed, \"burst never consumed\");\n*1099│                std::thread::sleep(Duration::from_millis(25));\n 1100│            }\n 1101│            let s = stats().unwrap();\n 1102│            stop.store(true, Ordering::SeqCst);\n## propagate.rs\n 119│\n*120│    loop {\n 121│        match brain.read_event()? {\n 122│            BrokerEvent::NetStreamData {\n 123│                stream_id: sid,\n    │...\n 378│    let mut deadline = brain.reply_read_deadline();\n*379│    loop {\n 380│        let ev = brain.read_peer_reply_until(deadline)?;\n 381│        // Re-arm the budget on each chunk on OUR stream: a whole reply-read budget\n 382│        // of NO progress is a wedge, a streaming pull is healthy (REQ-HAZARD-PUMP-IPC-DEADLINE).\n    │...\n 473│                            drop(file.take()); // flush before reading back\n*474│                            let artifact = std::fs::read(&artifact_path).unwrap_or_default();\n 475│                            let _ = std::fs::remove_file(&artifact_path);\n 476│                            let offer = signed.take().ok_or_else(|| {\n 477│                                io::Error::other(\"peer sent Done before any Offer\")\n    │...\n 567│    let mut decoder = UpdDecoder::new();\n*568│    loop {\n 569│        match brain.read_event()? {\n 570│            BrokerEvent::NetStreamData {\n 571│                stream_id: sid,\n## reap.rs\n 231│            }\n*232│            std::thread::sleep(Duration::from_millis(20));\n 233│        }\n 234│        false\n 235│    }\n    │...\n 245│            }\n*246│            std::thread::sleep(Duration::from_millis(20));\n 247│        }\n 248│        false\n 249│    }\n    │...\n 258│        for _ in 0..250 {\n*259│            if let Ok(s) = std::fs::read_to_string(p) {\n 260│                if let Ok(pid) = s.trim().parse::<u32>() {\n 261│                    if pid != 0 {\n 262│                        return Some(pid);\n    │...\n 265│            }\n*266│            std::thread::sleep(Duration::from_millis(20));\n 267│        }\n 268│        None\n 269│    }\n## registryhost.rs\n  792│        let path = Self::repair_evict_path(&self.snapshot_dir);\n *793│        let Ok(raw) = std::fs::read_to_string(&path) else {\n  794│            return 0;\n  795│        };\n  796│        let _ = std::fs::remove_file(&path);\n     │...\n  986│    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);\n *987│    let Some(mut reg) = std::fs::read_to_string(&snap_path)\n  988│        .ok()\n  989│        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())\n  990│    else {\n     │...\n 1025│    let marker = RegistryHost::repair_evict_path(snap_dir);\n*1026│    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)\n 1027│        .ok()\n 1028│        .and_then(|s| serde_json::from_str(&s).ok())\n 1029│        .unwrap_or_default();\n     │...\n 1084│            Err(_) if attempt < 4 => {\n*1085│                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));\n 1086│            }\n 1087│            Err(e) => return Err(e),\n 1088│        }\n     │...\n 1363│        let on_disk: SubnetRegistry = serde_json::from_str(\n*1364│            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, \"home\")).unwrap(),\n 1365│        )\n 1366│        .unwrap();\n 1367│        assert!(\n     │...\n 1695│        let reg: SubnetRegistry =\n*1696│            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 1697│        assert_eq!(reg.node_labels().collect::<Vec<_>>(), vec![(\"bb22\", \"RENAMED\")]);\n 1698│        assert!(reg.endpoint_ids().next().is_none(), \"no phantom endpoint row\");\n 1699│    }\n     │...\n 1986│            let snap = RegistryHost::snapshot_path(&home.join(\"registry\"), \"adv\");\n*1987│            let bytes = std::fs::read(&snap).expect(\"snapshot written\");\n 1988│            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect(\"parses\");\n 1989│            assert!(!parsed.instances(\"vis-ag\").is_empty());\n 1990│        });\n     │...\n 2010│        let heard: BTreeMap<String, u64> =\n*2011│            serde_json::from_slice(&std::fs::read(&heard_file).expect(\"mirror written\")).unwrap();\n 2012│        let first = *heard.get(\"bb22\").expect(\"origin stamped\");\n 2013│        assert!(first > 0);\n 2014│        assert!(\n     │...\n 2019│        // A Stale verdict still refreshes the stamp (the sender is alive).\n*2020│        std::thread::sleep(std::time::Duration::from_millis(15));\n 2021│        let v = h.apply_feed(\"bb22\", &[upd(\"home\", \"ling\", \"bb22\", 5)], &p);\n 2022│        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);\n 2023│        let heard: BTreeMap<String, u64> =\n*2024│            serde_json::from_slice(&std::fs::read(&heard_file).unwrap()).unwrap();\n 2025│        assert!(\n 2026│            *heard.get(\"bb22\").unwrap() > first,\n 2027│            \"stale merge still refreshes recency\"\n     │...\n 2045│            let heard: BTreeMap<String, u64> = serde_json::from_slice(\n*2046│                &std::fs::read(RegistryHost::heard_path(&dir.path().join(\"registry\"))).unwrap(),\n 2047│            )\n 2048│            .unwrap();\n 2049│            *heard.get(\"bb22\").unwrap()\n     │...\n 2075│        let parsed: SubnetRegistry =\n*2076│            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2077│        assert!(\n 2078│            parsed.instances(\"sergey\").is_empty(),\n 2079│            \"snapshot rewritten without the ghost\"\n     │...\n 2124│        let parsed: SubnetRegistry =\n*2125│            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2126│        assert!(\n 2127│            parsed.instances(\"ghost\").is_empty(),\n 2128│            \"snapshot rewritten without the aged Offline ghost\"\n## relay.rs\n 90│        self.drain_backlog(&mut sink);\n*91│        while !stop.load(Ordering::Relaxed) {\n 92│            self.pump_once(&mut sink);\n 93│        }\n 94│    }\n## relcache.rs\n 253│    pub fn staged_platform(&self) -> Option<String> {\n*254│        let s = std::fs::read_to_string(self.platform_path()).ok()?;\n 255│        serde_json::from_str::<serde_json::Value>(&s)\n 256│            .ok()?\n 257│            .get(\"platform\")?\n    │...\n 305│    pub fn staged_docs(&self) -> Option<Vec<u8>> {\n*306│        std::fs::read(self.docs_bundle_path()).ok()\n 307│    }\n 308│\n 309│    /// Drop the staged docs bundle (after a successful apply landed it, or to\n    │...\n 331│        let signed: SignedRelease =\n*332│            serde_json::from_str(&std::fs::read_to_string(self.release_path()).ok()?).ok()?;\n 333│        if let Ok(set_meta) = serde_json::from_str::<UpdateSetMetadata>(&signed.metadata_json) {\n 334│            if set_meta\n 335│                .artifacts\n    │...\n 354│    pub fn load_artifact(&self) -> io::Result<Vec<u8>> {\n*355│        std::fs::read(self.artifact_path())\n 356│    }\n 357│\n 358│    /// Load the bytes for `platform` from the staged update. Legacy releases\n    │...\n 363│            Some(StagedUpdate::Single(_)) => self.load_artifact(),\n*364│            Some(StagedUpdate::Set(_)) => std::fs::read(self.artifact_path_for_platform(platform)),\n 365│            None => Err(io::Error::new(\n 366│                io::ErrorKind::NotFound,\n 367│                \"no complete staged update\",\n    │...\n 436│    pub fn applied_version(&self) -> Option<u64> {\n*437│        let s = std::fs::read_to_string(self.applied_path()).ok()?;\n 438│        serde_json::from_str::<serde_json::Value>(&s)\n 439│            .ok()?\n 440│            .get(\"version\")?\n    │...\n 458│    pub fn last_outcome(&self) -> Option<String> {\n*459│        let s = std::fs::read_to_string(self.last_outcome_path()).ok()?;\n 460│        Some(\n 461│            serde_json::from_str::<serde_json::Value>(&s)\n 462│                .ok()?\n    │...\n 487│    pub fn applied_state(&self) -> Option<AppliedRecord> {\n*488│        let s = std::fs::read_to_string(self.applied_state_path()).ok()?;\n 489│        serde_json::from_str(&s).ok()\n 490│    }\n 491│}\n## release.rs\n 377│    pub fn load_from(path: &std::path::Path, current_version: u64, now_ms: u64) -> VerifyPolicy {\n*378│        let file: ReleaseKeysFile = std::fs::read_to_string(path)\n 379│            .ok()\n 380│            .and_then(|raw| serde_json::from_str(&raw).ok())\n 381│            .unwrap_or_default();\n## resthost.rs\n  83│    let mut decoder = RestDecoder::new();\n *84│    loop {\n  85│        match brain.read_event()? {\n  86│            BrokerEvent::NetStreamData {\n  87│                stream_id: sid,\n    │...\n 203│    let mut decoder = RestDecoder::new();\n*204│    loop {\n 205│        match brain.read_event()? {\n 206│            BrokerEvent::NetStreamData {\n 207│                stream_id: sid,\n## seedmap.rs\n 125│    let stop = Arc::new(AtomicBool::new(false));\n*126│    loop {\n 127│        let conn = listener.accept()?;\n 128│        if stop.load(Ordering::SeqCst) {\n 129│            // (A) REQ-HAZARD-DAEMON-STOP-BARRIER: release the throwaway wake-conn AND\n    │...\n 159│    recv_hello(&mut conn, Role::Brain)?;\n*160│    loop {\n 161│        let env = match read_frame(&mut conn) {\n 162│            Ok(e) => e,\n 163│            Err(_) => return Ok(()), // peer hung up — connection done\n    │...\n 282│    });\n*283│    match rx.recv_timeout(bound) {\n 284│        Ok(res) => res,\n 285│        Err(_) => Err(io::Error::new(\n 286│            io::ErrorKind::TimedOut,\n    │...\n 296│/// already-dying name wedged the whole convoy). Every dial on the stop path must be\n*297│/// bounded, not just the barrier ping. Same worker-thread + recv_timeout shape as\n 298│/// [`ping_bounded`]: a busy / pinned name degrades to a DISTINCT [`io::ErrorKind::TimedOut`],\n 299│/// a gone name still returns its real NotFound / connection-refused. Scope: the stop\n 300│/// path only — put / take / is_running stay unbounded pending REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED.\n    │...\n 310│    });\n*311│    match rx.recv_timeout(bound) {\n 312│        Ok(res) => res,\n 313│        Err(_) => Err(io::Error::new(\n 314│            io::ErrorKind::TimedOut,\n    │...\n 363│    let deadline = std::time::Instant::now() + STOP_BARRIER_TIMEOUT;\n*364│    loop {\n 365│        match ping_bounded(name, PING_BARRIER_BOUND) {\n 366│            Err(e) if e.kind() != io::ErrorKind::TimedOut => return Ok(()),\n 367│            _ => {} // PONG (still serving) or TimedOut (still tearing down) — keep waiting\n    │...\n 374│        }\n*375│        std::thread::sleep(STOP_BARRIER_POLL);\n 376│    }\n 377│}\n 378│\n    │...\n 451│            }\n*452│            std::thread::sleep(Duration::from_millis(5));\n 453│        }\n 454│        panic!(\"seed daemon did not come up\");\n 455│    }\n    │...\n 481│                }\n*482│                std::thread::sleep(Duration::from_millis(5));\n 483│            }\n 484│            panic!(\"seed daemon still answering after stop\");\n 485│        });\n## serveprobe.rs\n  75│    let mut decoder = ServeProbeDecoder::new();\n *76│    loop {\n  77│        match brain.read_event()? {\n  78│            BrokerEvent::NetStreamData {\n  79│                stream_id: sid,\n    │...\n 122│    let mut decoder = ServeProbeDecoder::new();\n*123│    loop {\n 124│        match brain.read_event()? {\n 125│            BrokerEvent::NetStreamData {\n 126│                stream_id: sid,\n## shellchan.rs\n 211│        // Drain interleaved output until this row's exactly-once ack.\n*212│        loop {\n 213│            match brain.read_event().map_err(|e| format!(\"read: {e}\"))? {\n 214│                BrokerEvent::Applied { op_id, .. } if op_id == row_id as u64 => break,\n 215│                BrokerEvent::Exit { .. } => return Err(\"shell binary exited mid-delivery\".into()),\n\n## pump/\n### health.rs\n 79│    pub fn load_from(path: &Path) -> PumpHealth {\n*80│        match std::fs::read_to_string(path) {\n 81│            Ok(s) => serde_json::from_str(&s).unwrap_or_default(),\n 82│            Err(_) => PumpHealth::default(),\n 83│        }\n### mod.rs\n  593│\n *594│    while !stop.load(Ordering::Relaxed) {\n  595│        let now = Instant::now();\n  596│        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\n  597│        // while the loop is ALIVE — including idle ticks — never per work round,\n     │...\n  611│        if !any_due {\n *612│            thread::sleep(TICK);\n  613│            continue;\n  614│        }\n  615│\n     │...\n  704│        let round_deadline = round_start + PEER_REPLY_READ_BUDGET + ROUND_DRAIN_MARGIN;\n *705│        loop {\n  706│            let read_deadline = if pending.is_empty() {\n  707│                Instant::now() // no outstanding submits — sweep only what is queued\n  708│            } else {\n     │...\n 1136│        }\n*1137│        thread::sleep(Duration::from_millis(10));\n 1138│    }\n 1139│    Err(last)\n 1140│}\n     │...\n 1205│    let mut backoff = base;\n*1206│    while !stop.load(Ordering::Relaxed) {\n 1207│        let started = Instant::now();\n 1208│        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut body));\n 1209│        if stop.load(Ordering::Relaxed) {\n     │...\n 1233│        while slept < backoff && !stop.load(Ordering::Relaxed) {\n*1234│            thread::sleep(TICK.min(backoff - slept));\n 1235│            slept += TICK;\n 1236│        }\n 1237│    }\n     │...\n 1809│        write_heartbeat(&path);\n*1810│        let first: u64 = std::fs::read_to_string(&path)\n 1811│            .expect(\"heartbeat written\")\n 1812│            .trim()\n 1813│            .parse()\n     │...\n 1815│        assert!(first > 0, \"a real wall-clock stamp\");\n*1816│        thread::sleep(Duration::from_millis(15));\n 1817│        write_heartbeat(&path);\n*1818│        let second: u64 = std::fs::read_to_string(&path)\n 1819│            .unwrap()\n 1820│            .trim()\n 1821│            .parse()"},"isError":false,"timestamp":1784271486374}}
{"type":"message","id":"c4a0f889","parentId":"c38bf416","timestamp":"2026-07-17T06:58:06.382Z","message":{"role":"toolResult","toolCallId":"call_OqWlCtSTggNGrTEFG7K4lsje|fc_038f28d3e93bbfd4016a59d27e4610819684ddf9f5d02fe7d9","toolName":"grep","content":[{"type":"text","text":" 4|//! In the no-daemon interim model the long-lived `spt api listen` process hosted\n*5|//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo\n 6|//! fires, graceful signoff) directly in the agent's session. Consolidation\n 7|//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the\n 8|//! brain runs it as scheduled in-process loops, with a durable, configurable\n...\n 11|//! a re-host, not a rewrite: the heavy lifting stays in the already-pure\n*12|//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],\n 13|//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is\n 14|//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 — the\n 15|//! resident `spawn_psyche` seam retired); the ordering invariants\n*16|//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live\n 17|//! where they always have.\n 18|// [impl->REQ-DAEMON-1]\n 19|\n...\n 26|use spt_live::{\n*27|    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n*28|    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n 29|};\n 30|use spt_runtime::{Manifest, ManifestRuntime};\n 31|use spt_store::epoch::EpochSource;\n...\n 41|\n*42|/// Bound for a bounded history/echo invocation in the host (carried from M2b).\n 43|const LIVE_BOUNDED_TIMEOUT: Duration = Duration::from_secs(30);\n 44|\n 45|/// The per-event psyche turn's timeout FLOOR (F-030 design §7 Q1): a pulse period\n...\n 54|/// W2/W4 reseed/custody path (the non-empty `{psyche_context_file}` on a first/reseed\n*55|/// turn), NOT every fire. An event-driven fire (W2) passes the real event/commune\n 56|/// payload here.\n 57|const PSYCHE_PULSE_STANDING_PROMPT: &[u8] = b\"<pulse/>\\n\";\n 58|\n...\n 161|/// context-file), LOCAL to the turn. `base` is `base_keys(parent_sid)` — its\n*162|/// `{session_id}` is the PARENT's (echo/history/signoff keep that fill); here we\n 163|/// OVERRIDE `{session_id}` to the psyche's OWN sid and add the parent's sid under its\n 164|/// OWN explicit key `{parent_session_id}` (never aliased). `context_file` is the PATH\n 165|/// to the core-written mind file (F-030 W4, REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): the\n...\n 262|\n*263|/// Loud-ONCE-per-endpoint diagnostic when the ECHO fire skips because a RELATIVE\n*264|/// `commune_dir` has no endpoint cwd to resolve against (REQ-ECHO-DROP-DIR-RESOLVE):\n 265|/// the skip is NAMED, never a raw relative write into whatever cwd the daemon\n 266|/// happens to hold (System32 under WMI = os error 5; a writable daemon cwd =\n*267|/// communes silently lost to a wrong dir — the latent-worse variant).\n*268|// [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n*269|fn warn_echo_no_cwd_once(id: &str, raw: &Path) {\n 270|    warn_once(\n*271|        format!(\"echo:{id}\"),\n 272|        format!(\n*273|            \"ECHO_COMMUNE_SKIP:{id}: relative commune dir {raw:?} has no endpoint cwd to \\\n*274|             resolve against — echo commune skipped until bind records a cwd\"\n 275|        ),\n 276|    );\n 277|}\n 278|\n*279|/// Quiet-ONCE-per-endpoint note when commune-sync skips because the manifest\n*280|/// declares no `[session.echo_commune]` role (REQ-PSYCHE-ROLE-OPTIONAL-SKIP):\n 281|/// the role is an OPTIONAL template per the published contract — its absence is\n 282|/// a configuration fact, never a turn failure, never a strike.\n 283|// [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n*284|fn note_echo_role_absent_once(id: &str) {\n 285|    warn_once(\n*286|        format!(\"echo-role:{id}\"),\n 287|        format!(\n*288|            \"ECHO_ROLE_ABSENT:{id}: manifest declares no [session.echo_commune] role — \\\n*289|             commune-sync skipped (the role is optional)\"\n 290|        ),\n 291|    );\n 292|}\n 293|\n*294|/// One pulse tick's observed effects: any drops ingested + whether an echo fired.\n 295|#[derive(Debug, Clone, Default)]\n 296|pub struct TickReport {\n 297|    pub ingested: Vec<Ingested>,\n*298|    pub echo_fired: bool,\n*299|    /// F-030 W1: the per-event psyche-turn outcome when the echo-gate fired this\n*300|    /// tick. `None` ⇒ no fire this tick; `Some(Ok(()))` ⇒ the turn AND its commune-\n 301|    /// sync side-effect both succeeded; `Some(Err(reason))` ⇒ the turn OR the\n*302|    /// commune-sync failed (a strike — [`run_pulse_loop`] feeds it the failure budget).\n 303|    pub turn_outcome: Option<Result<(), String>>,\n 304|}\n 305|\n...\n 333|    fn runtime_snapshot(&self) -> ManifestRuntime {\n*334|        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n 335|    }\n 336|\n 337|    /// A snapshot **clone** of the manifest (guard dropped on return, as above).\n 338|    fn manifest_snapshot(&self) -> Manifest {\n*339|        self.manifest.read().unwrap_or_else(|p| p.into_inner()).clone()\n 340|    }\n 341|\n 342|    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n 343|    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n*344|    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n 345|    /// brain thread, PTY, broker session, and output log are untouched; only these\n 346|    /// two cells' contents change. (W3c)\n 347|    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n...\n 378|    id: String,\n*379|    /// The adapter-declared commune/signoff drop dirs — the RAW manifest values,\n 380|    /// possibly RELATIVE (e.g. claude-spt's `.claude`, documented \"resolved\n 381|    /// per-endpoint against its cwd\"). Resolution to an absolute path + the\n 382|    /// project_id derivation happen AT INGEST TIME against the endpoint's fresh\n...\n 386|    /// drops into, so the two-tier store never fills.\n*387|    commune_dir: Option<PathBuf>,\n 388|    signoff_dir: Option<PathBuf>,\n 389|    cfg: DaemonConfig,\n 390|    /// This node's advertised label for the `{node}` fill (REQ-MANIFEST-NODE-KEY),\n...\n 431|        // against the endpoint's fresh cwd, NOT frozen here (REQ-STORE-CONTEXT-BRANCH-FILL).\n*432|        // The old code froze project_id = project_id_for_dir(commune_dir.parent()); with a\n 433|        // relative `.claude` that parent is \"\" → a garbage project_id, and drop_dirs read\n 434|        // relative to the daemon's cwd → the store never filled. Both moved to pulse_tick.\n*435|        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);\n 436|        // [impl->REQ-INSTALL-11] the cell pins the adapter install dir so the\n 437|        // psyche role's bare program token resolves there before PATH (a\n 438|        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n...\n 442|            id: id.to_string(),\n*443|            commune_dir,\n 444|            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),\n 445|            cfg,\n 446|            // Resolve the advertised label once, here (brain startup), not per fill.\n...\n 465|        let dir = self.install_dir.as_ref()?;\n*466|        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n 467|        Some(crate::release::sha256_hex(&bytes))\n 468|    }\n 469|\n...\n 486|        };\n*487|        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n 488|            return;\n 489|        };\n 490|        let hash = crate::release::sha256_hex(&bytes);\n...\n 539|\n*540|    /// The unique watched drop dirs (commune + signoff; usually the same dir).\n 541|    fn drop_dirs(&self) -> Vec<PathBuf> {\n 542|        let mut dirs: Vec<PathBuf> = Vec::new();\n*543|        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\n 544|            .into_iter()\n 545|            .flatten()\n 546|        {\n...\n 582|    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n*583|    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n*584|    /// echo-commune (T3).\n*585|    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n 586|        let now = now_ms();\n 587|        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n 588|        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n...\n 635|        }\n*636|        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\n*637|        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\n*638|        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\n*639|        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\n*640|        // below can never void an already-relayed turn. The commune-sync is NON-\n 641|        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\n 642|        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\n 643|        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\n...\n 645|        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n*646|        let (echo_fired, turn_outcome) =\n*647|            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\n 648|                let turn_res =\n 649|                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\n*650|                let commune_res = self.fire_echo(session_id);\n*651|                if let Err(e) = &commune_res {\n*652|                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\n 653|                }\n 654|                // A strike if EITHER leg failed; the reason names the failing leg.\n*655|                // The turn's relay already happened above regardless of the commune.\n*656|                let outcome = match (turn_res, commune_res) {\n 657|                    (Ok(()), Ok(())) => Ok(()),\n 658|                    (Err(e), _) => Err(format!(\"turn: {e}\")),\n*659|                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\n 660|                };\n 661|                (true, Some(outcome))\n 662|            } else {\n...\n 666|            ingested,\n*667|            echo_fired,\n 668|            turn_outcome,\n 669|        })\n 670|    }\n...\n 691|    // [impl->REQ-DAEMON-1]\n*692|    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n 693|        &self,\n 694|        session_id: Option<&str>,\n 695|        stop: &AtomicBool,\n...\n 731|            // Pull any live-update manifest swap from disk before the tick so the\n*732|            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\n 733|            self.reload_manifest_if_changed(&mut manifest_hash);\n 734|            match self.pulse_tick(session_id) {\n 735|                Ok(report) => {\n...\n 752|\n*753|    /// Fetch history per the manifest `[history]` strategy (empty if none).\n*754|    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n 755|        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n*756|        // history fetch below), so a live-update refresh never waits on it.\n 757|        let manifest = self.cell.manifest_snapshot();\n*758|        let Some(h) = &manifest.history else {\n 759|            return Vec::new();\n 760|        };\n*761|        fetch_history(\n 762|            h,\n 763|            &self.base_keys(session_id),\n 764|            &self.id,\n...\n 774|    ///\n*775|    /// - **active → (dormant|suspended)**: the transition echo commune\n*776|    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n 777|    ///   outgoing instance's final context delta, BEFORE the flip persists\n 778|    ///   (KH 3.3) — it syncs to whichever instance activates next.\n 779|    /// - **wake (→ active)**: re-activation in place catches up immediately —\n...\n 799|            now_ms(),\n*800|            || self.fire_echo(session_id),\n 801|            || self.fire_wake_effects(),\n 802|        )?;\n 803|        // The shell leg of the edge (M5-D4a): suspend closes online shells,\n...\n 825|\n*826|    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.\n 827|    ///\n 828|    /// LIFECYCLE-TRUTH W1 skips (both Ok — a skip is a named configuration fact,\n 829|    /// never a strike):\n*830|    /// - **no `[session.echo_commune]` role** — the role is OPTIONAL per the\n 831|    ///   published contract; skip with a once-per-endpoint note\n 832|    ///   (REQ-PSYCHE-ROLE-OPTIONAL-SKIP — pre-fix this hard-failed every fire and\n 833|    ///   3-strike-stamped a healthy host).\n*834|    /// - **relative `commune_dir` with no recorded endpoint cwd** — skip LOUD, never\n*835|    ///   a raw relative write (REQ-ECHO-DROP-DIR-RESOLVE — pre-fix the raw manifest\n*836|    ///   dir went straight to `run_echo_commune`, which under the WMI-launched\n 837|    ///   daemon's System32 cwd = deterministic os error 5).\n 838|    ///\n 839|    /// The dir routes through [`resolve_endpoint_drop_dir`] — the SAME resolver the\n...\n 841|    /// the endpoint's cwd as its default working dir (a role-declared cwd wins).\n*842|    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n*843|    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n*844|        let Some(raw) = &self.commune_dir else {\n 845|            return Ok(());\n 846|        };\n 847|        // [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n*848|        if self.cell.manifest_snapshot().session.echo_commune.is_none() {\n*849|            note_echo_role_absent_once(&self.id);\n 850|            return Ok(());\n 851|        }\n 852|        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n...\n 855|        else {\n*856|            warn_echo_no_cwd_once(&self.id, raw);\n 857|            return Ok(());\n 858|        };\n*859|        let history = self.history(session_id);\n 860|        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n*861|        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n 862|        // dir so the summarizer child runs where the endpoint lives, never in the\n*863|        // daemon's own process cwd (REQ-ECHO-DROP-DIR-RESOLVE rider).\n 864|        let runtime = self\n 865|            .cell\n 866|            .runtime_snapshot()\n 867|            .with_spawn_cwd(endpoint_cwd.map(PathBuf::from));\n*868|        run_echo_commune(\n 869|            &runtime,\n 870|            &self.id,\n 871|            &self.base_keys(session_id),\n*872|            &history,\n 873|            &dir,\n 874|            LIVE_BOUNDED_TIMEOUT,\n 875|        )\n...\n 877|        // Every successful psyche operation clears a stale fault stamp — this\n*878|        // covers the checkpoint/rest-edge transition echo, the signoff echo, and\n*879|        // the pulse commune-sync (the event-turn leg clears via\n 880|        // `note_turn_outcome`'s Ok arm). A SKIP above deliberately does NOT clear:\n 881|        // nothing ran, so nothing was proven healthy.\n 882|        // [impl->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS]\n...\n 887|    /// Clear the perch's `psyche_host_error` fault stamp — called on EVERY\n*888|    /// successful psyche operation (event turn, transition/checkpoint echo,\n*889|    /// signoff echo), so a stale FAILED stamp can never sit over a healthy psyche\n 890|    /// (REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS — pre-fix only the pulse-loop\n 891|    /// `note_turn_outcome` Ok leg cleared it). Best-effort store write.\n 892|    // [impl->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS]\n...\n 949|    ///   composed mind `download_psyche_context`: role/live/project tiers + any pending\n*950|    ///   commune/signoff drop, or the `<fresh-psyche/>` marker when the mind composes\n 951|    ///   empty); a LATER (ContinueExisting) turn writes it EXACTLY 0 bytes (same\n 952|    ///   `{session_id}`, no re-preload — perri PIN 1: NO sentinel EVER). The shim\n 953|    ///   reads-file-then-trims and discriminates fresh-vs-continue on the FILE CONTENT\n...\n 974|    ///\n*975|    /// stdin = the EVENT/COMMUNE payload for THIS turn only. A bare periodic pulse (no\n 976|    /// pending event) feeds the minimal `<pulse/>` standing prompt. The composed mind\n 977|    /// does NOT ride stdin — it rides the `{psyche_context_file}` FILE, whose path is\n 978|    /// ALWAYS filled: the shim reads-then-trims it every turn and the fresh/continue\n...\n 1002|        // needs no `.live-bin` own-copy (unlike the retired resident spawn) — the\n*1003|        // install-dir/PATH snapshot is correct here (same class as translate/digest).\n 1004|        //\n 1005|        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n 1006|        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n...\n 1024|        // F-030 W2 — psyche-own sid custody. `base_keys`' `{session_id}` is the PARENT's\n*1025|        // (echo/history/signoff keep that fill); this turn OVERRIDES it to the psyche's\n 1026|        // OWN sid, LOCAL to the turn. Some(custody) → a LATER (ContinueExisting) turn:\n 1027|        // `{session_id}` = the stored sid; the `{psyche_context_file}` is written EXACTLY\n 1028|        // 0 bytes (continue). None → a FIRST (FreshWithPreload) turn: mint a UUID, persist\n...\n 1118|    /// (FreshWithPreload) psyche turn (F-030 W2/W4): the durable role/live/project tiers\n*1119|    /// plus any not-yet-synthesized commune/signoff drop, resolved against the parent\n 1120|    /// endpoint's bound cwd — the SAME composition `spt api psyche-download` emits.\n 1121|    /// `None` when nothing is stored yet (a role-less/context-less fresh endpoint —\n 1122|    /// the first turn then simply seeds from the minted `{session_id}` alone, the file\n...\n 1137|        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\n*1138|        let commune_file = self.psyche_drop_file(self.commune_dir.as_deref(), cwd_path, \"commune\");\n 1139|        let signoff_file = self.psyche_drop_file(self.signoff_dir.as_deref(), cwd_path, \"signoff\");\n 1140|        download_psyche_context(\n 1141|            &self.id,\n 1142|            &project_id,\n*1143|            commune_file.as_deref(),\n 1144|            signoff_file.as_deref(),\n 1145|        )\n 1146|    }\n...\n 1209|\n*1210|    /// Execute the graceful signoff (T6): echo-commune BEFORE teardown (3.3),\n 1211|    /// grace recheck before any signoff write (1.1). The graceful path is an\n 1212|    /// **intentional** shutdown, so it uses zero grace + an always-gone recheck —\n 1213|    /// the Self is leaving on purpose, there is nothing to recover. The\n...\n 1222|    /// *unexpected* harness death. It runs the identical load-bearing ordering —\n*1223|    /// echo-commune (3.3) → grace wait (1.1) → liveness recheck → teardown — but\n 1224|    /// with the **real** grace window from config and the **real** recheck\n 1225|    /// `still_gone`. A Self that recovered across the grace window (a binary\n 1226|    /// handoff relaunch, a brief stale poll) makes `still_gone` false and is\n...\n 1234|    // [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]\n*1235|    // [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]\n 1236|    pub fn supervised_crash_signoff(\n 1237|        &self,\n 1238|        session_id: Option<&str>,\n...\n 1246|    /// the 1.1 hazard turns on — the `grace` window and the `recheck` verdict — so\n*1247|    /// they are the only parameters; the echo (3.3) and teardown effects are\n 1248|    /// identical. Keeping one body guarantees the crash path can never drift out\n*1249|    /// of the echo-before-teardown ordering the graceful path proves.\n 1250|    fn signoff_with(\n 1251|        &self,\n 1252|        session_id: Option<&str>,\n...\n 1255|    ) -> Result<SignoffOutcome, String> {\n*1256|        let mut echo_err: Option<String> = None;\n 1257|        let mut teardown_err: Option<String> = None;\n 1258|        let outcome = graceful_signoff::<std::convert::Infallible>(\n 1259|            session_id,\n...\n 1261|            || {\n*1262|                if let Err(e) = self.fire_echo(session_id) {\n*1263|                    echo_err = Some(e);\n 1264|                }\n 1265|                Ok(())\n 1266|            },\n...\n 1278|        .unwrap_or(SignoffOutcome {\n*1279|            echo_ran: false,\n 1280|            signed_off: false,\n 1281|        });\n*1282|        if let Some(e) = echo_err {\n*1283|            return Err(format!(\"echo failed during signoff: {e}\"));\n 1284|        }\n 1285|        if let Some(e) = teardown_err {\n 1286|            return Err(format!(\"teardown failed during signoff: {e}\"));\n...\n 1367|\n*1368|    // A live-capable manifest: a Psyche init template + a commune dir to watch.\n 1369|    // The Psyche command is a trivial no-op (we never spawn it in these tests;\n 1370|    // we exercise the pulse/ingest loop, which does not launch the Psyche).\n 1371|    #[cfg(windows)]\n...\n 1375|\n*1376|    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {\n*1377|        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n 1378|        let toml = format!(\n 1379|            \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*1380|             [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 1381|             [session.psyche_init]\\ncommand='{NOOP_CMD}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\n 1382|        );\n 1383|        Manifest::from_toml_str(&toml).unwrap()\n...\n 1385|\n*1386|    // A stdin→stdout passthrough so the echo-commune summarizer succeeds and\n*1387|    // writes a real drop file — lets the signoff tests observe that the echo\n 1388|    // (3.3) actually ran before any teardown.\n 1389|    #[cfg(windows)]\n 1390|    const PASSTHROUGH: &str = \"sort\";\n...\n 1393|\n*1394|    // A live manifest that ALSO declares an echo-commune summarizer, so\n*1395|    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.\n*1396|    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {\n*1397|        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n 1398|        let toml = format!(\n 1399|            \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*1400|             [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 1401|             [session.psyche_init]\\ncommand='{NOOP_CMD}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\\n\\\n*1402|             [session.echo_commune]\\ncommand='{PASSTHROUGH}'\\n\"\n 1403|        );\n 1404|        Manifest::from_toml_str(&toml).unwrap()\n 1405|    }\n...\n 1438|\n*1439|    /// Write a per-platform script that echoes `$CLAUDE_TEST_ROOT` to `marker`, and\n 1440|    /// return the `psyche_resume` command string that runs it. Shell logic lives in a\n 1441|    /// FILE, never an inline `sh -c '...'` — the tokenize single-quote-literal contract\n 1442|    /// (REQ-HAZARD-TEMPLATE-ARGV-FILL) forbids the inline form (the F-030 lesson).\n*1443|    fn write_env_echo_fixture(home: &std::path::Path, marker: &std::path::Path) -> String {\n 1444|        #[cfg(windows)]\n 1445|        {\n*1446|            let script = home.join(\"echo_env.bat\");\n 1447|            std::fs::write(\n 1448|                &script,\n*1449|                format!(\"@echo %CLAUDE_TEST_ROOT%>\\\"{}\\\"\\r\\n\", marker.display()),\n 1450|            )\n 1451|            .unwrap();\n 1452|            format!(\"cmd /C \\\"{}\\\"\", script.display())\n...\n 1455|        {\n*1456|            let script = home.join(\"echo_env.sh\");\n 1457|            std::fs::write(\n 1458|                &script,\n*1459|                format!(\"echo \\\"$CLAUDE_TEST_ROOT\\\" > \\\"{}\\\"\\n\", marker.display()),\n 1460|            )\n 1461|            .unwrap();\n 1462|            format!(\"sh {}\", script.display())\n...\n 1482|            let stamp = \"sentinel-acct-root-9f3a\";\n*1483|            let command = write_env_echo_fixture(home, &marker);\n*1484|            let commune = home.join(\"commune\");\n*1485|            std::fs::create_dir_all(&commune).unwrap();\n*1486|            let dir = commune.to_string_lossy().replace('\\\\', \"/\");\n 1487|            let toml = format!(\n 1488|                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*1489|                 [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 1490|                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n 1491|                 [session.psyche_resume]\\ncommand='{command}'\\nkeys=[]\\n\"\n 1492|            );\n...\n 1503|\n*1504|            let got = std::fs::read_to_string(&marker)\n 1505|                .expect(\"the psyche child must have spawned + written the marker\");\n 1506|            assert_eq!(\n 1507|                got.trim(),\n...\n 1539|\n*1540|    #[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.\n*1541|    fn pulse_tick_ingests_drop() {\n 1542|        with_home(|_| {\n 1543|            let drops = tempfile::tempdir().unwrap();\n 1544|            let manifest = live_manifest(drops.path());\n...\n 1547|\n*1548|            std::fs::write(drops.path().join(\"doyle-commune.md\"), \"brief\").unwrap();\n 1549|            let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 1550|            assert_eq!(report.ingested.len(), 1, \"the drop was folded in\");\n*1551|            assert!(!report.echo_fired, \"no echo gate armed\");\n 1552|            assert!(\n*1553|                !drops.path().join(\"doyle-commune.md\").exists(),\n 1554|                \"drop consumed\"\n 1555|            );\n 1556|        });\n...\n 1823|        let fresh = write_psyche_context_file(&perch, first_turn_psyche_context(Some(\"MIND\"))).unwrap();\n*1824|        assert_eq!(std::fs::read_to_string(&fresh).unwrap(), \"MIND\");\n 1825|        assert!(std::fs::metadata(&fresh).unwrap().len() > 0);\n 1826|\n 1827|        // FRESH with a zero-context agent: the NON-EMPTY <fresh-psyche/> marker (never\n...\n 1830|        assert_eq!(marker, cont, \"overwrite in place — same nested-perch path each turn\");\n*1831|        assert_eq!(std::fs::read_to_string(&marker).unwrap(), PSYCHE_FRESH_MARKER);\n 1832|        assert!(std::fs::metadata(&marker).unwrap().len() > 0);\n 1833|    }\n 1834|\n...\n 1981|    // [int->REQ-STORE-CONTEXT-BRANCH-FILL] use-it-like-a-human: a REAL pulse tick with\n*1982|    // the PRODUCTION-shape RELATIVE \".claude\" commune_dir + an endpoint whose info.cwd\n 1983|    // is a real project dir. The drop lands in <cwd>/.claude (where the agent drops it),\n 1984|    // and the fix resolves the relative dir against the endpoint cwd — so ingest FINDS\n 1985|    // it and the two-tier store FILLS: a-<id> + p-<project_id_for_dir(cwd)>. RED pre-fix\n...\n 1987|    #[test]\n*1988|    fn relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch() {\n 1989|        with_home(|_| {\n 1990|            // A real project cwd, NOT under the owlery — the endpoint's info.cwd.\n 1991|            let project = tempfile::tempdir().unwrap();\n...\n 2007|\n*2008|            // The agent drops a TWO-slice commune into <cwd>/.claude — the RELATIVE dir.\n 2009|            let drop_dir = project.path().join(\".claude\");\n 2010|            std::fs::create_dir_all(&drop_dir).unwrap();\n 2011|            std::fs::write(\n*2012|                drop_dir.join(\"doyle-commune.md\"),\n 2013|                \"<live-context>\\nwho I am\\n</live-context>\\n\\\n 2014|                 <project-context>\\nwhat I do here\\n</project-context>\",\n 2015|            )\n...\n 2017|\n*2018|            // The PRODUCTION claude-spt value: a RELATIVE commune_dir.\n 2019|            let manifest = live_manifest(Path::new(\".claude\"));\n 2020|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n 2021|                .expect(\"live host\");\n...\n 2028|            );\n*2029|            assert!(!drop_dir.join(\"doyle-commune.md\").exists(), \"drop consumed\");\n 2030|\n 2031|            // The two-tier store FILLED — the whole point of the RCA.\n 2032|            let cs = spt_store::contextstore::ContextStore::open_or_init().unwrap();\n...\n 2045|    /// Write a per-platform script that prints the child's OWN working directory\n*2046|    /// to stdout, and return the `[session.echo_commune]` command string that runs\n 2047|    /// it — the spawn-cwd rider probe (file-based, never inline `sh -c '...'`, the\n 2048|    /// tokenize single-quote-literal contract).\n*2049|    fn write_cwd_echo_fixture(home: &std::path::Path) -> String {\n 2050|        #[cfg(windows)]\n 2051|        {\n*2052|            let script = home.join(\"echo_cwd.bat\");\n 2053|            std::fs::write(&script, \"@cd\\r\\n\").unwrap();\n 2054|            format!(\"cmd /C \\\"{}\\\"\", script.display())\n 2055|        }\n...\n 2057|        {\n*2058|            let script = home.join(\"echo_cwd.sh\");\n 2059|            std::fs::write(&script, \"pwd\\n\").unwrap();\n 2060|            format!(\"sh {}\", script.display())\n 2061|        }\n...\n 2063|\n*2064|    // [unit->REQ-ECHO-DROP-DIR-RESOLVE] the ECHO fire resolves a RELATIVE manifest\n*2065|    // `commune_dir` (the PRODUCTION claude-spt \".claude\") against the ENDPOINT's\n 2066|    // recorded cwd through the SAME resolver the ingest leg uses — the drop lands in\n 2067|    // <endpoint cwd>/.claude, and the summarizer child itself RUNS in the endpoint\n 2068|    // cwd (the spawn-cwd rider). RED pre-fix: the raw relative dir went straight to\n*2069|    // `run_echo_commune`, which create_dir_all'd it against the DAEMON's process cwd\n*2070|    // (System32 under WMI = os error 5; a writable daemon cwd = communes silently\n 2071|    // lost to the wrong dir).\n 2072|    #[test]\n*2073|    fn echo_fire_resolves_relative_commune_dir_against_endpoint_cwd() {\n 2074|        with_home(|home| {\n 2075|            // A real project dir — the endpoint's recorded info.cwd.\n 2076|            let project = tempfile::tempdir().unwrap();\n...\n 2089|\n*2090|            // Manifest: RELATIVE commune_dir + an echo role that prints its own cwd.\n*2091|            let command = write_cwd_echo_fixture(home);\n 2092|            let toml = format!(\n 2093|                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*2094|                 [session]\\ncommune_dir=\\\".claude\\\"\\n\\n\\\n 2095|                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n*2096|                 [session.echo_commune]\\ncommand='{command}'\\n\"\n 2097|            );\n 2098|            let manifest = Manifest::from_toml_str(&toml).expect(\"manifest parses\");\n 2099|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n...\n 2101|\n*2102|            host.fire_echo(None).expect(\"echo fires clean\");\n 2103|\n 2104|            // The drop landed under the ENDPOINT cwd, not the daemon's.\n*2105|            let drop_path = project.path().join(\".claude\").join(\"doyle-commune.md\");\n*2106|            let body = std::fs::read_to_string(&drop_path)\n*2107|                .expect(\"commune drop written under <endpoint cwd>/.claude\");\n 2108|            // The rider: the summarizer child RAN in the endpoint cwd (its printed\n 2109|            // cwd is the project dir). Normalize separators/case for the compare.\n 2110|            let norm = |s: &str| s.replace('\\\\', \"/\").to_lowercase();\n...\n 2112|                norm(&body).contains(&norm(&project.path().to_string_lossy())),\n*2113|                \"echo child must run in the endpoint cwd, printed: {body:?}\"\n 2114|            );\n 2115|        });\n 2116|    }\n 2117|\n*2118|    // [unit->REQ-ECHO-DROP-DIR-RESOLVE] a RELATIVE commune_dir with NO recorded\n*2119|    // endpoint cwd = LOUD SKIP: fire_echo returns Ok (no strike), the summarizer is\n 2120|    // NEVER spawned, and nothing is written into whatever cwd the daemon happens to\n*2121|    // hold. RED pre-fix: the echo ran and wrote (or os-error-5'd) against the\n 2122|    // daemon's own process cwd.\n 2123|    #[test]\n*2124|    fn echo_fire_skips_loud_on_relative_dir_without_cwd() {\n 2125|        with_home(|home| {\n 2126|            // Perch record WITHOUT a cwd.\n 2127|            let perch_path = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n...\n 2137|\n*2138|            // The echo role writes a marker file — if the skip happens BEFORE the\n 2139|            // spawn (as it must), the marker never appears.\n*2140|            let marker = home.join(\"echo_ran_marker.txt\");\n 2141|            #[cfg(windows)]\n 2142|            let command = {\n 2143|                let script = home.join(\"mark.bat\");\n*2144|                std::fs::write(&script, format!(\"@echo ran>\\\"{}\\\"\\r\\n\", marker.display()))\n 2145|                    .unwrap();\n 2146|                format!(\"cmd /C \\\"{}\\\"\", script.display())\n 2147|            };\n...\n 2150|                let script = home.join(\"mark.sh\");\n*2151|                std::fs::write(&script, format!(\"echo ran > \\\"{}\\\"\\n\", marker.display()))\n 2152|                    .unwrap();\n 2153|                format!(\"sh {}\", script.display())\n 2154|            };\n...\n 2156|                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*2157|                 [session]\\ncommune_dir=\\\".claude\\\"\\n\\n\\\n 2158|                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n*2159|                 [session.echo_commune]\\ncommand='{command}'\\n\"\n 2160|            );\n 2161|            let manifest = Manifest::from_toml_str(&toml).expect(\"manifest parses\");\n 2162|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n...\n 2164|\n*2165|            let out = host.fire_echo(None);\n 2166|            assert!(out.is_ok(), \"a no-cwd skip is Ok (no strike), got {out:?}\");\n 2167|            assert!(\n 2168|                !marker.exists(),\n...\n 2174|    // [unit->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS] the checkpoint/rest-edge path: a\n*2175|    // SUCCESSFUL transition echo (the suspend/checkpoint boundary commune) clears a\n 2176|    // pre-existing psyche_host_error stamp. RED pre-fix: only the pulse-loop\n 2177|    // `note_turn_outcome` Ok leg cleared, so a stale FAILED stamp sat over a healthy\n 2178|    // psyche across checkpoint/wake cycles (three field confirmations).\n 2179|    #[test]\n*2180|    fn rest_edge_echo_success_clears_stale_psyche_stamp() {\n 2181|        with_home(|_| {\n 2182|            let drops = tempfile::tempdir().unwrap();\n*2183|            let manifest = live_manifest_with_echo(drops.path());\n 2184|            let perch_path = seed_supervised_perch(\"doyle\", std::process::id());\n 2185|            // A stale fault stamp from a prior failing period.\n 2186|            spt_store::info::set_psyche_host_error(&perch_path, Some(\"stale failure\")).unwrap();\n...\n 2190|            // Active → Dormant: the checkpoint/suspend boundary — fires the\n*2191|            // transition echo (KH 3.3) through fire_echo.\n 2192|            let report = host\n 2193|                .rest_event(None, crate::resting::RestEvent::Detach)\n 2194|                .expect(\"rest edge applies\")\n 2195|                .expect(\"a real edge\");\n*2196|            assert!(report.echo_fired, \"the suspend edge fires the echo\");\n 2197|            assert!(\n*2198|                drops.path().join(\"doyle-commune.md\").exists(),\n*2199|                \"the transition echo wrote its commune\"\n 2200|            );\n 2201|            let info = spt_store::info::read_info(&perch_path).unwrap();\n 2202|            assert!(\n 2203|                info.psyche_host_error.is_none(),\n*2204|                \"a successful checkpoint-boundary echo clears the stale stamp, got {:?}\",\n 2205|                info.psyche_host_error\n 2206|            );\n 2207|        });\n...\n 2209|\n*2210|    // [unit->REQ-PSYCHE-ROLE-OPTIONAL-SKIP] a manifest with NO [session.echo_commune]\n*2211|    // role ticks STRIKE-FREE: commune-sync SKIPS (the role is optional per the\n 2212|    // published contract) and the fire's outcome is Ok, so repeated gate-armed pulses\n 2213|    // never stamp psyche_host_error. RED pre-fix: every fire hard-failed\n*2214|    // \"manifest declares no [session.echo_commune] role\" → 3 strikes → a FAILED stamp\n 2215|    // over a healthy host (perri's field filing).\n 2216|    #[test]\n 2217|    fn role_less_manifest_ticks_strike_free() {\n...\n 2220|            let dir = drops.path().to_string_lossy().replace('\\\\', \"/\");\n*2221|            // commune_dir + a WORKING psyche_resume, but NO echo_commune role.\n 2222|            let toml = format!(\n 2223|                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*2224|                 [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 2225|                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n 2226|                 [session.psyche_resume]\\ncommand='{PASSTHROUGH}'\\nkeys=[]\\n\"\n 2227|            );\n...\n 2234|            for _ in 0..3 {\n*2235|                // Arm the echo gate so the fire (turn + commune-sync) runs this tick.\n*2236|                let gate = perch::resolve_echo_gate_file(\"doyle\", ParentHint::Infer);\n 2237|                if let Some(p) = gate.parent() {\n 2238|                    std::fs::create_dir_all(p).unwrap();\n 2239|                }\n...\n 2312|            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n*2313|                let cur = std::fs::read(crate::deadline::anchor_path(\"pulse\"))\n 2314|                    .expect(\"anchor file exists after loop entry\");\n 2315|                let mut slot = first_bytes.lock().unwrap();\n 2316|                match &*slot {\n...\n 2355|            let anchor_of = || {\n*2356|                let raw = std::fs::read_to_string(crate::deadline::anchor_path(\"pulse\")).unwrap();\n 2357|                serde_json::from_str::<DeadlineAnchor>(&raw).unwrap().anchor_ms\n 2358|            };\n 2359|\n...\n 2426|    // [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition\n*2427|    // echo commune (run_echo_commune through the manifest summarizer) exactly\n 2428|    // once per active→resting edge — the final context delta lands in the\n*2429|    // commune drop dir, where it syncs to whichever instance activates next;\n*2430|    // the dormant→suspended edge fires no second echo.\n 2431|    // [unit->REQ-INST-3] and the wake edge re-activates in place: it\n 2432|    // resurfaces undismissed notifs at the Wake boundary and drops the\n 2433|    // freshness-pull marker the peer pump consumes.\n 2434|    #[test]\n*2435|    fn rest_event_fires_echo_once_and_wake_resurfaces() {\n 2436|        with_home(|home| {\n 2437|            let drops = tempfile::tempdir().unwrap();\n*2438|            let manifest = live_manifest_with_echo(drops.path());\n 2439|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n 2440|                .expect(\"live host\");\n 2441|            seed_supervised_perch(\"doyle\", std::process::id());\n...\n 2453|\n*2454|            // active → dormant: the echo commune wrote the final delta.\n*2455|            let commune = drops.path().join(\"doyle-commune.md\");\n 2456|            let report = host\n 2457|                .rest_event(Some(\"sid-1\"), crate::resting::RestEvent::Detach)\n 2458|                .expect(\"apply ok\")\n...\n 2460|            assert!(\n*2461|                report.echo_fired,\n*2462|                \"active→dormant fires the transition echo\"\n 2463|            );\n*2464|            assert!(commune.exists(), \"the echo wrote a commune before the flip\");\n*2465|            std::fs::remove_file(&commune).unwrap();\n 2466|\n*2467|            // dormant → suspended: already resting — no second echo.\n 2468|            let report = host\n 2469|                .rest_event(Some(\"sid-1\"), crate::resting::RestEvent::Suspend)\n 2470|                .expect(\"apply ok\")\n 2471|                .expect(\"edge\");\n*2472|            assert!(!report.echo_fired, \"resting→resting fires no echo\");"}],"details":{"scopePath":"crates/spt-daemon/src/lifecycle.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\lifecycle.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":200,"fileCount":1,"files":["crates/spt-daemon/src/lifecycle.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/lifecycle.rs","count":200}],"truncated":true,"perFileLimitReached":200,"displayContent":"    4│//! In the no-daemon interim model the long-lived `spt api listen` process hosted\n   *5│//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo\n    6│//! fires, graceful signoff) directly in the agent's session. Consolidation\n    7│//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the\n    8│//! brain runs it as scheduled in-process loops, with a durable, configurable\n     │...\n   11│//! a re-host, not a rewrite: the heavy lifting stays in the already-pure\n  *12│//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],\n   13│//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is\n   14│//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 — the\n   15│//! resident `spawn_psyche` seam retired); the ordering invariants\n  *16│//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live\n   17│//! where they always have.\n   18│// [impl->REQ-DAEMON-1]\n   19│\n     │...\n   26│use spt_live::{\n  *27│    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n  *28│    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n   29│};\n   30│use spt_runtime::{Manifest, ManifestRuntime};\n   31│use spt_store::epoch::EpochSource;\n     │...\n   41│\n  *42│/// Bound for a bounded history/echo invocation in the host (carried from M2b).\n   43│const LIVE_BOUNDED_TIMEOUT: Duration = Duration::from_secs(30);\n   44│\n   45│/// The per-event psyche turn's timeout FLOOR (F-030 design §7 Q1): a pulse period\n     │...\n   54│/// W2/W4 reseed/custody path (the non-empty `{psyche_context_file}` on a first/reseed\n  *55│/// turn), NOT every fire. An event-driven fire (W2) passes the real event/commune\n   56│/// payload here.\n   57│const PSYCHE_PULSE_STANDING_PROMPT: &[u8] = b\"<pulse/>\\n\";\n   58│\n     │...\n  161│/// context-file), LOCAL to the turn. `base` is `base_keys(parent_sid)` — its\n *162│/// `{session_id}` is the PARENT's (echo/history/signoff keep that fill); here we\n  163│/// OVERRIDE `{session_id}` to the psyche's OWN sid and add the parent's sid under its\n  164│/// OWN explicit key `{parent_session_id}` (never aliased). `context_file` is the PATH\n  165│/// to the core-written mind file (F-030 W4, REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): the\n     │...\n  262│\n *263│/// Loud-ONCE-per-endpoint diagnostic when the ECHO fire skips because a RELATIVE\n *264│/// `commune_dir` has no endpoint cwd to resolve against (REQ-ECHO-DROP-DIR-RESOLVE):\n  265│/// the skip is NAMED, never a raw relative write into whatever cwd the daemon\n  266│/// happens to hold (System32 under WMI = os error 5; a writable daemon cwd =\n *267│/// communes silently lost to a wrong dir — the latent-worse variant).\n *268│// [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n *269│fn warn_echo_no_cwd_once(id: &str, raw: &Path) {\n  270│    warn_once(\n *271│        format!(\"echo:{id}\"),\n  272│        format!(\n *273│            \"ECHO_COMMUNE_SKIP:{id}: relative commune dir {raw:?} has no endpoint cwd to \\\n *274│             resolve against — echo commune skipped until bind records a cwd\"\n  275│        ),\n  276│    );\n  277│}\n  278│\n *279│/// Quiet-ONCE-per-endpoint note when commune-sync skips because the manifest\n *280│/// declares no `[session.echo_commune]` role (REQ-PSYCHE-ROLE-OPTIONAL-SKIP):\n  281│/// the role is an OPTIONAL template per the published contract — its absence is\n  282│/// a configuration fact, never a turn failure, never a strike.\n  283│// [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n *284│fn note_echo_role_absent_once(id: &str) {\n  285│    warn_once(\n *286│        format!(\"echo-role:{id}\"),\n  287│        format!(\n *288│            \"ECHO_ROLE_ABSENT:{id}: manifest declares no [session.echo_commune] role — \\\n *289│             commune-sync skipped (the role is optional)\"\n  290│        ),\n  291│    );\n  292│}\n  293│\n *294│/// One pulse tick's observed effects: any drops ingested + whether an echo fired.\n  295│#[derive(Debug, Clone, Default)]\n  296│pub struct TickReport {\n  297│    pub ingested: Vec<Ingested>,\n *298│    pub echo_fired: bool,\n *299│    /// F-030 W1: the per-event psyche-turn outcome when the echo-gate fired this\n *300│    /// tick. `None` ⇒ no fire this tick; `Some(Ok(()))` ⇒ the turn AND its commune-\n  301│    /// sync side-effect both succeeded; `Some(Err(reason))` ⇒ the turn OR the\n *302│    /// commune-sync failed (a strike — [`run_pulse_loop`] feeds it the failure budget).\n  303│    pub turn_outcome: Option<Result<(), String>>,\n  304│}\n  305│\n     │...\n  333│    fn runtime_snapshot(&self) -> ManifestRuntime {\n *334│        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()\n  335│    }\n  336│\n  337│    /// A snapshot **clone** of the manifest (guard dropped on return, as above).\n  338│    fn manifest_snapshot(&self) -> Manifest {\n *339│        self.manifest.read().unwrap_or_else(|p| p.into_inner()).clone()\n  340│    }\n  341│\n  342│    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk\n  343│    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The\n *344│    /// next psyche spawn / echo commune reads the new content. Brain-parity: the\n  345│    /// brain thread, PTY, broker session, and output log are untouched; only these\n  346│    /// two cells' contents change. (W3c)\n  347│    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n     │...\n  378│    id: String,\n *379│    /// The adapter-declared commune/signoff drop dirs — the RAW manifest values,\n  380│    /// possibly RELATIVE (e.g. claude-spt's `.claude`, documented \"resolved\n  381│    /// per-endpoint against its cwd\"). Resolution to an absolute path + the\n  382│    /// project_id derivation happen AT INGEST TIME against the endpoint's fresh\n     │...\n  386│    /// drops into, so the two-tier store never fills.\n *387│    commune_dir: Option<PathBuf>,\n  388│    signoff_dir: Option<PathBuf>,\n  389│    cfg: DaemonConfig,\n  390│    /// This node's advertised label for the `{node}` fill (REQ-MANIFEST-NODE-KEY),\n     │...\n  431│        // against the endpoint's fresh cwd, NOT frozen here (REQ-STORE-CONTEXT-BRANCH-FILL).\n *432│        // The old code froze project_id = project_id_for_dir(commune_dir.parent()); with a\n  433│        // relative `.claude` that parent is \"\" → a garbage project_id, and drop_dirs read\n  434│        // relative to the daemon's cwd → the store never filled. Both moved to pulse_tick.\n *435│        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);\n  436│        // [impl->REQ-INSTALL-11] the cell pins the adapter install dir so the\n  437│        // psyche role's bare program token resolves there before PATH (a\n  438│        // self-contained `.spt`); a live update's refresh re-pins it (W3c).\n     │...\n  442│            id: id.to_string(),\n *443│            commune_dir,\n  444│            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),\n  445│            cfg,\n  446│            // Resolve the advertised label once, here (brain startup), not per fill.\n     │...\n  465│        let dir = self.install_dir.as_ref()?;\n *466│        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n  467│        Some(crate::release::sha256_hex(&bytes))\n  468│    }\n  469│\n     │...\n  486│        };\n *487│        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n  488│            return;\n  489│        };\n  490│        let hash = crate::release::sha256_hex(&bytes);\n     │...\n  539│\n *540│    /// The unique watched drop dirs (commune + signoff; usually the same dir).\n  541│    fn drop_dirs(&self) -> Vec<PathBuf> {\n  542│        let mut dirs: Vec<PathBuf> = Vec::new();\n *543│        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\n  544│            .into_iter()\n  545│            .flatten()\n  546│        {\n     │...\n  582│    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n *583│    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n *584│    /// echo-commune (T3).\n *585│    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n  586│        let now = now_ms();\n  587│        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n  588│        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n     │...\n  635│        }\n *636│        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\n *637│        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\n *638│        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\n *639│        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\n *640│        // below can never void an already-relayed turn. The commune-sync is NON-\n  641│        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\n  642│        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\n  643│        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\n     │...\n  645│        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n *646│        let (echo_fired, turn_outcome) =\n *647│            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\n  648│                let turn_res =\n  649│                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\n *650│                let commune_res = self.fire_echo(session_id);\n *651│                if let Err(e) = &commune_res {\n *652│                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\n  653│                }\n  654│                // A strike if EITHER leg failed; the reason names the failing leg.\n *655│                // The turn's relay already happened above regardless of the commune.\n *656│                let outcome = match (turn_res, commune_res) {\n  657│                    (Ok(()), Ok(())) => Ok(()),\n  658│                    (Err(e), _) => Err(format!(\"turn: {e}\")),\n *659│                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\n  660│                };\n  661│                (true, Some(outcome))\n  662│            } else {\n     │...\n  666│            ingested,\n *667│            echo_fired,\n  668│            turn_outcome,\n  669│        })\n  670│    }\n     │...\n  691│    // [impl->REQ-DAEMON-1]\n *692│    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n  693│        &self,\n  694│        session_id: Option<&str>,\n  695│        stop: &AtomicBool,\n     │...\n  731│            // Pull any live-update manifest swap from disk before the tick so the\n *732│            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\n  733│            self.reload_manifest_if_changed(&mut manifest_hash);\n  734│            match self.pulse_tick(session_id) {\n  735│                Ok(report) => {\n     │...\n  752│\n *753│    /// Fetch history per the manifest `[history]` strategy (empty if none).\n *754│    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n  755│        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n *756│        // history fetch below), so a live-update refresh never waits on it.\n  757│        let manifest = self.cell.manifest_snapshot();\n *758│        let Some(h) = &manifest.history else {\n  759│            return Vec::new();\n  760│        };\n *761│        fetch_history(\n  762│            h,\n  763│            &self.base_keys(session_id),\n  764│            &self.id,\n     │...\n  774│    ///\n *775│    /// - **active → (dormant|suspended)**: the transition echo commune\n *776│    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n  777│    ///   outgoing instance's final context delta, BEFORE the flip persists\n  778│    ///   (KH 3.3) — it syncs to whichever instance activates next.\n  779│    /// - **wake (→ active)**: re-activation in place catches up immediately —\n     │...\n  799│            now_ms(),\n *800│            || self.fire_echo(session_id),\n  801│            || self.fire_wake_effects(),\n  802│        )?;\n  803│        // The shell leg of the edge (M5-D4a): suspend closes online shells,\n     │...\n  825│\n *826│    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.\n  827│    ///\n  828│    /// LIFECYCLE-TRUTH W1 skips (both Ok — a skip is a named configuration fact,\n  829│    /// never a strike):\n *830│    /// - **no `[session.echo_commune]` role** — the role is OPTIONAL per the\n  831│    ///   published contract; skip with a once-per-endpoint note\n  832│    ///   (REQ-PSYCHE-ROLE-OPTIONAL-SKIP — pre-fix this hard-failed every fire and\n  833│    ///   3-strike-stamped a healthy host).\n *834│    /// - **relative `commune_dir` with no recorded endpoint cwd** — skip LOUD, never\n *835│    ///   a raw relative write (REQ-ECHO-DROP-DIR-RESOLVE — pre-fix the raw manifest\n *836│    ///   dir went straight to `run_echo_commune`, which under the WMI-launched\n  837│    ///   daemon's System32 cwd = deterministic os error 5).\n  838│    ///\n  839│    /// The dir routes through [`resolve_endpoint_drop_dir`] — the SAME resolver the\n     │...\n  841│    /// the endpoint's cwd as its default working dir (a role-declared cwd wins).\n *842│    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n *843│    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n *844│        let Some(raw) = &self.commune_dir else {\n  845│            return Ok(());\n  846│        };\n  847│        // [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n *848│        if self.cell.manifest_snapshot().session.echo_commune.is_none() {\n *849│            note_echo_role_absent_once(&self.id);\n  850│            return Ok(());\n  851│        }\n  852│        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n     │...\n  855│        else {\n *856│            warn_echo_no_cwd_once(&self.id, raw);\n  857│            return Ok(());\n  858│        };\n *859│        let history = self.history(session_id);\n  860│        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n *861│        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n  862│        // dir so the summarizer child runs where the endpoint lives, never in the\n *863│        // daemon's own process cwd (REQ-ECHO-DROP-DIR-RESOLVE rider).\n  864│        let runtime = self\n  865│            .cell\n  866│            .runtime_snapshot()\n  867│            .with_spawn_cwd(endpoint_cwd.map(PathBuf::from));\n *868│        run_echo_commune(\n  869│            &runtime,\n  870│            &self.id,\n  871│            &self.base_keys(session_id),\n *872│            &history,\n  873│            &dir,\n  874│            LIVE_BOUNDED_TIMEOUT,\n  875│        )\n     │...\n  877│        // Every successful psyche operation clears a stale fault stamp — this\n *878│        // covers the checkpoint/rest-edge transition echo, the signoff echo, and\n *879│        // the pulse commune-sync (the event-turn leg clears via\n  880│        // `note_turn_outcome`'s Ok arm). A SKIP above deliberately does NOT clear:\n  881│        // nothing ran, so nothing was proven healthy.\n  882│        // [impl->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS]\n     │...\n  887│    /// Clear the perch's `psyche_host_error` fault stamp — called on EVERY\n *888│    /// successful psyche operation (event turn, transition/checkpoint echo,\n *889│    /// signoff echo), so a stale FAILED stamp can never sit over a healthy psyche\n  890│    /// (REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS — pre-fix only the pulse-loop\n  891│    /// `note_turn_outcome` Ok leg cleared it). Best-effort store write.\n  892│    // [impl->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS]\n     │...\n  949│    ///   composed mind `download_psyche_context`: role/live/project tiers + any pending\n *950│    ///   commune/signoff drop, or the `<fresh-psyche/>` marker when the mind composes\n  951│    ///   empty); a LATER (ContinueExisting) turn writes it EXACTLY 0 bytes (same\n  952│    ///   `{session_id}`, no re-preload — perri PIN 1: NO sentinel EVER). The shim\n  953│    ///   reads-file-then-trims and discriminates fresh-vs-continue on the FILE CONTENT\n     │...\n  974│    ///\n *975│    /// stdin = the EVENT/COMMUNE payload for THIS turn only. A bare periodic pulse (no\n  976│    /// pending event) feeds the minimal `<pulse/>` standing prompt. The composed mind\n  977│    /// does NOT ride stdin — it rides the `{psyche_context_file}` FILE, whose path is\n  978│    /// ALWAYS filled: the shim reads-then-trims it every turn and the fresh/continue\n     │...\n 1002│        // needs no `.live-bin` own-copy (unlike the retired resident spawn) — the\n*1003│        // install-dir/PATH snapshot is correct here (same class as translate/digest).\n 1004│        //\n 1005│        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n 1006│        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n     │...\n 1024│        // F-030 W2 — psyche-own sid custody. `base_keys`' `{session_id}` is the PARENT's\n*1025│        // (echo/history/signoff keep that fill); this turn OVERRIDES it to the psyche's\n 1026│        // OWN sid, LOCAL to the turn. Some(custody) → a LATER (ContinueExisting) turn:\n 1027│        // `{session_id}` = the stored sid; the `{psyche_context_file}` is written EXACTLY\n 1028│        // 0 bytes (continue). None → a FIRST (FreshWithPreload) turn: mint a UUID, persist\n     │...\n 1118│    /// (FreshWithPreload) psyche turn (F-030 W2/W4): the durable role/live/project tiers\n*1119│    /// plus any not-yet-synthesized commune/signoff drop, resolved against the parent\n 1120│    /// endpoint's bound cwd — the SAME composition `spt api psyche-download` emits.\n 1121│    /// `None` when nothing is stored yet (a role-less/context-less fresh endpoint —\n 1122│    /// the first turn then simply seeds from the minted `{session_id}` alone, the file\n     │...\n 1137│        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\n*1138│        let commune_file = self.psyche_drop_file(self.commune_dir.as_deref(), cwd_path, \"commune\");\n 1139│        let signoff_file = self.psyche_drop_file(self.signoff_dir.as_deref(), cwd_path, \"signoff\");\n 1140│        download_psyche_context(\n 1141│            &self.id,\n 1142│            &project_id,\n*1143│            commune_file.as_deref(),\n 1144│            signoff_file.as_deref(),\n 1145│        )\n 1146│    }\n     │...\n 1209│\n*1210│    /// Execute the graceful signoff (T6): echo-commune BEFORE teardown (3.3),\n 1211│    /// grace recheck before any signoff write (1.1). The graceful path is an\n 1212│    /// **intentional** shutdown, so it uses zero grace + an always-gone recheck —\n 1213│    /// the Self is leaving on purpose, there is nothing to recover. The\n     │...\n 1222│    /// *unexpected* harness death. It runs the identical load-bearing ordering —\n*1223│    /// echo-commune (3.3) → grace wait (1.1) → liveness recheck → teardown — but\n 1224│    /// with the **real** grace window from config and the **real** recheck\n 1225│    /// `still_gone`. A Self that recovered across the grace window (a binary\n 1226│    /// handoff relaunch, a brief stale poll) makes `still_gone` false and is\n     │...\n 1234│    // [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]\n*1235│    // [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]\n 1236│    pub fn supervised_crash_signoff(\n 1237│        &self,\n 1238│        session_id: Option<&str>,\n     │...\n 1246│    /// the 1.1 hazard turns on — the `grace` window and the `recheck` verdict — so\n*1247│    /// they are the only parameters; the echo (3.3) and teardown effects are\n 1248│    /// identical. Keeping one body guarantees the crash path can never drift out\n*1249│    /// of the echo-before-teardown ordering the graceful path proves.\n 1250│    fn signoff_with(\n 1251│        &self,\n 1252│        session_id: Option<&str>,\n     │...\n 1255│    ) -> Result<SignoffOutcome, String> {\n*1256│        let mut echo_err: Option<String> = None;\n 1257│        let mut teardown_err: Option<String> = None;\n 1258│        let outcome = graceful_signoff::<std::convert::Infallible>(\n 1259│            session_id,\n     │...\n 1261│            || {\n*1262│                if let Err(e) = self.fire_echo(session_id) {\n*1263│                    echo_err = Some(e);\n 1264│                }\n 1265│                Ok(())\n 1266│            },\n     │...\n 1278│        .unwrap_or(SignoffOutcome {\n*1279│            echo_ran: false,\n 1280│            signed_off: false,\n 1281│        });\n*1282│        if let Some(e) = echo_err {\n*1283│            return Err(format!(\"echo failed during signoff: {e}\"));\n 1284│        }\n 1285│        if let Some(e) = teardown_err {\n 1286│            return Err(format!(\"teardown failed during signoff: {e}\"));\n     │...\n 1367│\n*1368│    // A live-capable manifest: a Psyche init template + a commune dir to watch.\n 1369│    // The Psyche command is a trivial no-op (we never spawn it in these tests;\n 1370│    // we exercise the pulse/ingest loop, which does not launch the Psyche).\n 1371│    #[cfg(windows)]\n     │...\n 1375│\n*1376│    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {\n*1377│        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n 1378│        let toml = format!(\n 1379│            \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*1380│             [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 1381│             [session.psyche_init]\\ncommand='{NOOP_CMD}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\"\n 1382│        );\n 1383│        Manifest::from_toml_str(&toml).unwrap()\n     │...\n 1385│\n*1386│    // A stdin→stdout passthrough so the echo-commune summarizer succeeds and\n*1387│    // writes a real drop file — lets the signoff tests observe that the echo\n 1388│    // (3.3) actually ran before any teardown.\n 1389│    #[cfg(windows)]\n 1390│    const PASSTHROUGH: &str = \"sort\";\n     │...\n 1393│\n*1394│    // A live manifest that ALSO declares an echo-commune summarizer, so\n*1395│    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.\n*1396│    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {\n*1397│        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n 1398│        let toml = format!(\n 1399│            \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*1400│             [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 1401│             [session.psyche_init]\\ncommand='{NOOP_CMD}'\\ncwd=\\\"{{psyche_dir}}\\\"\\nkeys=[]\\n\\n\\\n*1402│             [session.echo_commune]\\ncommand='{PASSTHROUGH}'\\n\"\n 1403│        );\n 1404│        Manifest::from_toml_str(&toml).unwrap()\n 1405│    }\n     │...\n 1438│\n*1439│    /// Write a per-platform script that echoes `$CLAUDE_TEST_ROOT` to `marker`, and\n 1440│    /// return the `psyche_resume` command string that runs it. Shell logic lives in a\n 1441│    /// FILE, never an inline `sh -c '...'` — the tokenize single-quote-literal contract\n 1442│    /// (REQ-HAZARD-TEMPLATE-ARGV-FILL) forbids the inline form (the F-030 lesson).\n*1443│    fn write_env_echo_fixture(home: &std::path::Path, marker: &std::path::Path) -> String {\n 1444│        #[cfg(windows)]\n 1445│        {\n*1446│            let script = home.join(\"echo_env.bat\");\n 1447│            std::fs::write(\n 1448│                &script,\n*1449│                format!(\"@echo %CLAUDE_TEST_ROOT%>\\\"{}\\\"\\r\\n\", marker.display()),\n 1450│            )\n 1451│            .unwrap();\n 1452│            format!(\"cmd /C \\\"{}\\\"\", script.display())\n     │...\n 1455│        {\n*1456│            let script = home.join(\"echo_env.sh\");\n 1457│            std::fs::write(\n 1458│                &script,\n*1459│                format!(\"echo \\\"$CLAUDE_TEST_ROOT\\\" > \\\"{}\\\"\\n\", marker.display()),\n 1460│            )\n 1461│            .unwrap();\n 1462│            format!(\"sh {}\", script.display())\n     │...\n 1482│            let stamp = \"sentinel-acct-root-9f3a\";\n*1483│            let command = write_env_echo_fixture(home, &marker);\n*1484│            let commune = home.join(\"commune\");\n*1485│            std::fs::create_dir_all(&commune).unwrap();\n*1486│            let dir = commune.to_string_lossy().replace('\\\\', \"/\");\n 1487│            let toml = format!(\n 1488│                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*1489│                 [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 1490│                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n 1491│                 [session.psyche_resume]\\ncommand='{command}'\\nkeys=[]\\n\"\n 1492│            );\n     │...\n 1503│\n*1504│            let got = std::fs::read_to_string(&marker)\n 1505│                .expect(\"the psyche child must have spawned + written the marker\");\n 1506│            assert_eq!(\n 1507│                got.trim(),\n     │...\n 1539│\n*1540│    #[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.\n*1541│    fn pulse_tick_ingests_drop() {\n 1542│        with_home(|_| {\n 1543│            let drops = tempfile::tempdir().unwrap();\n 1544│            let manifest = live_manifest(drops.path());\n     │...\n 1547│\n*1548│            std::fs::write(drops.path().join(\"doyle-commune.md\"), \"brief\").unwrap();\n 1549│            let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 1550│            assert_eq!(report.ingested.len(), 1, \"the drop was folded in\");\n*1551│            assert!(!report.echo_fired, \"no echo gate armed\");\n 1552│            assert!(\n*1553│                !drops.path().join(\"doyle-commune.md\").exists(),\n 1554│                \"drop consumed\"\n 1555│            );\n 1556│        });\n     │...\n 1823│        let fresh = write_psyche_context_file(&perch, first_turn_psyche_context(Some(\"MIND\"))).unwrap();\n*1824│        assert_eq!(std::fs::read_to_string(&fresh).unwrap(), \"MIND\");\n 1825│        assert!(std::fs::metadata(&fresh).unwrap().len() > 0);\n 1826│\n 1827│        // FRESH with a zero-context agent: the NON-EMPTY <fresh-psyche/> marker (never\n     │...\n 1830│        assert_eq!(marker, cont, \"overwrite in place — same nested-perch path each turn\");\n*1831│        assert_eq!(std::fs::read_to_string(&marker).unwrap(), PSYCHE_FRESH_MARKER);\n 1832│        assert!(std::fs::metadata(&marker).unwrap().len() > 0);\n 1833│    }\n 1834│\n     │...\n 1981│    // [int->REQ-STORE-CONTEXT-BRANCH-FILL] use-it-like-a-human: a REAL pulse tick with\n*1982│    // the PRODUCTION-shape RELATIVE \".claude\" commune_dir + an endpoint whose info.cwd\n 1983│    // is a real project dir. The drop lands in <cwd>/.claude (where the agent drops it),\n 1984│    // and the fix resolves the relative dir against the endpoint cwd — so ingest FINDS\n 1985│    // it and the two-tier store FILLS: a-<id> + p-<project_id_for_dir(cwd)>. RED pre-fix\n     │...\n 1987│    #[test]\n*1988│    fn relative_commune_dir_resolves_against_endpoint_cwd_and_fills_project_branch() {\n 1989│        with_home(|_| {\n 1990│            // A real project cwd, NOT under the owlery — the endpoint's info.cwd.\n 1991│            let project = tempfile::tempdir().unwrap();\n     │...\n 2007│\n*2008│            // The agent drops a TWO-slice commune into <cwd>/.claude — the RELATIVE dir.\n 2009│            let drop_dir = project.path().join(\".claude\");\n 2010│            std::fs::create_dir_all(&drop_dir).unwrap();\n 2011│            std::fs::write(\n*2012│                drop_dir.join(\"doyle-commune.md\"),\n 2013│                \"<live-context>\\nwho I am\\n</live-context>\\n\\\n 2014│                 <project-context>\\nwhat I do here\\n</project-context>\",\n 2015│            )\n     │...\n 2017│\n*2018│            // The PRODUCTION claude-spt value: a RELATIVE commune_dir.\n 2019│            let manifest = live_manifest(Path::new(\".claude\"));\n 2020│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n 2021│                .expect(\"live host\");\n     │...\n 2028│            );\n*2029│            assert!(!drop_dir.join(\"doyle-commune.md\").exists(), \"drop consumed\");\n 2030│\n 2031│            // The two-tier store FILLED — the whole point of the RCA.\n 2032│            let cs = spt_store::contextstore::ContextStore::open_or_init().unwrap();\n     │...\n 2045│    /// Write a per-platform script that prints the child's OWN working directory\n*2046│    /// to stdout, and return the `[session.echo_commune]` command string that runs\n 2047│    /// it — the spawn-cwd rider probe (file-based, never inline `sh -c '...'`, the\n 2048│    /// tokenize single-quote-literal contract).\n*2049│    fn write_cwd_echo_fixture(home: &std::path::Path) -> String {\n 2050│        #[cfg(windows)]\n 2051│        {\n*2052│            let script = home.join(\"echo_cwd.bat\");\n 2053│            std::fs::write(&script, \"@cd\\r\\n\").unwrap();\n 2054│            format!(\"cmd /C \\\"{}\\\"\", script.display())\n 2055│        }\n     │...\n 2057│        {\n*2058│            let script = home.join(\"echo_cwd.sh\");\n 2059│            std::fs::write(&script, \"pwd\\n\").unwrap();\n 2060│            format!(\"sh {}\", script.display())\n 2061│        }\n     │...\n 2063│\n*2064│    // [unit->REQ-ECHO-DROP-DIR-RESOLVE] the ECHO fire resolves a RELATIVE manifest\n*2065│    // `commune_dir` (the PRODUCTION claude-spt \".claude\") against the ENDPOINT's\n 2066│    // recorded cwd through the SAME resolver the ingest leg uses — the drop lands in\n 2067│    // <endpoint cwd>/.claude, and the summarizer child itself RUNS in the endpoint\n 2068│    // cwd (the spawn-cwd rider). RED pre-fix: the raw relative dir went straight to\n*2069│    // `run_echo_commune`, which create_dir_all'd it against the DAEMON's process cwd\n*2070│    // (System32 under WMI = os error 5; a writable daemon cwd = communes silently\n 2071│    // lost to the wrong dir).\n 2072│    #[test]\n*2073│    fn echo_fire_resolves_relative_commune_dir_against_endpoint_cwd() {\n 2074│        with_home(|home| {\n 2075│            // A real project dir — the endpoint's recorded info.cwd.\n 2076│            let project = tempfile::tempdir().unwrap();\n     │...\n 2089│\n*2090│            // Manifest: RELATIVE commune_dir + an echo role that prints its own cwd.\n*2091│            let command = write_cwd_echo_fixture(home);\n 2092│            let toml = format!(\n 2093│                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*2094│                 [session]\\ncommune_dir=\\\".claude\\\"\\n\\n\\\n 2095│                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n*2096│                 [session.echo_commune]\\ncommand='{command}'\\n\"\n 2097│            );\n 2098│            let manifest = Manifest::from_toml_str(&toml).expect(\"manifest parses\");\n 2099│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n     │...\n 2101│\n*2102│            host.fire_echo(None).expect(\"echo fires clean\");\n 2103│\n 2104│            // The drop landed under the ENDPOINT cwd, not the daemon's.\n*2105│            let drop_path = project.path().join(\".claude\").join(\"doyle-commune.md\");\n*2106│            let body = std::fs::read_to_string(&drop_path)\n*2107│                .expect(\"commune drop written under <endpoint cwd>/.claude\");\n 2108│            // The rider: the summarizer child RAN in the endpoint cwd (its printed\n 2109│            // cwd is the project dir). Normalize separators/case for the compare.\n 2110│            let norm = |s: &str| s.replace('\\\\', \"/\").to_lowercase();\n     │...\n 2112│                norm(&body).contains(&norm(&project.path().to_string_lossy())),\n*2113│                \"echo child must run in the endpoint cwd, printed: {body:?}\"\n 2114│            );\n 2115│        });\n 2116│    }\n 2117│\n*2118│    // [unit->REQ-ECHO-DROP-DIR-RESOLVE] a RELATIVE commune_dir with NO recorded\n*2119│    // endpoint cwd = LOUD SKIP: fire_echo returns Ok (no strike), the summarizer is\n 2120│    // NEVER spawned, and nothing is written into whatever cwd the daemon happens to\n*2121│    // hold. RED pre-fix: the echo ran and wrote (or os-error-5'd) against the\n 2122│    // daemon's own process cwd.\n 2123│    #[test]\n*2124│    fn echo_fire_skips_loud_on_relative_dir_without_cwd() {\n 2125│        with_home(|home| {\n 2126│            // Perch record WITHOUT a cwd.\n 2127│            let perch_path = perch::resolve_perch_path(\"doyle\", ParentHint::Infer);\n     │...\n 2137│\n*2138│            // The echo role writes a marker file — if the skip happens BEFORE the\n 2139│            // spawn (as it must), the marker never appears.\n*2140│            let marker = home.join(\"echo_ran_marker.txt\");\n 2141│            #[cfg(windows)]\n 2142│            let command = {\n 2143│                let script = home.join(\"mark.bat\");\n*2144│                std::fs::write(&script, format!(\"@echo ran>\\\"{}\\\"\\r\\n\", marker.display()))\n 2145│                    .unwrap();\n 2146│                format!(\"cmd /C \\\"{}\\\"\", script.display())\n 2147│            };\n     │...\n 2150│                let script = home.join(\"mark.sh\");\n*2151│                std::fs::write(&script, format!(\"echo ran > \\\"{}\\\"\\n\", marker.display()))\n 2152│                    .unwrap();\n 2153│                format!(\"sh {}\", script.display())\n 2154│            };\n     │...\n 2156│                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*2157│                 [session]\\ncommune_dir=\\\".claude\\\"\\n\\n\\\n 2158│                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n*2159│                 [session.echo_commune]\\ncommand='{command}'\\n\"\n 2160│            );\n 2161│            let manifest = Manifest::from_toml_str(&toml).expect(\"manifest parses\");\n 2162│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n     │...\n 2164│\n*2165│            let out = host.fire_echo(None);\n 2166│            assert!(out.is_ok(), \"a no-cwd skip is Ok (no strike), got {out:?}\");\n 2167│            assert!(\n 2168│                !marker.exists(),\n     │...\n 2174│    // [unit->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS] the checkpoint/rest-edge path: a\n*2175│    // SUCCESSFUL transition echo (the suspend/checkpoint boundary commune) clears a\n 2176│    // pre-existing psyche_host_error stamp. RED pre-fix: only the pulse-loop\n 2177│    // `note_turn_outcome` Ok leg cleared, so a stale FAILED stamp sat over a healthy\n 2178│    // psyche across checkpoint/wake cycles (three field confirmations).\n 2179│    #[test]\n*2180│    fn rest_edge_echo_success_clears_stale_psyche_stamp() {\n 2181│        with_home(|_| {\n 2182│            let drops = tempfile::tempdir().unwrap();\n*2183│            let manifest = live_manifest_with_echo(drops.path());\n 2184│            let perch_path = seed_supervised_perch(\"doyle\", std::process::id());\n 2185│            // A stale fault stamp from a prior failing period.\n 2186│            spt_store::info::set_psyche_host_error(&perch_path, Some(\"stale failure\")).unwrap();\n     │...\n 2190│            // Active → Dormant: the checkpoint/suspend boundary — fires the\n*2191│            // transition echo (KH 3.3) through fire_echo.\n 2192│            let report = host\n 2193│                .rest_event(None, crate::resting::RestEvent::Detach)\n 2194│                .expect(\"rest edge applies\")\n 2195│                .expect(\"a real edge\");\n*2196│            assert!(report.echo_fired, \"the suspend edge fires the echo\");\n 2197│            assert!(\n*2198│                drops.path().join(\"doyle-commune.md\").exists(),\n*2199│                \"the transition echo wrote its commune\"\n 2200│            );\n 2201│            let info = spt_store::info::read_info(&perch_path).unwrap();\n 2202│            assert!(\n 2203│                info.psyche_host_error.is_none(),\n*2204│                \"a successful checkpoint-boundary echo clears the stale stamp, got {:?}\",\n 2205│                info.psyche_host_error\n 2206│            );\n 2207│        });\n     │...\n 2209│\n*2210│    // [unit->REQ-PSYCHE-ROLE-OPTIONAL-SKIP] a manifest with NO [session.echo_commune]\n*2211│    // role ticks STRIKE-FREE: commune-sync SKIPS (the role is optional per the\n 2212│    // published contract) and the fire's outcome is Ok, so repeated gate-armed pulses\n 2213│    // never stamp psyche_host_error. RED pre-fix: every fire hard-failed\n*2214│    // \"manifest declares no [session.echo_commune] role\" → 3 strikes → a FAILED stamp\n 2215│    // over a healthy host (perri's field filing).\n 2216│    #[test]\n 2217│    fn role_less_manifest_ticks_strike_free() {\n     │...\n 2220│            let dir = drops.path().to_string_lossy().replace('\\\\', \"/\");\n*2221│            // commune_dir + a WORKING psyche_resume, but NO echo_commune role.\n 2222│            let toml = format!(\n 2223│                \"[adapter]\\nname=\\\"mock\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n\\\n*2224│                 [session]\\ncommune_dir=\\\"{dir}\\\"\\n\\n\\\n 2225│                 [session.psyche_init]\\ncommand='{NOOP_CMD}'\\nkeys=[]\\n\\n\\\n 2226│                 [session.psyche_resume]\\ncommand='{PASSTHROUGH}'\\nkeys=[]\\n\"\n 2227│            );\n     │...\n 2234│            for _ in 0..3 {\n*2235│                // Arm the echo gate so the fire (turn + commune-sync) runs this tick.\n*2236│                let gate = perch::resolve_echo_gate_file(\"doyle\", ParentHint::Infer);\n 2237│                if let Some(p) = gate.parent() {\n 2238│                    std::fs::create_dir_all(p).unwrap();\n 2239│                }\n     │...\n 2312│            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n*2313│                let cur = std::fs::read(crate::deadline::anchor_path(\"pulse\"))\n 2314│                    .expect(\"anchor file exists after loop entry\");\n 2315│                let mut slot = first_bytes.lock().unwrap();\n 2316│                match &*slot {\n     │...\n 2355│            let anchor_of = || {\n*2356│                let raw = std::fs::read_to_string(crate::deadline::anchor_path(\"pulse\")).unwrap();\n 2357│                serde_json::from_str::<DeadlineAnchor>(&raw).unwrap().anchor_ms\n 2358│            };\n 2359│\n     │...\n 2426│    // [unit->REQ-INST-4] the daemon-hosted rest edge fires the REAL transition\n*2427│    // echo commune (run_echo_commune through the manifest summarizer) exactly\n 2428│    // once per active→resting edge — the final context delta lands in the\n*2429│    // commune drop dir, where it syncs to whichever instance activates next;\n*2430│    // the dormant→suspended edge fires no second echo.\n 2431│    // [unit->REQ-INST-3] and the wake edge re-activates in place: it\n 2432│    // resurfaces undismissed notifs at the Wake boundary and drops the\n 2433│    // freshness-pull marker the peer pump consumes.\n 2434│    #[test]\n*2435│    fn rest_event_fires_echo_once_and_wake_resurfaces() {\n 2436│        with_home(|home| {\n 2437│            let drops = tempfile::tempdir().unwrap();\n*2438│            let manifest = live_manifest_with_echo(drops.path());\n 2439│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", DaemonConfig::default())\n 2440│                .expect(\"live host\");\n 2441│            seed_supervised_perch(\"doyle\", std::process::id());\n     │...\n 2453│\n*2454│            // active → dormant: the echo commune wrote the final delta.\n*2455│            let commune = drops.path().join(\"doyle-commune.md\");\n 2456│            let report = host\n 2457│                .rest_event(Some(\"sid-1\"), crate::resting::RestEvent::Detach)\n 2458│                .expect(\"apply ok\")\n     │...\n 2460│            assert!(\n*2461│                report.echo_fired,\n*2462│                \"active→dormant fires the transition echo\"\n 2463│            );\n*2464│            assert!(commune.exists(), \"the echo wrote a commune before the flip\");\n*2465│            std::fs::remove_file(&commune).unwrap();\n 2466│\n*2467│            // dormant → suspended: already resting — no second echo.\n 2468│            let report = host\n 2469│                .rest_event(Some(\"sid-1\"), crate::resting::RestEvent::Suspend)\n 2470│                .expect(\"apply ok\")\n 2471│                .expect(\"edge\");\n*2472│            assert!(!report.echo_fired, \"resting→resting fires no echo\");"},"isError":false,"timestamp":1784271486382}}
{"type":"message","id":"86b0b3f2","parentId":"c4a0f889","timestamp":"2026-07-17T06:58:06.389Z","message":{"role":"toolResult","toolCallId":"call_Y5QqpDwxlKtuE7zl6Pp5zlsM|fc_038f28d3e93bbfd4016a59d27e46288196a2473b4842718336","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### brainproc.rs\n 62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n*63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n 64|\n 65|/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n 66|/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n...\n 259|    loop {\n*260|        thread::sleep(BRAIN_HEARTBEAT);\n 261|        // `net-status` is both the net-capability probe and the broker-liveness\n 262|        // probe: an `Err` is the broker gone (or a future planned\n 263|        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n### config.rs\n 4|//! In the no-daemon interim model the pulse cadence was a hard-coded\n*5|//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n 6|//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n 7|//! period is **durable and configurable**, sourced from a small JSON file under\n 8|//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n...\n 21|/// override it per machine.\n*22|pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n 23|\n 24|/// Default drop-file precedence protection window (epoch-ms), mirroring the\n 25|/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n...\n 92|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n*93|    pulse_period_ms: Option<u64>,\n 94|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 95|    protection_window_ms: Option<u64>,\n 96|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n...\n 128|/// Resolved daemon configuration. The brain's scheduled loops read their cadence\n*129|/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.\n 130|/// (No longer `Copy` — `resources_blurb` carries a `String`; callers clone.)\n 131|#[derive(Debug, Clone, PartialEq, Eq)]\n 132|pub struct DaemonConfig {\n*133|    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).\n*134|    pub pulse_period: Duration,\n 135|    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136|    pub protection_window_ms: u64,\n 137|    /// The orphan-watch grace window (B7): the recovery wait before a\n...\n 204|        DaemonConfig {\n*205|            pulse_period: DEFAULT_PULSE_PERIOD,\n 206|            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\n 207|            crash_grace: DEFAULT_CRASH_GRACE,\n 208|            full_auto_update: false, // gated by default (REQ-UPD-4)\n...\n 254|        DaemonConfig {\n*255|            pulse_period: file\n*256|                .pulse_period_ms\n 257|                .map(Duration::from_millis)\n*258|                .unwrap_or(d.pulse_period),\n 259|            protection_window_ms: file.protection_window_ms.unwrap_or(d.protection_window_ms),\n 260|            crash_grace: file\n 261|                .crash_grace_ms\n...\n 302|        let file = DaemonConfigFile {\n*303|            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),\n 304|            protection_window_ms: Some(self.protection_window_ms),\n 305|            crash_grace_ms: Some(self.crash_grace.as_millis() as u64),\n 306|            full_auto_update: Some(self.full_auto_update),\n...\n 362|        assert_eq!(cfg, DaemonConfig::default());\n*363|        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);\n 364|    }\n 365|\n 366|    #[test] // [unit->REQ-DAEMON-1] the pulse period is config-driven, not a constant.\n*367|    fn pulse_period_is_config_driven() {\n 368|        let dir = tempfile::tempdir().unwrap();\n 369|        let path = dir.path().join(\"daemon.json\");\n*370|        std::fs::write(&path, r#\"{\"pulse_period_ms\": 250}\"#).unwrap();\n 371|\n 372|        let cfg = DaemonConfig::load_from(&path);\n*373|        assert_eq!(cfg.pulse_period, Duration::from_millis(250));\n 374|        assert_ne!(\n*375|            cfg.pulse_period, DEFAULT_PULSE_PERIOD,\n 376|            \"must override the default\"\n 377|        );\n 378|        // The unspecified field keeps its default.\n...\n 394|        let cfg = DaemonConfig {\n*395|            pulse_period: Duration::from_millis(750),\n 396|            protection_window_ms: 99,\n 397|            crash_grace: Duration::from_millis(1234),\n 398|            full_auto_update: true,\n...\n 527|        // Unspecified fields keep their defaults.\n*528|        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);\n 529|        // An absent file yields the default grace.\n 530|        assert_eq!(\n 531|            DaemonConfig::load_from(&dir.path().join(\"nope.json\")).crash_grace,\n...\n 576|        // key) loads as empty — forward/back-compat, never a parse failure.\n*577|        std::fs::write(&path, r#\"{\"pulse_period_ms\": 5000}\"#).unwrap();\n 578|        assert!(\n 579|            DaemonConfig::load_from(&path).startup_endpoints.is_empty(),\n 580|            \"a file without the key back-loads as empty, not an error\"\n...\n 599|            let seeded = DaemonConfig {\n*600|                pulse_period: Duration::from_millis(1234),\n 601|                detached_subnets: vec![\"work\".to_string()],\n 602|                ..DaemonConfig::default()\n 603|            };\n### deadline.rs\n 39|use crate::brainproc::StartReason;\n*40|use crate::config::DEFAULT_PULSE_PERIOD;\n 41|\n 42|/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n 43|///\n...\n 73|        }\n*74|        let default_ms = DEFAULT_PULSE_PERIOD.as_millis() as u64;\n 75|        default_ms.max(1)\n 76|    }\n 77|\n...\n 292|        let got = DeadlineAnchor::effective_interval(0);\n*293|        assert_eq!(got, DEFAULT_PULSE_PERIOD.as_millis() as u64);\n 294|        assert!(got > 0, \"the derive must never divide by zero\");\n 295|        // A non-zero interval passes through untouched.\n 296|        assert_eq!(DeadlineAnchor::effective_interval(250), 250);\n### lifecycle.rs\n 506|    /// The configured pulse period (config-driven, not a constant).\n*507|    pub fn pulse_period(&self) -> Duration {\n*508|        self.cfg.pulse_period\n 509|    }\n 510|\n 511|    /// Base substitution keys spt-core fills for the lifecycle templates.\n...\n 677|    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n*678|    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n 679|    /// restart instant on every restart. Now the loop rehydrates a\n 680|    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n 681|    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n...\n 701|        let key = session_id.unwrap_or(\"pulse\");\n*702|        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\n 703|        // A disk error must not stop the pulse: degrade to an in-memory anchor at\n 704|        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\n 705|        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\n...\n 900|    fn event_turn_timeout(&self) -> Duration {\n*901|        event_turn_timeout_for(self.pulse_period())\n 902|    }\n 903|\n 904|    /// The agent's OWN subnet — the parent endpoint's `home_subnet` (info.json),\n...\n 1317|    /// set before any death was observed. `owner_pid` is the supervised harness\n*1318|    /// pid; the poll cadence reuses the configured `pulse_period` (the same\n 1319|    /// scheduled-supervision clock as the pulse loop).\n 1320|    // [impl->REQ-DAEMON-4]\n 1321|    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]\n...\n 1339|            }\n*1340|            sleep_interruptible(self.cfg.pulse_period, stop);\n 1341|        }\n 1342|        None\n 1343|    }\n...\n 2272|            let cfg = DaemonConfig {\n*2273|                pulse_period: Duration::from_millis(15),\n 2274|                ..DaemonConfig::default()\n 2275|            };\n 2276|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n*2277|            assert_eq!(host.pulse_period(), Duration::from_millis(15));\n 2278|\n 2279|            let stop = AtomicBool::new(false);\n 2280|            let ticks = AtomicUsize::new(0);\n...\n 2304|            let cfg = DaemonConfig {\n*2305|                pulse_period: Duration::from_millis(10),\n 2306|                ..DaemonConfig::default()\n 2307|            };\n 2308|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n...\n 2339|            let cfg = DaemonConfig {\n*2340|                pulse_period: Duration::from_millis(10),\n 2341|                ..DaemonConfig::default()\n 2342|            };\n 2343|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n...\n 2602|            let cfg = DaemonConfig {\n*2603|                pulse_period: Duration::from_millis(10),\n 2604|                crash_grace: Duration::from_millis(10),\n 2605|                ..DaemonConfig::default()\n 2606|            };\n...\n 2634|            let cfg = DaemonConfig {\n*2635|                pulse_period: Duration::from_millis(10),\n 2636|                ..DaemonConfig::default()\n 2637|            };\n 2638|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n### livehost.rs\n 1100|        DaemonConfig {\n*1101|            pulse_period: Duration::from_millis(15),\n 1102|            ..DaemonConfig::default()\n 1103|        }\n 1104|    }\n\n## spt-live/src/\n### pulse.rs\n 30|/// per-endpoint override) is the M3 daemon's; this is the interim constant.\n*31|pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n 32|\n 33|/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`\n 34|/// is threaded into the drop-file precedence guard (6.5).\n...\n 43|        PulseConfig {\n*44|            period: DEFAULT_PULSE_PERIOD,\n 45|            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\n 46|        }\n 47|    }"}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt-live/src, crates/spt-store/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":41,"fileCount":6,"files":["crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-live/src/pulse.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":2},{"path":"crates/spt-daemon/src/config.rs","count":20},{"path":"crates/spt-daemon/src/deadline.rs","count":3},{"path":"crates/spt-daemon/src/lifecycle.rs","count":13},{"path":"crates/spt-daemon/src/livehost.rs","count":1},{"path":"crates/spt-live/src/pulse.rs","count":2}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/src/\n### brainproc.rs\n  62│/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n *63│const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n  64│\n  65│/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n  66│/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n    │...\n 259│    loop {\n*260│        thread::sleep(BRAIN_HEARTBEAT);\n 261│        // `net-status` is both the net-capability probe and the broker-liveness\n 262│        // probe: an `Err` is the broker gone (or a future planned\n 263│        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n### config.rs\n   4│//! In the no-daemon interim model the pulse cadence was a hard-coded\n  *5│//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n   6│//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n   7│//! period is **durable and configurable**, sourced from a small JSON file under\n   8│//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n    │...\n  21│/// override it per machine.\n *22│pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n  23│\n  24│/// Default drop-file precedence protection window (epoch-ms), mirroring the\n  25│/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n    │...\n  92│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n *93│    pulse_period_ms: Option<u64>,\n  94│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n  95│    protection_window_ms: Option<u64>,\n  96│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    │...\n 128│/// Resolved daemon configuration. The brain's scheduled loops read their cadence\n*129│/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.\n 130│/// (No longer `Copy` — `resources_blurb` carries a `String`; callers clone.)\n 131│#[derive(Debug, Clone, PartialEq, Eq)]\n 132│pub struct DaemonConfig {\n*133│    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).\n*134│    pub pulse_period: Duration,\n 135│    /// The drop-file precedence protection window in epoch-ms (6.5).\n 136│    pub protection_window_ms: u64,\n 137│    /// The orphan-watch grace window (B7): the recovery wait before a\n    │...\n 204│        DaemonConfig {\n*205│            pulse_period: DEFAULT_PULSE_PERIOD,\n 206│            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\n 207│            crash_grace: DEFAULT_CRASH_GRACE,\n 208│            full_auto_update: false, // gated by default (REQ-UPD-4)\n    │...\n 254│        DaemonConfig {\n*255│            pulse_period: file\n*256│                .pulse_period_ms\n 257│                .map(Duration::from_millis)\n*258│                .unwrap_or(d.pulse_period),\n 259│            protection_window_ms: file.protection_window_ms.unwrap_or(d.protection_window_ms),\n 260│            crash_grace: file\n 261│                .crash_grace_ms\n    │...\n 302│        let file = DaemonConfigFile {\n*303│            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),\n 304│            protection_window_ms: Some(self.protection_window_ms),\n 305│            crash_grace_ms: Some(self.crash_grace.as_millis() as u64),\n 306│            full_auto_update: Some(self.full_auto_update),\n    │...\n 362│        assert_eq!(cfg, DaemonConfig::default());\n*363│        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);\n 364│    }\n 365│\n 366│    #[test] // [unit->REQ-DAEMON-1] the pulse period is config-driven, not a constant.\n*367│    fn pulse_period_is_config_driven() {\n 368│        let dir = tempfile::tempdir().unwrap();\n 369│        let path = dir.path().join(\"daemon.json\");\n*370│        std::fs::write(&path, r#\"{\"pulse_period_ms\": 250}\"#).unwrap();\n 371│\n 372│        let cfg = DaemonConfig::load_from(&path);\n*373│        assert_eq!(cfg.pulse_period, Duration::from_millis(250));\n 374│        assert_ne!(\n*375│            cfg.pulse_period, DEFAULT_PULSE_PERIOD,\n 376│            \"must override the default\"\n 377│        );\n 378│        // The unspecified field keeps its default.\n    │...\n 394│        let cfg = DaemonConfig {\n*395│            pulse_period: Duration::from_millis(750),\n 396│            protection_window_ms: 99,\n 397│            crash_grace: Duration::from_millis(1234),\n 398│            full_auto_update: true,\n    │...\n 527│        // Unspecified fields keep their defaults.\n*528│        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);\n 529│        // An absent file yields the default grace.\n 530│        assert_eq!(\n 531│            DaemonConfig::load_from(&dir.path().join(\"nope.json\")).crash_grace,\n    │...\n 576│        // key) loads as empty — forward/back-compat, never a parse failure.\n*577│        std::fs::write(&path, r#\"{\"pulse_period_ms\": 5000}\"#).unwrap();\n 578│        assert!(\n 579│            DaemonConfig::load_from(&path).startup_endpoints.is_empty(),\n 580│            \"a file without the key back-loads as empty, not an error\"\n    │...\n 599│            let seeded = DaemonConfig {\n*600│                pulse_period: Duration::from_millis(1234),\n 601│                detached_subnets: vec![\"work\".to_string()],\n 602│                ..DaemonConfig::default()\n 603│            };\n### deadline.rs\n  39│use crate::brainproc::StartReason;\n *40│use crate::config::DEFAULT_PULSE_PERIOD;\n  41│\n  42│/// The canonical anchor path for a loop key: `<spt_home>/deadline-<key>.json`.\n  43│///\n    │...\n  73│        }\n *74│        let default_ms = DEFAULT_PULSE_PERIOD.as_millis() as u64;\n  75│        default_ms.max(1)\n  76│    }\n  77│\n    │...\n 292│        let got = DeadlineAnchor::effective_interval(0);\n*293│        assert_eq!(got, DEFAULT_PULSE_PERIOD.as_millis() as u64);\n 294│        assert!(got > 0, \"the derive must never divide by zero\");\n 295│        // A non-zero interval passes through untouched.\n 296│        assert_eq!(DeadlineAnchor::effective_interval(250), 250);\n### lifecycle.rs\n  506│    /// The configured pulse period (config-driven, not a constant).\n *507│    pub fn pulse_period(&self) -> Duration {\n *508│        self.cfg.pulse_period\n  509│    }\n  510│\n  511│    /// Base substitution keys spt-core fills for the lifecycle templates.\n     │...\n  677│    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n *678│    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n  679│    /// restart instant on every restart. Now the loop rehydrates a\n  680│    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n  681│    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n     │...\n  701│        let key = session_id.unwrap_or(\"pulse\");\n *702│        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\n  703│        // A disk error must not stop the pulse: degrade to an in-memory anchor at\n  704│        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\n  705│        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\n     │...\n  900│    fn event_turn_timeout(&self) -> Duration {\n *901│        event_turn_timeout_for(self.pulse_period())\n  902│    }\n  903│\n  904│    /// The agent's OWN subnet — the parent endpoint's `home_subnet` (info.json),\n     │...\n 1317│    /// set before any death was observed. `owner_pid` is the supervised harness\n*1318│    /// pid; the poll cadence reuses the configured `pulse_period` (the same\n 1319│    /// scheduled-supervision clock as the pulse loop).\n 1320│    // [impl->REQ-DAEMON-4]\n 1321│    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]\n     │...\n 1339│            }\n*1340│            sleep_interruptible(self.cfg.pulse_period, stop);\n 1341│        }\n 1342│        None\n 1343│    }\n     │...\n 2272│            let cfg = DaemonConfig {\n*2273│                pulse_period: Duration::from_millis(15),\n 2274│                ..DaemonConfig::default()\n 2275│            };\n 2276│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n*2277│            assert_eq!(host.pulse_period(), Duration::from_millis(15));\n 2278│\n 2279│            let stop = AtomicBool::new(false);\n 2280│            let ticks = AtomicUsize::new(0);\n     │...\n 2304│            let cfg = DaemonConfig {\n*2305│                pulse_period: Duration::from_millis(10),\n 2306│                ..DaemonConfig::default()\n 2307│            };\n 2308│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n     │...\n 2339│            let cfg = DaemonConfig {\n*2340│                pulse_period: Duration::from_millis(10),\n 2341│                ..DaemonConfig::default()\n 2342│            };\n 2343│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n     │...\n 2602│            let cfg = DaemonConfig {\n*2603│                pulse_period: Duration::from_millis(10),\n 2604│                crash_grace: Duration::from_millis(10),\n 2605│                ..DaemonConfig::default()\n 2606│            };\n     │...\n 2634│            let cfg = DaemonConfig {\n*2635│                pulse_period: Duration::from_millis(10),\n 2636│                ..DaemonConfig::default()\n 2637│            };\n 2638│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n### livehost.rs\n 1100│        DaemonConfig {\n*1101│            pulse_period: Duration::from_millis(15),\n 1102│            ..DaemonConfig::default()\n 1103│        }\n 1104│    }\n\n## spt-live/src/\n### pulse.rs\n 30│/// per-endpoint override) is the M3 daemon's; this is the interim constant.\n*31│pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n 32│\n 33│/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`\n 34│/// is threaded into the drop-file precedence guard (6.5).\n   │...\n 43│        PulseConfig {\n*44│            period: DEFAULT_PULSE_PERIOD,\n 45│            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\n 46│        }\n 47│    }"},"isError":false,"timestamp":1784271486389}}
{"type":"message","id":"b53e3656","parentId":"86b0b3f2","timestamp":"2026-07-17T06:58:06.391Z","message":{"role":"toolResult","toolCallId":"call_a9hUTAXLPdoTjgNwqrFwvScY|fc_038f28d3e93bbfd4016a59d27e463481968011b9ae3552ec48","toolName":"read","content":[{"type":"text","text":"//! Project-index invalidation events — the store-anchored nudge queue between\n//! mutation sites and the daemon's single-flight index writer (ADR-0037 §3,\n//! REQ-PROJECT-INDEX-INVALIDATION).\n//!\n//! Mutation seams (session append, context-store commit, rename, fork, purge)\n//! run in MANY processes — CLI verbs, adapter hooks, the daemon brain itself —\n//! and the brain deliberately does not drain its broker connection\n//! (brainproc's non-draining hazard), so a socket push is the wrong carrier.\n//! Instead every seam drops a tiny event file under\n//! `$SPT_HOME/index/invalidations/`; the writer polls the directory cheaply\n//! (one `read_dir` — pure file I/O, no git), DRAINS all pending events, and\n//! coalesces them into one refresh (multiple invalidations → one reconcile).\n//! Daemon-offline nudges simply accumulate and are consumed at the next brain\n//! start — the same recover-by-reading-state stance as the rest of the store.\n//!\n//! Nudging is **fire-and-forget best-effort**: an unwritable queue must never\n//! fail the mutation that triggered it (the periodic reconcile backstop\n//! catches anything a lost nudge missed).\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicU64, Ordering};\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::projindex;\n\n/// One invalidation event — the refresh scope a mutation site requests\n/// (ADR-0037 §3 event set).\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(tag = \"scope\", rename_all = \"kebab-case\")]\npub enum InvalScope {\n    /// A context-store mutation (any `p-*`/`a-*` commit, rename, fork): ONE\n    /// debounced global membership refresh — branch-tip fingerprints decide\n    /// what actually rescans.\n    Global,\n    /// A per-endpoint event (new session/cwd, bind/start, purge): refresh that\n    /// endpoint's row. `cwd`, when present, also invalidates the shared\n    /// cwd-derivation cache entry for that dir (a new session may sit in a dir\n    /// whose repo identity changed since it was last derived).\n    Endpoint {\n        id: String,\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\n        cwd: Option<String>,\n    },\n}\n\n/// `$SPT_HOME/index/invalidations/` — the drop-dir the writer drains.\npub fn invalidations_dir() -> PathBuf {\n    projindex::index_dir().join(\"invalidations\")\n}\n\n/// The drop-dir under an EXPLICIT home root — the form store-layer mutation\n/// funnels use (they carry their own root and must not read ambient env; a\n/// test-rooted store then nudges into its own temp tree, never the real home).\npub fn invalidations_dir_under(home: &Path) -> PathBuf {\n    home.join(\"index\").join(\"invalidations\")\n}\n\n/// Process-local uniquifier so two nudges in one nanosecond don't collide.\nstatic NUDGE_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// Drop one event file into `dir`. Unique name = nanos + pid + a process-local\n/// sequence; the write is a plain create (no atomic dance needed — a torn\n/// event file is skipped by the tolerant drain and the backstop covers it).\npub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\n    std::fs::create_dir_all(dir)?;\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    let name = format!(\n        \"{nanos}-{}-{}.json\",\n        std::process::id(),\n        NUDGE_SEQ.fetch_add(1, Ordering::Relaxed)\n    );\n    let body = serde_json::to_string(scope)\n        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;\n    std::fs::write(dir.join(name), body)\n}\n\n/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\n/// the fire-and-forget form every mutation seam calls (an unwritable queue\n/// must never fail the mutation; the periodic reconcile is the backstop).\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn nudge(scope: &InvalScope) {\n    let _ = nudge_at(&invalidations_dir(), scope);\n}\n\n/// Whether any event is pending in `dir` — the writer's cheap poll (one\n/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\npub fn pending_at(dir: &Path) -> bool {\n    std::fs::read_dir(dir)\n        .map(|mut d| d.next().is_some())\n        .unwrap_or(false)\n}\n\n/// Drain EVERY pending event out of `dir`: read + delete each file, tolerant\n/// of unparseable bodies (skipped + removed) and of files vanishing mid-walk\n/// (a concurrent drain — only the daemon writer drains in production, but the\n/// contract must not depend on that). The returned list is the coalescing\n/// input: the caller unions scopes into one refresh.\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn drain_at(dir: &Path) -> Vec<InvalScope> {\n    let Ok(entries) = std::fs::read_dir(dir) else {\n        return Vec::new();\n    };\n    let mut out = Vec::new();\n    for entry in entries.flatten() {\n        let path = entry.path();\n        if let Ok(body) = std::fs::read_to_string(&path) {\n            if let Ok(scope) = serde_json::from_str::<InvalScope>(&body) {\n                out.push(scope);\n            }\n        }\n        let _ = std::fs::remove_file(&path);\n    }\n    out\n}\n\n/// The coalesced form of a drained event batch — what one reconcile consumes\n/// (multiple invalidations → ONE refresh, ADR-0037 §3).\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct Coalesced {\n    /// Any [`InvalScope::Global`] event was present → membership refresh\n    /// (fingerprint-gated).\n    pub global: bool,\n    /// Endpoint ids needing a row refresh (deduped, sorted).\n    pub endpoints: std::collections::BTreeSet<String>,\n    /// Cwds whose shared derivation-cache entry must be re-derived (deduped\n    /// on the ORIGINAL spelling; the writer normalizes).\n    pub cwds: std::collections::BTreeSet<String>,\n}\n\nimpl Coalesced {\n    pub fn is_empty(&self) -> bool {\n        !self.global && self.endpoints.is_empty() && self.cwds.is_empty()\n    }\n}\n\n/// Union a drained batch into one refresh request — the coalescing proof\n/// surface (N events of any mix in, ONE `Coalesced` out).\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn coalesce(events: Vec<InvalScope>) -> Coalesced {\n    let mut out = Coalesced::default();\n    for e in events {\n        match e {\n            InvalScope::Global => out.global = true,\n            InvalScope::Endpoint { id, cwd } => {\n                out.endpoints.insert(id);\n                if let Some(c) = cwd {\n                    out.cwds.insert(c);\n                }\n            }\n        }\n    }\n    out\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tempfile::tempdir;\n\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] the queue roundtrip: N nudges of\n    // every scope class land as N files, drain returns them ALL and empties the\n    // dir (drained = consumed), and a second drain is empty (no double-fire).\n    #[test]\n    fn nudge_drain_roundtrip_consumes_queue() {\n        let tmp = tempdir().unwrap();\n        let dir = tmp.path().join(\"invalidations\");\n        assert!(!pending_at(&dir), \"missing dir = nothing pending\");\n\n        nudge_at(&dir, &InvalScope::Global).unwrap();\n        nudge_at(\n            &dir,\n            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\n        )\n        .unwrap();\n        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\n        assert!(pending_at(&dir));\n\n        let drained = drain_at(&dir);\n        assert_eq!(drained.len(), 3);\n        assert!(!pending_at(&dir), \"drain consumes every event\");\n        assert!(drain_at(&dir).is_empty(), \"no double-fire\");\n    }\n\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] coalescing: a burst of mixed events\n    // unions into ONE refresh request — global sticky, endpoint ids + cwds\n    // deduped. This is the \"multiple invalidations coalesce into one refresh\"\n    // contract at the data level.\n    #[test]\n    fn burst_coalesces_into_one_refresh() {\n        let batch = vec![\n            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n            InvalScope::Global,\n            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\n            InvalScope::Global,\n        ];\n        let one = coalesce(batch);\n        assert!(one.global);\n        assert_eq!(\n            one.endpoints.iter().cloned().collect::<Vec<_>>(),\n            vec![\"a\".to_string(), \"b\".to_string()]\n        );\n        assert_eq!(one.cwds.len(), 1);\n        assert!(!one.is_empty());\n        assert!(coalesce(Vec::new()).is_empty());\n    }\n\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] tolerant drain: a torn/garbage\n    // event file is skipped AND removed (never wedges the queue), parseable\n    // siblings still deliver.\n    #[test]\n    fn garbage_event_is_skipped_and_removed() {\n        let tmp = tempdir().unwrap();\n        let dir = tmp.path().join(\"invalidations\");\n        std::fs::create_dir_all(&dir).unwrap();\n        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\n        nudge_at(&dir, &InvalScope::Global).unwrap();\n\n…\n    }\n}\n\n[Showing lines 1-223 of 229. Use :224 to continue]"}],"details":{"truncation":{"content":"//! Project-index invalidation events — the store-anchored nudge queue between\r\n//! mutation sites and the daemon's single-flight index writer (ADR-0037 §3,\r\n//! REQ-PROJECT-INDEX-INVALIDATION).\r\n//!\r\n//! Mutation seams (session append, context-store commit, rename, fork, purge)\r\n//! run in MANY processes — CLI verbs, adapter hooks, the daemon brain itself —\r\n//! and the brain deliberately does not drain its broker connection\r\n//! (brainproc's non-draining hazard), so a socket push is the wrong carrier.\r\n//! Instead every seam drops a tiny event file under\r\n//! `$SPT_HOME/index/invalidations/`; the writer polls the directory cheaply\r\n//! (one `read_dir` — pure file I/O, no git), DRAINS all pending events, and\r\n//! coalesces them into one refresh (multiple invalidations → one reconcile).\r\n//! Daemon-offline nudges simply accumulate and are consumed at the next brain\r\n//! start — the same recover-by-reading-state stance as the rest of the store.\r\n//!\r\n//! Nudging is **fire-and-forget best-effort**: an unwritable queue must never\r\n//! fail the mutation that triggered it (the periodic reconcile backstop\r\n//! catches anything a lost nudge missed).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::projindex;\r\n\r\n/// One invalidation event — the refresh scope a mutation site requests\r\n/// (ADR-0037 §3 event set).\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(tag = \"scope\", rename_all = \"kebab-case\")]\r\npub enum InvalScope {\r\n    /// A context-store mutation (any `p-*`/`a-*` commit, rename, fork): ONE\r\n    /// debounced global membership refresh — branch-tip fingerprints decide\r\n    /// what actually rescans.\r\n    Global,\r\n    /// A per-endpoint event (new session/cwd, bind/start, purge): refresh that\r\n    /// endpoint's row. `cwd`, when present, also invalidates the shared\r\n    /// cwd-derivation cache entry for that dir (a new session may sit in a dir\r\n    /// whose repo identity changed since it was last derived).\r\n    Endpoint {\r\n        id: String,\r\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n        cwd: Option<String>,\r\n    },\r\n}\r\n\r\n/// `$SPT_HOME/index/invalidations/` — the drop-dir the writer drains.\r\npub fn invalidations_dir() -> PathBuf {\r\n    projindex::index_dir().join(\"invalidations\")\r\n}\r\n\r\n/// The drop-dir under an EXPLICIT home root — the form store-layer mutation\r\n/// funnels use (they carry their own root and must not read ambient env; a\r\n/// test-rooted store then nudges into its own temp tree, never the real home).\r\npub fn invalidations_dir_under(home: &Path) -> PathBuf {\r\n    home.join(\"index\").join(\"invalidations\")\r\n}\r\n\r\n/// Process-local uniquifier so two nudges in one nanosecond don't collide.\r\nstatic NUDGE_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// Drop one event file into `dir`. Unique name = nanos + pid + a process-local\r\n/// sequence; the write is a plain create (no atomic dance needed — a torn\r\n/// event file is skipped by the tolerant drain and the backstop covers it).\r\npub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\r\n    std::fs::create_dir_all(dir)?;\r\n    let nanos = std::time::SystemTime::now()\r\n        .duration_since(std::time::UNIX_EPOCH)\r\n        .map(|d| d.as_nanos())\r\n        .unwrap_or(0);\r\n    let name = format!(\r\n        \"{nanos}-{}-{}.json\",\r\n        std::process::id(),\r\n        NUDGE_SEQ.fetch_add(1, Ordering::Relaxed)\r\n    );\r\n    let body = serde_json::to_string(scope)\r\n        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;\r\n    std::fs::write(dir.join(name), body)\r\n}\r\n\r\n/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\r\n/// the fire-and-forget form every mutation seam calls (an unwritable queue\r\n/// must never fail the mutation; the periodic reconcile is the backstop).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn nudge(scope: &InvalScope) {\r\n    let _ = nudge_at(&invalidations_dir(), scope);\r\n}\r\n\r\n/// Whether any event is pending in `dir` — the writer's cheap poll (one\r\n/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\r\npub fn pending_at(dir: &Path) -> bool {\r\n    std::fs::read_dir(dir)\r\n        .map(|mut d| d.next().is_some())\r\n        .unwrap_or(false)\r\n}\r\n\r\n/// Drain EVERY pending event out of `dir`: read + delete each file, tolerant\r\n/// of unparseable bodies (skipped + removed) and of files vanishing mid-walk\r\n/// (a concurrent drain — only the daemon writer drains in production, but the\r\n/// contract must not depend on that). The returned list is the coalescing\r\n/// input: the caller unions scopes into one refresh.\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn drain_at(dir: &Path) -> Vec<InvalScope> {\r\n    let Ok(entries) = std::fs::read_dir(dir) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out = Vec::new();\r\n    for entry in entries.flatten() {\r\n        let path = entry.path();\r\n        if let Ok(body) = std::fs::read_to_string(&path) {\r\n            if let Ok(scope) = serde_json::from_str::<InvalScope>(&body) {\r\n                out.push(scope);\r\n            }\r\n        }\r\n        let _ = std::fs::remove_file(&path);\r\n    }\r\n    out\r\n}\r\n\r\n/// The coalesced form of a drained event batch — what one reconcile consumes\r\n/// (multiple invalidations → ONE refresh, ADR-0037 §3).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\r\npub struct Coalesced {\r\n    /// Any [`InvalScope::Global`] event was present → membership refresh\r\n    /// (fingerprint-gated).\r\n    pub global: bool,\r\n    /// Endpoint ids needing a row refresh (deduped, sorted).\r\n    pub endpoints: std::collections::BTreeSet<String>,\r\n    /// Cwds whose shared derivation-cache entry must be re-derived (deduped\r\n    /// on the ORIGINAL spelling; the writer normalizes).\r\n    pub cwds: std::collections::BTreeSet<String>,\r\n}\r\n\r\nimpl Coalesced {\r\n    pub fn is_empty(&self) -> bool {\r\n        !self.global && self.endpoints.is_empty() && self.cwds.is_empty()\r\n    }\r\n}\r\n\r\n/// Union a drained batch into one refresh request — the coalescing proof\r\n/// surface (N events of any mix in, ONE `Coalesced` out).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn coalesce(events: Vec<InvalScope>) -> Coalesced {\r\n    let mut out = Coalesced::default();\r\n    for e in events {\r\n        match e {\r\n            InvalScope::Global => out.global = true,\r\n            InvalScope::Endpoint { id, cwd } => {\r\n                out.endpoints.insert(id);\r\n                if let Some(c) = cwd {\r\n                    out.cwds.insert(c);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use tempfile::tempdir;\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] the queue roundtrip: N nudges of\r\n    // every scope class land as N files, drain returns them ALL and empties the\r\n    // dir (drained = consumed), and a second drain is empty (no double-fire).\r\n    #[test]\r\n    fn nudge_drain_roundtrip_consumes_queue() {\r\n        let tmp = tempdir().unwrap();\r\n        let dir = tmp.path().join(\"invalidations\");\r\n        assert!(!pending_at(&dir), \"missing dir = nothing pending\");\r\n\r\n        nudge_at(&dir, &InvalScope::Global).unwrap();\r\n        nudge_at(\r\n            &dir,\r\n            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\r\n        )\r\n        .unwrap();\r\n        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\r\n        assert!(pending_at(&dir));\r\n\r\n        let drained = drain_at(&dir);\r\n        assert_eq!(drained.len(), 3);\r\n        assert!(!pending_at(&dir), \"drain consumes every event\");\r\n        assert!(drain_at(&dir).is_empty(), \"no double-fire\");\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] coalescing: a burst of mixed events\r\n    // unions into ONE refresh request — global sticky, endpoint ids + cwds\r\n    // deduped. This is the \"multiple invalidations coalesce into one refresh\"\r\n    // contract at the data level.\r\n    #[test]\r\n    fn burst_coalesces_into_one_refresh() {\r\n        let batch = vec![\r\n            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\r\n            InvalScope::Global,\r\n            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\r\n            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\r\n            InvalScope::Global,\r\n        ];\r\n        let one = coalesce(batch);\r\n        assert!(one.global);\r\n        assert_eq!(\r\n            one.endpoints.iter().cloned().collect::<Vec<_>>(),\r\n            vec![\"a\".to_string(), \"b\".to_string()]\r\n        );\r\n        assert_eq!(one.cwds.len(), 1);\r\n        assert!(!one.is_empty());\r\n        assert!(coalesce(Vec::new()).is_empty());\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] tolerant drain: a torn/garbage\r\n    // event file is skipped AND removed (never wedges the queue), parseable\r\n    // siblings still deliver.\r\n    #[test]\r\n    fn garbage_event_is_skipped_and_removed() {\r\n        let tmp = tempdir().unwrap();\r\n        let dir = tmp.path().join(\"invalidations\");\r\n        std::fs::create_dir_all(&dir).unwrap();\r\n        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\r\n        nudge_at(&dir, &InvalScope::Global).unwrap();\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":229,"totalBytes":9382,"outputLines":223,"outputBytes":9382,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Project-index invalidation events — the store-anchored nudge queue between\r\n//! mutation sites and the daemon's single-flight index writer (ADR-0037 §3,\r\n//! REQ-PROJECT-INDEX-INVALIDATION).\r\n//!\r\n//! Mutation seams (session append, context-store commit, rename, fork, purge)\r\n//! run in MANY processes — CLI verbs, adapter hooks, the daemon brain itself —\r\n//! and the brain deliberately does not drain its broker connection\r\n//! (brainproc's non-draining hazard), so a socket push is the wrong carrier.\r\n//! Instead every seam drops a tiny event file under\r\n//! `$SPT_HOME/index/invalidations/`; the writer polls the directory cheaply\r\n//! (one `read_dir` — pure file I/O, no git), DRAINS all pending events, and\r\n//! coalesces them into one refresh (multiple invalidations → one reconcile).\r\n//! Daemon-offline nudges simply accumulate and are consumed at the next brain\r\n//! start — the same recover-by-reading-state stance as the rest of the store.\r\n//!\r\n//! Nudging is **fire-and-forget best-effort**: an unwritable queue must never\r\n//! fail the mutation that triggered it (the periodic reconcile backstop\r\n//! catches anything a lost nudge missed).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::projindex;\r\n\r\n/// One invalidation event — the refresh scope a mutation site requests\r\n/// (ADR-0037 §3 event set).\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(tag = \"scope\", rename_all = \"kebab-case\")]\r\npub enum InvalScope {\r\n    /// A context-store mutation (any `p-*`/`a-*` commit, rename, fork): ONE\r\n    /// debounced global membership refresh — branch-tip fingerprints decide\r\n    /// what actually rescans.\r\n    Global,\r\n    /// A per-endpoint event (new session/cwd, bind/start, purge): refresh that\r\n    /// endpoint's row. `cwd`, when present, also invalidates the shared\r\n    /// cwd-derivation cache entry for that dir (a new session may sit in a dir\r\n    /// whose repo identity changed since it was last derived).\r\n    Endpoint {\r\n        id: String,\r\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n        cwd: Option<String>,\r\n    },\r\n}\r\n\r\n/// `$SPT_HOME/index/invalidations/` — the drop-dir the writer drains.\r\npub fn invalidations_dir() -> PathBuf {\r\n    projindex::index_dir().join(\"invalidations\")\r\n}\r\n\r\n/// The drop-dir under an EXPLICIT home root — the form store-layer mutation\r\n/// funnels use (they carry their own root and must not read ambient env; a\r\n/// test-rooted store then nudges into its own temp tree, never the real home).\r\npub fn invalidations_dir_under(home: &Path) -> PathBuf {\r\n    home.join(\"index\").join(\"invalidations\")\r\n}\r\n\r\n/// Process-local uniquifier so two nudges in one nanosecond don't collide.\r\nstatic NUDGE_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// Drop one event file into `dir`. Unique name = nanos + pid + a process-local\r\n/// sequence; the write is a plain create (no atomic dance needed — a torn\r\n/// event file is skipped by the tolerant drain and the backstop covers it).\r\npub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\r\n    std::fs::create_dir_all(dir)?;\r\n    let nanos = std::time::SystemTime::now()\r\n        .duration_since(std::time::UNIX_EPOCH)\r\n        .map(|d| d.as_nanos())\r\n        .unwrap_or(0);\r\n    let name = format!(\r\n        \"{nanos}-{}-{}.json\",\r\n        std::process::id(),\r\n        NUDGE_SEQ.fetch_add(1, Ordering::Relaxed)\r\n    );\r\n    let body = serde_json::to_string(scope)\r\n        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;\r\n    std::fs::write(dir.join(name), body)\r\n}\r\n\r\n/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\r\n/// the fire-and-forget form every mutation seam calls (an unwritable queue\r\n/// must never fail the mutation; the periodic reconcile is the backstop).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn nudge(scope: &InvalScope) {\r\n    let _ = nudge_at(&invalidations_dir(), scope);\r\n}\r\n\r\n/// Whether any event is pending in `dir` — the writer's cheap poll (one\r\n/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\r\npub fn pending_at(dir: &Path) -> bool {\r\n    std::fs::read_dir(dir)\r\n        .map(|mut d| d.next().is_some())\r\n        .unwrap_or(false)\r\n}\r\n\r\n/// Drain EVERY pending event out of `dir`: read + delete each file, tolerant\r\n/// of unparseable bodies (skipped + removed) and of files vanishing mid-walk\r\n/// (a concurrent drain — only the daemon writer drains in production, but the\r\n/// contract must not depend on that). The returned list is the coalescing\r\n/// input: the caller unions scopes into one refresh.\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn drain_at(dir: &Path) -> Vec<InvalScope> {\r\n    let Ok(entries) = std::fs::read_dir(dir) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out = Vec::new();\r\n    for entry in entries.flatten() {\r\n        let path = entry.path();\r\n        if let Ok(body) = std::fs::read_to_string(&path) {\r\n            if let Ok(scope) = serde_json::from_str::<InvalScope>(&body) {\r\n                out.push(scope);\r\n            }\r\n        }\r\n        let _ = std::fs::remove_file(&path);\r\n    }\r\n    out\r\n}\r\n\r\n/// The coalesced form of a drained event batch — what one reconcile consumes\r\n/// (multiple invalidations → ONE refresh, ADR-0037 §3).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\r\npub struct Coalesced {\r\n    /// Any [`InvalScope::Global`] event was present → membership refresh\r\n    /// (fingerprint-gated).\r\n    pub global: bool,\r\n    /// Endpoint ids needing a row refresh (deduped, sorted).\r\n    pub endpoints: std::collections::BTreeSet<String>,\r\n    /// Cwds whose shared derivation-cache entry must be re-derived (deduped\r\n    /// on the ORIGINAL spelling; the writer normalizes).\r\n    pub cwds: std::collections::BTreeSet<String>,\r\n}\r\n\r\nimpl Coalesced {\r\n    pub fn is_empty(&self) -> bool {\r\n        !self.global && self.endpoints.is_empty() && self.cwds.is_empty()\r\n    }\r\n}\r\n\r\n/// Union a drained batch into one refresh request — the coalescing proof\r\n/// surface (N events of any mix in, ONE `Coalesced` out).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn coalesce(events: Vec<InvalScope>) -> Coalesced {\r\n    let mut out = Coalesced::default();\r\n    for e in events {\r\n        match e {\r\n            InvalScope::Global => out.global = true,\r\n            InvalScope::Endpoint { id, cwd } => {\r\n                out.endpoints.insert(id);\r\n                if let Some(c) = cwd {\r\n                    out.cwds.insert(c);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use tempfile::tempdir;\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] the queue roundtrip: N nudges of\r\n    // every scope class land as N files, drain returns them ALL and empties the\r\n    // dir (drained = consumed), and a second drain is empty (no double-fire).\r\n    #[test]\r\n    fn nudge_drain_roundtrip_consumes_queue() {\r\n        let tmp = tempdir().unwrap();\r\n        let dir = tmp.path().join(\"invalidations\");\r\n        assert!(!pending_at(&dir), \"missing dir = nothing pending\");\r\n\r\n        nudge_at(&dir, &InvalScope::Global).unwrap();\r\n        nudge_at(\r\n            &dir,\r\n            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\r\n        )\r\n        .unwrap();\r\n        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\r\n        assert!(pending_at(&dir));\r\n\r\n        let drained = drain_at(&dir);\r\n        assert_eq!(drained.len(), 3);\r\n        assert!(!pending_at(&dir), \"drain consumes every event\");\r\n        assert!(drain_at(&dir).is_empty(), \"no double-fire\");\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] coalescing: a burst of mixed events\r\n    // unions into ONE refresh request — global sticky, endpoint ids + cwds\r\n    // deduped. This is the \"multiple invalidations coalesce into one refresh\"\r\n    // contract at the data level.\r\n    #[test]\r\n    fn burst_coalesces_into_one_refresh() {\r\n        let batch = vec![\r\n            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\r\n            InvalScope::Global,\r\n            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\r\n            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\r\n            InvalScope::Global,\r\n        ];\r\n        let one = coalesce(batch);\r\n        assert!(one.global);\r\n        assert_eq!(\r\n            one.endpoints.iter().cloned().collect::<Vec<_>>(),\r\n            vec![\"a\".to_string(), \"b\".to_string()]\r\n        );\r\n        assert_eq!(one.cwds.len(), 1);\r\n        assert!(!one.is_empty());\r\n        assert!(coalesce(Vec::new()).is_empty());\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] tolerant drain: a torn/garbage\r\n    // event file is skipped AND removed (never wedges the queue), parseable\r\n    // siblings still deliver.\r\n    #[test]\r\n    fn garbage_event_is_skipped_and_removed() {\r\n        let tmp = tempdir().unwrap();\r\n        let dir = tmp.path().join(\"invalidations\");\r\n        std::fs::create_dir_all(&dir).unwrap();\r\n        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\r\n        nudge_at(&dir, &InvalScope::Global).unwrap();\r\n\r\n…\n    }\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,null,227,228]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\projinval.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":229,"totalBytes":9382,"outputLines":223,"outputBytes":9382,"shownRange":{"start":1,"end":223},"nextOffset":224}}},"isError":false,"timestamp":1784271486391}}
{"type":"message","id":"b4a20a13","parentId":"b53e3656","timestamp":"2026-07-17T06:58:15.534Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying exact lifecycle sections**"},{"type":"toolCall","id":"call_yO4rphDc4BMNY3yxn0ZA2EK3|fc_038f28d3e93bbfd4016a59d28772248196abf0babfd40477e5","name":"read","arguments":{"i":"Reading pulse tick scans","path":"crates/spt-daemon/src/lifecycle.rs","selector":"460-740"}},{"type":"toolCall","id":"call_4cTWWurnuzwys1XLYXsCAHfV|fc_038f28d3e93bbfd4016a59d28772308196a804527b6caaf968","name":"read","arguments":{"i":"Reading history fetch path","path":"crates/spt-daemon/src/lifecycle.rs","selector":"740-885,930-1160"}},{"type":"toolCall","id":"call_JK2M1qUTMs8tFNp5yxXDvaab|fc_038f28d3e93bbfd4016a59d287723881969bd1bc6cbe525bd4","name":"grep","arguments":{"i":"Tracing ingest drops implementation","pattern":"pub fn ingest_drops|fn ingest_drops|read_dir|read_to_string|File::open","path":"crates/spt-live/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_BPYnns2KYIvMnga565MhTnlo|fc_038f28d3e93bbfd4016a59d287723c81969c7bfd9a0811547b","name":"grep","arguments":{"i":"Tracing history fetch implementation","pattern":"pub fn fetch_history|fn fetch_history|read_to_string|std::fs::read|File::open|BufReader","path":"crates/spt-live/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_RDBZlnIL6p1he5tXM5iOlJ5f|fc_038f28d3e93bbfd4016a59d28772448196bfcace825584ebec","name":"read","arguments":{"i":"Reading sessions ledger","path":"crates/spt-store/src/sessions.rs","selector":"1-180"}},{"type":"toolCall","id":"call_rHooVlLqExWYJ9khhkuMvOIr|fc_038f28d3e93bbfd4016a59d28772488196b66b16caecc902ae","name":"read","arguments":{"i":"Reading pump cadence and work","path":"crates/spt-daemon/src/pump/mod.rs","selector":"1-170,520-750"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":26524,"output":318,"cacheRead":92672,"cacheWrite":0,"totalTokens":119514,"reasoningTokens":9,"cost":{"input":0.13262000000000002,"output":0.00954,"cacheRead":0.046335999999999995,"cacheWrite":0,"total":0.188496}},"stopReason":"toolUse","timestamp":1784271486437,"responseId":"resp_038f28d3e93bbfd4016a59d27ecfb88196834b648798c1db47","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d281b0048196a09729be816af290","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdKHikBlesbkALQjDm_DLGdL7Q2f77llh6K25aFgMFMmV02Dbs30U0lGM0GG-6xjsnaTl1vJQGZ59XIptuUCKFMyBWyrhn9Ecxb1tLva2_TaBNmqSoYkOLSjXWjGfe2RyK6pM_V9N4LnlPUv0yS734ZnjCXyTl6hP-sLlW3OJ4FbWi6qnZofHQi_5Yt76Wcn0wWhp_YPB37c9xF3h1vnRdzSvzehsbgeivcULZQzu8TAyy6In8VVk2eBZ70qdWFzC5JZJfT81bdxhE8NCkGzPSDuRyhgDqFDJufrL4zOGCdUEaw7d6zIPb5vQ4ZpREifUE83R3axNkIELcfQki-qIrLSeuQrZ8hY_w9IRPpv_aatalIErP5NbJrIFOTRC-qyRCisn4G6LZr_2N5mO0sy0g7doOHwpjjJKxXSC62m2pyK562M1K_Sll5V3r_uh0f50mtTOPGJLvopiLEEGJfOM8e9xsEEFYXDXLQDfBzMZN9seGlXXxDACG1Pe4kmMtIkWTVtm6uMQvnzbKPWa6NmANtA7_zFXQSimfsnPXY3UqW0SfOQQUbs1ISVKjTSZTsB3as8RG8XeyRSB2luBVeccK0KrJwyjG1sSxtWhOCzUFwkOxbrBktwiHTr4pPcQ__qdgTmp-o_VYM1_yVOOE9dMESrILTG5zaNomjvhih1sIO0gMhlQbEGqCRurN3PdnM-zwr28EVUM6_Ybev00GR8qjtbRfYy9ohZHELrQiMLwS-68OSmBeSHqNfhUK-LoWpfIDLi21Kf0_EWWGV9wYLDBkl1pAgpEFYmBbFDDOgKso1iQWFSIKNvR6EOpsZatnzJvrFuwotylWJ8_fXyaoAca_YTlew7hVqyqVeCAPUtU8Yg1uzns7baRpnEj-NLuzOe_2INDKnQHBpH-tIi0l2WLBXRnt9E8NloC5K97f88fN4Scm5euctS5Gd9frtI5WjHlQ3u9FHg9Tr2wr1dCyCSgSIvvQ==","summary":[{"type":"summary_text","text":"**Identifying exact lifecycle sections**"}]},{"id":"fc_038f28d3e93bbfd4016a59d28772248196abf0babfd40477e5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading pulse tick scans\",\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"selector\":\"460-740\"}","call_id":"call_yO4rphDc4BMNY3yxn0ZA2EK3","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d28772308196a804527b6caaf968","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading history fetch path\",\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"selector\":\"740-885,930-1160\"}","call_id":"call_4cTWWurnuzwys1XLYXsCAHfV","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d287723881969bd1bc6cbe525bd4","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing ingest drops implementation\",\"pattern\":\"pub fn ingest_drops|fn ingest_drops|read_dir|read_to_string|File::open\",\"path\":\"crates/spt-live/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_JK2M1qUTMs8tFNp5yxXDvaab","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d287723c81969c7bfd9a0811547b","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing history fetch implementation\",\"pattern\":\"pub fn fetch_history|fn fetch_history|read_to_string|std::fs::read|File::open|BufReader\",\"path\":\"crates/spt-live/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_BPYnns2KYIvMnga565MhTnlo","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d28772448196bfcace825584ebec","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading sessions ledger\",\"path\":\"crates/spt-store/src/sessions.rs\",\"selector\":\"1-180\"}","call_id":"call_RDBZlnIL6p1he5tXM5iOlJ5f","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d28772488196b66b16caecc902ae","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading pump cadence and work\",\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"selector\":\"1-170,520-750\"}","call_id":"call_rHooVlLqExWYJ9khhkuMvOIr","name":"read"}]},"duration":9069.528699994087,"ttft":3268.4211000055075,"contextSnapshot":{"promptTokens":119196,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yO4rphDc4BMNY3yxn0ZA2EK3|fc_038f28d3e93bbfd4016a59d28772248196abf0babfd40477e5","toolName":"read","startedAt":"2026-07-17T06:58:15.534Z","args":{"path":"crates/spt-daemon/src/lifecycle.rs"},"intent":"Reading pulse tick scans"},"id":"1564604c","parentId":"b4a20a13","timestamp":"2026-07-17T06:58:15.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4cTWWurnuzwys1XLYXsCAHfV|fc_038f28d3e93bbfd4016a59d28772308196a804527b6caaf968","toolName":"read","startedAt":"2026-07-17T06:58:15.534Z","args":{"path":"crates/spt-daemon/src/lifecycle.rs"},"intent":"Reading history fetch path"},"id":"d56681b8","parentId":"1564604c","timestamp":"2026-07-17T06:58:15.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JK2M1qUTMs8tFNp5yxXDvaab|fc_038f28d3e93bbfd4016a59d287723881969bd1bc6cbe525bd4","toolName":"grep","startedAt":"2026-07-17T06:58:15.534Z","args":{"path":"crates/spt-live/src"},"intent":"Tracing ingest drops implementation"},"id":"1987d7dc","parentId":"d56681b8","timestamp":"2026-07-17T06:58:15.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BPYnns2KYIvMnga565MhTnlo|fc_038f28d3e93bbfd4016a59d287723c81969c7bfd9a0811547b","toolName":"grep","startedAt":"2026-07-17T06:58:15.534Z","args":{"path":"crates/spt-live/src"},"intent":"Tracing history fetch implementation"},"id":"e88e9d62","parentId":"1987d7dc","timestamp":"2026-07-17T06:58:15.534Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RDBZlnIL6p1he5tXM5iOlJ5f|fc_038f28d3e93bbfd4016a59d28772448196bfcace825584ebec","toolName":"read","startedAt":"2026-07-17T06:58:15.535Z","args":{"path":"crates/spt-store/src/sessions.rs"},"intent":"Reading sessions ledger"},"id":"932eec2c","parentId":"e88e9d62","timestamp":"2026-07-17T06:58:15.535Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rHooVlLqExWYJ9khhkuMvOIr|fc_038f28d3e93bbfd4016a59d28772488196b66b16caecc902ae","toolName":"read","startedAt":"2026-07-17T06:58:15.535Z","args":{"path":"crates/spt-daemon/src/pump/mod.rs"},"intent":"Reading pump cadence and work"},"id":"da89708c","parentId":"932eec2c","timestamp":"2026-07-17T06:58:15.535Z"}
{"type":"message","id":"4aadcabf","parentId":"da89708c","timestamp":"2026-07-17T06:58:15.553Z","message":{"role":"toolResult","toolCallId":"call_rHooVlLqExWYJ9khhkuMvOIr|fc_038f28d3e93bbfd4016a59d28772488196b66b16caecc902ae","toolName":"read","content":[{"type":"text","text":"//! Outbound peer pump (D9-1-2) — the production trigger loops every D5–D8\n//! slice deferred (\"rides D9 with the daemon lifecycle\").\n//!\n//! One daemon-hosted loop — *the* peer pump — is a pure scheduler + per-peer\n//! fan-out over four [`PumpWorker`] legs. The pump owns *when* (each worker's\n//! cadence + its wake markers, fed to the pure [`due`] kernel) and *toward\n//! whom* (every roster member of every non-detached member subnet); each worker\n//! owns *what* (its node-local `pre_round` and its per-peer `peer_step`), a thin\n//! call over an already-shipped, idempotent driver. The four legs, in fan-out\n//! order, each in its own module:\n//!\n//! - **registry push** ([`registry`], [`RegistryHost::advertise_local`] → one\n//!   feed stream per peer): the node re-advertises its own rows; peers\n//!   reconverge within one cadence (replay-is-recovery, the D4d/D8 push\n//!   posture). Mesh-D6 widened the push target from the directly-paired set to\n//!   ALL roster members — a wider DIRECT fan-out, never a relay (REQ-MESH-3).\n//!   Own rows only — a relayed third-party row would carry an origin the\n//!   receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10), so every row still\n//!   arrives from its author over a handshake.\n//! - **notif push** ([`notif`], [`crate::notifsync::emit_notif_feed`]): the\n//!   full semilattice spool per subnet; the receiver's join dedups.\n//! - **sync pull** ([`sync`], [`crate::sync::request_sync`]): anti-entropy for\n//!   the minds. Want-refs are the union of [`crate::sync::select_refs`]\n//!   (locally tracked branches the policy allows toward the peer) and\n//!   **registry-derived** `a-<id>` refs (endpoints known in the shared\n//!   subnets — how a fresh instance bootstraps a mind it has never held), both\n//!   gated by the same [`SyncPolicy::allows`] the responder enforces (symmetric\n//!   scoping).\n//! - **update pull** ([`update`], [`crate::propagate::request_update`]): the\n//!   peer-propagated self-update (REQ-UPD-1), consent-gated by default\n//!   (REQ-UPD-4), with typed convergence outcomes (REQ-UPD-6).\n//!\n//! The scheduler kernels are pure and unit-tested with scripted workers (the\n//! choreography table below): [`compute_due`] (prime-all / wake-forces-one),\n//! [`pre_rounds`] (once per due round, before any peer_step), [`run_due`]\n//! (abort-on-first-fail), [`mark_ran`] (the §V4 stagger), [`fan_targets`]\n//! (detached-skip). LLM-bearing work is never a [`PumpWorker`] (KNOWN-HAZARDS\n//! 7.4) — see the trait's contract.\n//!\n//! Conflicts a pull surfaces are reported through [`PumpHooks::on_pull`] —\n//! the default production hook logs them loud. The bounded Psyche reconcile\n//! turn itself still runs at the activation paths (ADR-0013): the daemon\n//! does not yet host per-agent manifest runtimes (the 7.4 fan-out), so an\n//! in-pump turn has no runtime to ride; conflict artifacts persist safely\n//! meanwhile (REQ-HAZARD-CONFLICT-BOTH-PRESERVED).\n//!\n//! ## Wire discipline (inherited from the test suites, now production)\n//!\n//! Every pump write is fire-and-forget (`op_id: None`) — a journaled\n//! `net_stream_send` blocks on its ack and DISCARDS interleaved events\n//! (gotcha #1). Stream opens for push feeds carry no op either: the feeds\n//! are idempotent by construction. The sync pull's `open_op` IS durable\n//! (exactly-once open identity), minted from a dedicated [`EpochSource`]\n//! file so a daemon restart can never reuse one.\n//!\n//! Per-leg wire-discipline evidence lives with each worker now: registry push\n//! ([`registry`], REQ-INST-7), notif push ([`notif`], REQ-NOTIF-1), sync pull\n//! ([`sync`], REQ-INST-5).\n\nuse std::collections::{HashMap, HashSet};\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::endpoint::addr_for_node_hex;\nuse spt_store::epoch::EpochSource;\nuse spt_store::peeraddrs::PeerAddrStore;\nuse spt_store::perch;\nuse spt_store::subnet::SubnetStore;\nuse spt_store::visibility::VisibilityStore;\n\nuse crate::brain::{Brain, BrokerEvent, PEER_REPLY_READ_BUDGET};\nuse crate::config::DaemonConfig;\nuse crate::effect::{Minter, MintedOp};\nuse crate::msg::{\n    NetPresenceEvent, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,\n};\nuse crate::registryhost::RegistryHost;\nuse crate::sync::{SyncPolicy, SyncPullReport};\n\npub mod health;\nuse health::PumpHealth;\n\nmod notif;\nmod registry;\nmod sync;\nmod update;\nuse notif::NotifWorker;\nuse registry::RegistryWorker;\nuse sync::SyncWorker;\nuse update::UpdateWorker;\n\n/// The pump's tick granularity — each cadence fires when its period elapsed.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall\n/// warning reads 3-cadence staleness, so a few seconds of heartbeat\n/// granularity is invisible to it — and 5 writes/sec of disk churn is not.\nconst HEARTBEAT_PERIOD: Duration = Duration::from_secs(5);\n\n/// Per-call deadline budget for the pump's brain-IPC reads (REQ-HAZARD-PUMP-\n/// IPC-DEADLINE). Far above any legitimate stream-open / sync round-trip, below\n/// the 60s QUIC idle timeout (we fire first; recovery is idempotent either\n/// way). A read that exceeds this is treated as a poisoned client and\n/// escalated to a supervised restart, NEVER a per-peer retry.\nconst PUMP_PEER_IO_TIMEOUT: Duration = Duration::from_secs(30);\n\n/// Supervisor backoff floor: the first restart after a pump death.\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(5);\n\n/// Supervisor backoff cap (M8 decision 23): a persistently-crashing pump\n/// retries at most every 5 minutes — visible, bounded, never silent.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(300);\n\n/// A run that survived this long before dying counts as healthy — the next\n/// backoff resets to the floor instead of doubling (a once-a-day panic must\n/// not ratchet the pump toward permanent 5-minute outages).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(60);\n\n/// Per-peer dial-backoff floor (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): the\n/// first failed dial to a peer waits this long before the pump re-dials THAT\n/// peer. Scoped to the peer — a dead peer backs off while live peers stay hot.\nconst PEER_BACKOFF_BASE: Duration = Duration::from_secs(5);\n\n/// Per-peer dial-backoff cap (G5): a persistently-dead peer is re-dialed at most\n/// every 5 minutes (same bound as the supervisor) — bounded, never a hot redial\n/// loop, never silent. Reset to the floor on any successful connect.\nconst PEER_BACKOFF_CAP: Duration = Duration::from_secs(300);\n\n/// Round-drain margin over W1's per-dial reply-read budget (G3, Q1): because the\n/// dials are CONCURRENT, every outcome lands within ~one W1 bound, so the round\n/// closes shortly after it. Derived symbolically from\n/// [`PEER_REPLY_READ_BUDGET`] (not a hardcoded 12s) so it tracks any W1 retune.\nconst ROUND_DRAIN_MARGIN: Duration = Duration::from_secs(2);\n\n/// Per-peer dial schedule (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): a peer is\n/// dial-eligible when `now >= next_due`. A `DIAL_FAILED` (or a mid-round\n/// `peer_step` failure — Q3) doubles `backoff` (capped [`PEER_BACKOFF_CAP`]) and\n/// pushes `next_due` out; a successful connect drops the entry (reset). Kept in a\n/// `HashMap<peer_hex, PeerSched>` on the pump loop — RAM-only, re-primed on a\n/// supervised restart like `WorkerLasts`.\n#[derive(Clone, Copy, Debug)]\nstruct PeerSched {\n    next_due: Instant,\n    backoff: Duration,\n}\n\n/// The next per-peer backoff after a failed dial (pure, unit-tested): the first\n/// failure waits [`PEER_BACKOFF_BASE`]; each subsequent failure doubles, capped\n/// at [`PEER_BACKOFF_CAP`]. `next_due = now + backoff`.\n// [impl->REQ-PUMP-PEER-ISOLATION]\nfn next_peer_backoff(prev: Option<PeerSched>, now: Instant) -> PeerSched {\n    let backoff = match prev {\n        None => PEER_BACKOFF_BASE,\n        Some(p) => p.backoff.saturating_mul(2).min(PEER_BACKOFF_CAP),\n    };\n    PeerSched {\n        next_due: now + backoff,\n        backoff,\n    }\n}\n\n/// Whether a peer may be dialed this round (pure): a peer with no schedule entry\n/// (never failed, or just reset by a success) is always eligible; one in backoff\n/// is eligible only once `next_due` has passed.\n// [impl->REQ-PUMP-PEER-ISOLATION]\nfn peer_eligible(sched: &HashMap<String, PeerSched>, peer_hex: &str, now: Instant) -> bool {\n…\n}\n…\npub fn run_peer_pump(\n    broker_name: &str,\n    registry: Arc<RegistryHost>,\n    paths: &PumpPaths,\n    resolver: PeerResolver,\n    config: PumpConfig,\n    hooks: PumpHooks,\n    stop: &AtomicBool,\n) -> io::Result<()> {\n    let PumpConfig {\n        cadence,\n        full_auto_update,\n    } = config;\n    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\n    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n    // hfenduleam stall). A timed-out read escalates to a supervised restart\n    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    let mut brain = connect(broker_name)?;\n    // The DEDICATED event-drain carrier (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION):\n    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\n    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\n    // whose reply-reads consume every event kind off their carrier; sharing one\n    // carrier would make a pull-read swallow a presence frame (advancing the\n    // exactly-once cursor while dropping the outcome — the D4c break). Two\n    // carriers keep the drain and the pulls from swallowing each other by\n    // construction. Tail-follow (no history replay, baseline on first live event)\n    // so a long-lived broker's evicted ring can never gap-trip a false restart.\n    let mut events = connect(broker_name)?;\n    events.net_presence_subscribe_tail()?;\n    // Startup reconcile + invariant migration (ADR-0039 Decisions 3+4,\n    // REQ-PEER-ROUTE-CHAIN / REQ-PEERADDR-INVARIANT): validated roster\n    // addresses reconcile into the dial cache BEFORE the first round —\n    // recovery is connection-independent (a node restarted mid-strand heals\n    // here, no operator surgery) — and rows violating `outer key ==\n    // address.id` are repaired from the roster or dropped loudly (the first\n    // post-upgrade load is the migration; the file is never bare-deleted —\n    // valid rows and the id-only fallback behind them stay untouched).\n    // Idempotent, so re-running on every supervised pump restart is free.\n    startup_reconcile_peeraddrs(paths, registry.node_hex());\n    // Reachability health (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): loaded\n    // so the last-good stamps survive a restart; the live counts re-prime as\n    // rounds run. Persisted beside the heartbeat; the status CLI reads it.\n    let health_path = PumpHealth::path_beside(&paths.heartbeat);\n    let mut health = PumpHealth::load_from(&health_path);\n    health.note_disconnected(0); // no conns exist yet in this pump generation\n    let mut ops = EpochSource::load_from(&paths.ops);\n    // The live per-peer connection cache (updated by presence events) + the\n    // per-peer dial schedule (G5 backoff). Both RAM-only, re-primed on restart.\n    let mut conns: HashMap<String, u64> = HashMap::new();\n    let mut sched: HashMap<String, PeerSched> = HashMap::new();\n    // The four cadenced legs behind the worker seam, in fan-out order\n    // (registry → notif → sync → update). The shell drives them generically;\n    // each leg's *what* lives in its module (§Frozen). Construction capture\n    // (§Frozen-4): each worker owns the registry Arc / path slices / config\n    // knob it needs — the sync worker additionally owns the conflict-surface\n    // hooks (ADR-0013).\n    let mut registry_worker = RegistryWorker::new(Arc::clone(&registry), paths, &cadence);\n    let mut notif_worker = NotifWorker::new(paths, &cadence);\n    let mut sync_worker = SyncWorker::new(Arc::clone(&registry), paths, &cadence, hooks);\n    let mut update_worker = UpdateWorker::new(Arc::clone(&registry), paths, &cadence, full_auto_update);\n    let mut workers: [&mut dyn PumpWorker; 4] = [\n        &mut registry_worker,\n        &mut notif_worker,\n        &mut sync_worker,\n        &mut update_worker,\n    ];\n    // WorkerLasts (ADR-0018 §V4): RAM-only last-ran instants, parallel to\n    // `workers`. `None` = never run yet → due immediately, so the first tick\n    // primes every leg. Never `Instant - Duration` — the underflow panic on a\n    // sub-uptime monotonic clock (KH 5.9) lives nowhere in this path.\n    let mut last: [Option<Instant>; 4] = [None; 4];\n    let mut last_hb: Option<Instant> = None;\n\n    while !stop.load(Ordering::Relaxed) {\n        let now = Instant::now();\n        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\n        // while the loop is ALIVE — including idle ticks — never per work round,\n        // so a wedged step and a healthy quiet pump are distinguishable.\n        // Throttled; per-tick, never a worker (REQ-DAEMON-5), no wake.\n        // [impl->REQ-DAEMON-5]\n        if due(last_hb, false, now, HEARTBEAT_PERIOD) {\n            write_heartbeat(&paths.heartbeat);\n            last_hb = Some(now);\n        }\n        // Poll every worker's wake and decide due (the pure scheduler step):\n        // every wake is consumed EVERY tick so a marker is never stranded (the\n        // registry advertise-now, REQ-CONV-2; the sync freshness pull,\n        // REQ-INST-3). The take IS the remove — one wake claims exactly one\n        // forced round.\n        let (due_flags, any_due) = compute_due(&mut workers, &last, now);\n        if !any_due {\n            thread::sleep(TICK);\n            continue;\n        }\n\n        // Per-round shared loads (§Frozen-3): subnets / attachment /\n        // roster-backed policy, read ONCE and handed to every worker.\n        let ctx = RoundCtx::load(paths);\n\n        // Each due worker's node-local pre_round, once per round before ANY\n        // peer_step (only the registry leg does work here — its sweep +\n        // advertise; the rest default to a no-op).\n        pre_rounds(&mut workers, &due_flags, &ctx);\n\n        // ── PUMP-TRUTH W2 round (REQ-PUMP-PEER-ISOLATION) — concurrent dials +\n        // per-peer fault isolation. Group this round's fan-out targets by peer (a\n        // peer may target several subnets; one conn serves them all).\n        let mut peer_subnets: HashMap<String, Vec<String>> = HashMap::new();\n        for (sub_name, peer_hex) in fan_targets(&ctx, registry.node_hex()) {\n            peer_subnets.entry(peer_hex).or_default().push(sub_name);\n        }\n        // Health targets = this round's fan-out set; snapshot the registry\n        // host's last-admitted stamp while here (REQ-PUMP-STAGE-TRUTH).\n        health.set_targets(peer_subnets.keys().cloned(), now_ms());\n        health.note_registry_admit(registry.last_admit_ms());\n        let round_start = Instant::now();\n        let mut pending: HashSet<String> = HashSet::new();\n\n        // Submit NON-BLOCKING dials for eligible unconnected peers; run the due\n        // legs NOW for already-connected ones. The live peer advertises without\n        // waiting on a dead one (no head-of-line block) — a dead peer is only a\n        // pending outcome, never a serial blocker.\n        for (peer_hex, subs) in &peer_subnets {\n            if let Some(&conn_id) = conns.get(peer_hex) {\n                run_peer_subnets(\n                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n                    conn_id, subs, peer_hex, &ctx, round_start,\n                )?;\n            } else if peer_eligible(&sched, peer_hex, round_start) {\n                if let Some(addr) =\n                    resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs, &ctx.policy.roster)\n                {\n                    match brain.submit_dial(addr) {\n                        Ok(()) => {\n                            pending.insert(peer_hex.clone());\n                        }\n                        // A raw carrier-op TimedOut on the submit IPC itself is a\n                        // genuine broker-IPC desync → bubble → supervised restart.\n                        Err(e) if e.kind() == io::ErrorKind::TimedOut => return Err(e),\n                        // The broker refused the submit (net disabled / bad addr):\n                        // an ordinary per-peer failure — back off, round continues.\n                        Err(e) => {\n                            eprintln!(\n                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\n                                now_ms(),\n                                mono_ms()\n                            );\n                            health.note_failed(peer_hex, \"submit\", conns.len(), now_ms());\n                            let prev = sched.get(peer_hex).copied();\n                            sched.insert(peer_hex.clone(), next_peer_backoff(prev, round_start));\n                        }\n                    }\n                } else {\n                    // No route on any chain leg (REQ-PUMP-STAGE-TRUTH stage\n                    // `address-resolution`). Transition-logged: the first\n                    // unroutable round per peer is loud, repeats are quiet —\n                    // the peer retries on its own cadence like before.\n                    if health.note_failed(\n                        peer_hex,\n                        crate::seedproofx::STAGE_RESOLVE,\n                        conns.len(),\n                        now_ms(),\n                    ) {\n                        eprintln!(\n                            \"PUMP_PEER_FAIL:{peer_hex}:stage={}: no route (cache/roster/discovery all empty) wall_ms={} mono_ms={}\",\n                            crate::seedproofx::STAGE_RESOLVE,\n                            now_ms(),\n                            mono_ms()\n                        );\n                    }\n                }\n            }\n        }\n\n        // Drain dial outcomes on the DEDICATED event carrier until the round\n        // deadline (concurrent dials all land within ~one W1 bound). A CONNECTED\n        // runs that peer's legs in THIS round; a DIAL_FAILED backs the peer off; a\n        // peer unresolved at the deadline simply reschedules — NEVER a whole-round\n        // poison. `TimedOut` = quiet (drain done); any other read error = the\n        // broker stopped answering the stream (dead carrier) → bubble → supervised\n        // restart (the re-pointed REQ-HAZARD-PUMP-IPC-DEADLINE: a dead BROKER still\n        // restarts the pump; a dead PEER never does).\n        // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n        let round_deadline = round_start + PEER_REPLY_READ_BUDGET + ROUND_DRAIN_MARGIN;\n        loop {\n            let read_deadline = if pending.is_empty() {\n                Instant::now() // no outstanding submits — sweep only what is queued\n            } else {\n                round_deadline\n            };\n            match classify_drain_read(events.read_event_until(Some(read_deadline))) {\n                DrainStep::Apply(ev) => handle_presence_event(\n                    ev, &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n                    &mut pending, &peer_subnets, &ctx, &paths.peer_addrs, &mut health, round_start,\n                )?,\n                DrainStep::Skip => continue, // the event carrier only carries presence\n                DrainStep::RoundDone => break, // quiet (TimedOut) — drain done\n                DrainStep::Restart(e) => return Err(e), // dead broker/carrier → restart\n            }\n        }\n\n        // Persist the round's reachability truth for the status surfaces\n        // (REQ-PUMP-STAGE-TRUTH: the acceptance surface of this contract).\n        health.save_to(&health_path);\n\n        // Mark-after-round with a POST-round instant (ADR-0018 §V4: the\n        // stagger — ran = ATTEMPTED, not succeeded; the next due is measured\n        // from here, not from the round's start).\n        mark_ran(&mut last, &due_flags, Instant::now());\n    }\n    Ok(())\n}\n\n/// Compute each worker's due flag for this tick (the pure scheduler step):\n/// poll EVERY worker's wake first — even a not-otherwise-due leg, so a marker\n/// is never stranded — then the [`due`] kernel over its RAM-only last-ran\n/// instant. Returns the per-worker flags (parallel to `workers`) and whether\n/// ANY leg is due (an all-idle tick sleeps without loading a round).\nfn compute_due(\n    workers: &mut [&mut dyn PumpWorker],\n    last: &[Option<Instant>],\n    now: Instant,\n) -> (Vec<bool>, bool) {\n    let mut flags = vec![false; workers.len()];\n    let mut any = false;\n    for i in 0..workers.len() {\n        let wake = workers[i].poll_wake();\n        flags[i] = due(last[i], wake, now, workers[i].cadence());\n        any |= flags[i];\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! Outbound peer pump (D9-1-2) — the production trigger loops every D5–D8\n//! slice deferred (\"rides D9 with the daemon lifecycle\").\n//!\n//! One daemon-hosted loop — *the* peer pump — is a pure scheduler + per-peer\n//! fan-out over four [`PumpWorker`] legs. The pump owns *when* (each worker's\n//! cadence + its wake markers, fed to the pure [`due`] kernel) and *toward\n//! whom* (every roster member of every non-detached member subnet); each worker\n//! owns *what* (its node-local `pre_round` and its per-peer `peer_step`), a thin\n//! call over an already-shipped, idempotent driver. The four legs, in fan-out\n//! order, each in its own module:\n//!\n//! - **registry push** ([`registry`], [`RegistryHost::advertise_local`] → one\n//!   feed stream per peer): the node re-advertises its own rows; peers\n//!   reconverge within one cadence (replay-is-recovery, the D4d/D8 push\n//!   posture). Mesh-D6 widened the push target from the directly-paired set to\n//!   ALL roster members — a wider DIRECT fan-out, never a relay (REQ-MESH-3).\n//!   Own rows only — a relayed third-party row would carry an origin the\n//!   receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10), so every row still\n//!   arrives from its author over a handshake.\n//! - **notif push** ([`notif`], [`crate::notifsync::emit_notif_feed`]): the\n//!   full semilattice spool per subnet; the receiver's join dedups.\n//! - **sync pull** ([`sync`], [`crate::sync::request_sync`]): anti-entropy for\n//!   the minds. Want-refs are the union of [`crate::sync::select_refs`]\n//!   (locally tracked branches the policy allows toward the peer) and\n//!   **registry-derived** `a-<id>` refs (endpoints known in the shared\n//!   subnets — how a fresh instance bootstraps a mind it has never held), both\n//!   gated by the same [`SyncPolicy::allows`] the responder enforces (symmetric\n//!   scoping).\n//! - **update pull** ([`update`], [`crate::propagate::request_update`]): the\n//!   peer-propagated self-update (REQ-UPD-1), consent-gated by default\n//!   (REQ-UPD-4), with typed convergence outcomes (REQ-UPD-6).\n//!\n//! The scheduler kernels are pure and unit-tested with scripted workers (the\n//! choreography table below): [`compute_due`] (prime-all / wake-forces-one),\n//! [`pre_rounds`] (once per due round, before any peer_step), [`run_due`]\n//! (abort-on-first-fail), [`mark_ran`] (the §V4 stagger), [`fan_targets`]\n//! (detached-skip). LLM-bearing work is never a [`PumpWorker`] (KNOWN-HAZARDS\n//! 7.4) — see the trait's contract.\n//!\n//! Conflicts a pull surfaces are reported through [`PumpHooks::on_pull`] —\n//! the default production hook logs them loud. The bounded Psyche reconcile\n//! turn itself still runs at the activation paths (ADR-0013): the daemon\n//! does not yet host per-agent manifest runtimes (the 7.4 fan-out), so an\n//! in-pump turn has no runtime to ride; conflict artifacts persist safely\n//! meanwhile (REQ-HAZARD-CONFLICT-BOTH-PRESERVED).\n//!\n//! ## Wire discipline (inherited from the test suites, now production)\n//!\n//! Every pump write is fire-and-forget (`op_id: None`) — a journaled\n//! `net_stream_send` blocks on its ack and DISCARDS interleaved events\n//! (gotcha #1). Stream opens for push feeds carry no op either: the feeds\n//! are idempotent by construction. The sync pull's `open_op` IS durable\n//! (exactly-once open identity), minted from a dedicated [`EpochSource`]\n//! file so a daemon restart can never reuse one.\n//!\n//! Per-leg wire-discipline evidence lives with each worker now: registry push\n//! ([`registry`], REQ-INST-7), notif push ([`notif`], REQ-NOTIF-1), sync pull\n//! ([`sync`], REQ-INST-5).\n\nuse std::collections::{HashMap, HashSet};\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::endpoint::addr_for_node_hex;\nuse spt_store::epoch::EpochSource;\nuse spt_store::peeraddrs::PeerAddrStore;\nuse spt_store::perch;\nuse spt_store::subnet::SubnetStore;\nuse spt_store::visibility::VisibilityStore;\n\nuse crate::brain::{Brain, BrokerEvent, PEER_REPLY_READ_BUDGET};\nuse crate::config::DaemonConfig;\nuse crate::effect::{Minter, MintedOp};\nuse crate::msg::{\n    NetPresenceEvent, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,\n};\nuse crate::registryhost::RegistryHost;\nuse crate::sync::{SyncPolicy, SyncPullReport};\n\npub mod health;\nuse health::PumpHealth;\n\nmod notif;\nmod registry;\nmod sync;\nmod update;\nuse notif::NotifWorker;\nuse registry::RegistryWorker;\nuse sync::SyncWorker;\nuse update::UpdateWorker;\n\n/// The pump's tick granularity — each cadence fires when its period elapsed.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall\n/// warning reads 3-cadence staleness, so a few seconds of heartbeat\n/// granularity is invisible to it — and 5 writes/sec of disk churn is not.\nconst HEARTBEAT_PERIOD: Duration = Duration::from_secs(5);\n\n/// Per-call deadline budget for the pump's brain-IPC reads (REQ-HAZARD-PUMP-\n/// IPC-DEADLINE). Far above any legitimate stream-open / sync round-trip, below\n/// the 60s QUIC idle timeout (we fire first; recovery is idempotent either\n/// way). A read that exceeds this is treated as a poisoned client and\n/// escalated to a supervised restart, NEVER a per-peer retry.\nconst PUMP_PEER_IO_TIMEOUT: Duration = Duration::from_secs(30);\n\n/// Supervisor backoff floor: the first restart after a pump death.\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(5);\n\n/// Supervisor backoff cap (M8 decision 23): a persistently-crashing pump\n/// retries at most every 5 minutes — visible, bounded, never silent.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(300);\n\n/// A run that survived this long before dying counts as healthy — the next\n/// backoff resets to the floor instead of doubling (a once-a-day panic must\n/// not ratchet the pump toward permanent 5-minute outages).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(60);\n\n/// Per-peer dial-backoff floor (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): the\n/// first failed dial to a peer waits this long before the pump re-dials THAT\n/// peer. Scoped to the peer — a dead peer backs off while live peers stay hot.\nconst PEER_BACKOFF_BASE: Duration = Duration::from_secs(5);\n\n/// Per-peer dial-backoff cap (G5): a persistently-dead peer is re-dialed at most\n/// every 5 minutes (same bound as the supervisor) — bounded, never a hot redial\n/// loop, never silent. Reset to the floor on any successful connect.\nconst PEER_BACKOFF_CAP: Duration = Duration::from_secs(300);\n\n/// Round-drain margin over W1's per-dial reply-read budget (G3, Q1): because the\n/// dials are CONCURRENT, every outcome lands within ~one W1 bound, so the round\n/// closes shortly after it. Derived symbolically from\n/// [`PEER_REPLY_READ_BUDGET`] (not a hardcoded 12s) so it tracks any W1 retune.\nconst ROUND_DRAIN_MARGIN: Duration = Duration::from_secs(2);\n\n/// Per-peer dial schedule (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): a peer is\n/// dial-eligible when `now >= next_due`. A `DIAL_FAILED` (or a mid-round\n/// `peer_step` failure — Q3) doubles `backoff` (capped [`PEER_BACKOFF_CAP`]) and\n/// pushes `next_due` out; a successful connect drops the entry (reset). Kept in a\n/// `HashMap<peer_hex, PeerSched>` on the pump loop — RAM-only, re-primed on a\n/// supervised restart like `WorkerLasts`.\n#[derive(Clone, Copy, Debug)]\nstruct PeerSched {\n    next_due: Instant,\n    backoff: Duration,\n}\n\n/// The next per-peer backoff after a failed dial (pure, unit-tested): the first\n/// failure waits [`PEER_BACKOFF_BASE`]; each subsequent failure doubles, capped\n/// at [`PEER_BACKOFF_CAP`]. `next_due = now + backoff`.\n// [impl->REQ-PUMP-PEER-ISOLATION]\nfn next_peer_backoff(prev: Option<PeerSched>, now: Instant) -> PeerSched {\n    let backoff = match prev {\n        None => PEER_BACKOFF_BASE,\n        Some(p) => p.backoff.saturating_mul(2).min(PEER_BACKOFF_CAP),\n    };\n    PeerSched {\n        next_due: now + backoff,\n        backoff,\n    }\n}\n\n/// Whether a peer may be dialed this round (pure): a peer with no schedule entry\n/// (never failed, or just reset by a success) is always eligible; one in backoff\n/// is eligible only once `next_due` has passed.\n// [impl->REQ-PUMP-PEER-ISOLATION]\nfn peer_eligible(sched: &HashMap<String, PeerSched>, peer_hex: &str, now: Instant) -> bool {\n…\n}\n…\npub fn run_peer_pump(\n    broker_name: &str,\n    registry: Arc<RegistryHost>,\n    paths: &PumpPaths,\n    resolver: PeerResolver,\n    config: PumpConfig,\n    hooks: PumpHooks,\n    stop: &AtomicBool,\n) -> io::Result<()> {\n    let PumpConfig {\n        cadence,\n        full_auto_update,\n    } = config;\n    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\n    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n    // hfenduleam stall). A timed-out read escalates to a supervised restart\n    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    let mut brain = connect(broker_name)?;\n    // The DEDICATED event-drain carrier (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION):\n    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\n    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\n    // whose reply-reads consume every event kind off their carrier; sharing one\n    // carrier would make a pull-read swallow a presence frame (advancing the\n    // exactly-once cursor while dropping the outcome — the D4c break). Two\n    // carriers keep the drain and the pulls from swallowing each other by\n    // construction. Tail-follow (no history replay, baseline on first live event)\n    // so a long-lived broker's evicted ring can never gap-trip a false restart.\n    let mut events = connect(broker_name)?;\n    events.net_presence_subscribe_tail()?;\n    // Startup reconcile + invariant migration (ADR-0039 Decisions 3+4,\n    // REQ-PEER-ROUTE-CHAIN / REQ-PEERADDR-INVARIANT): validated roster\n    // addresses reconcile into the dial cache BEFORE the first round —\n    // recovery is connection-independent (a node restarted mid-strand heals\n    // here, no operator surgery) — and rows violating `outer key ==\n    // address.id` are repaired from the roster or dropped loudly (the first\n    // post-upgrade load is the migration; the file is never bare-deleted —\n    // valid rows and the id-only fallback behind them stay untouched).\n    // Idempotent, so re-running on every supervised pump restart is free.\n    startup_reconcile_peeraddrs(paths, registry.node_hex());\n    // Reachability health (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): loaded\n    // so the last-good stamps survive a restart; the live counts re-prime as\n    // rounds run. Persisted beside the heartbeat; the status CLI reads it.\n    let health_path = PumpHealth::path_beside(&paths.heartbeat);\n    let mut health = PumpHealth::load_from(&health_path);\n    health.note_disconnected(0); // no conns exist yet in this pump generation\n    let mut ops = EpochSource::load_from(&paths.ops);\n    // The live per-peer connection cache (updated by presence events) + the\n    // per-peer dial schedule (G5 backoff). Both RAM-only, re-primed on restart.\n    let mut conns: HashMap<String, u64> = HashMap::new();\n    let mut sched: HashMap<String, PeerSched> = HashMap::new();\n    // The four cadenced legs behind the worker seam, in fan-out order\n    // (registry → notif → sync → update). The shell drives them generically;\n    // each leg's *what* lives in its module (§Frozen). Construction capture\n    // (§Frozen-4): each worker owns the registry Arc / path slices / config\n    // knob it needs — the sync worker additionally owns the conflict-surface\n    // hooks (ADR-0013).\n    let mut registry_worker = RegistryWorker::new(Arc::clone(&registry), paths, &cadence);\n    let mut notif_worker = NotifWorker::new(paths, &cadence);\n    let mut sync_worker = SyncWorker::new(Arc::clone(&registry), paths, &cadence, hooks);\n    let mut update_worker = UpdateWorker::new(Arc::clone(&registry), paths, &cadence, full_auto_update);\n    let mut workers: [&mut dyn PumpWorker; 4] = [\n        &mut registry_worker,\n        &mut notif_worker,\n        &mut sync_worker,\n        &mut update_worker,\n    ];\n    // WorkerLasts (ADR-0018 §V4): RAM-only last-ran instants, parallel to\n    // `workers`. `None` = never run yet → due immediately, so the first tick\n    // primes every leg. Never `Instant - Duration` — the underflow panic on a\n    // sub-uptime monotonic clock (KH 5.9) lives nowhere in this path.\n    let mut last: [Option<Instant>; 4] = [None; 4];\n    let mut last_hb: Option<Instant> = None;\n\n    while !stop.load(Ordering::Relaxed) {\n        let now = Instant::now();\n        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\n        // while the loop is ALIVE — including idle ticks — never per work round,\n        // so a wedged step and a healthy quiet pump are distinguishable.\n        // Throttled; per-tick, never a worker (REQ-DAEMON-5), no wake.\n        // [impl->REQ-DAEMON-5]\n        if due(last_hb, false, now, HEARTBEAT_PERIOD) {\n            write_heartbeat(&paths.heartbeat);\n            last_hb = Some(now);\n        }\n        // Poll every worker's wake and decide due (the pure scheduler step):\n        // every wake is consumed EVERY tick so a marker is never stranded (the\n        // registry advertise-now, REQ-CONV-2; the sync freshness pull,\n        // REQ-INST-3). The take IS the remove — one wake claims exactly one\n        // forced round.\n        let (due_flags, any_due) = compute_due(&mut workers, &last, now);\n        if !any_due {\n            thread::sleep(TICK);\n            continue;\n        }\n\n        // Per-round shared loads (§Frozen-3): subnets / attachment /\n        // roster-backed policy, read ONCE and handed to every worker.\n        let ctx = RoundCtx::load(paths);\n\n        // Each due worker's node-local pre_round, once per round before ANY\n        // peer_step (only the registry leg does work here — its sweep +\n        // advertise; the rest default to a no-op).\n        pre_rounds(&mut workers, &due_flags, &ctx);\n\n        // ── PUMP-TRUTH W2 round (REQ-PUMP-PEER-ISOLATION) — concurrent dials +\n        // per-peer fault isolation. Group this round's fan-out targets by peer (a\n        // peer may target several subnets; one conn serves them all).\n        let mut peer_subnets: HashMap<String, Vec<String>> = HashMap::new();\n        for (sub_name, peer_hex) in fan_targets(&ctx, registry.node_hex()) {\n            peer_subnets.entry(peer_hex).or_default().push(sub_name);\n        }\n        // Health targets = this round's fan-out set; snapshot the registry\n        // host's last-admitted stamp while here (REQ-PUMP-STAGE-TRUTH).\n        health.set_targets(peer_subnets.keys().cloned(), now_ms());\n        health.note_registry_admit(registry.last_admit_ms());\n        let round_start = Instant::now();\n        let mut pending: HashSet<String> = HashSet::new();\n\n        // Submit NON-BLOCKING dials for eligible unconnected peers; run the due\n        // legs NOW for already-connected ones. The live peer advertises without\n        // waiting on a dead one (no head-of-line block) — a dead peer is only a\n        // pending outcome, never a serial blocker.\n        for (peer_hex, subs) in &peer_subnets {\n            if let Some(&conn_id) = conns.get(peer_hex) {\n                run_peer_subnets(\n                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n                    conn_id, subs, peer_hex, &ctx, round_start,\n                )?;\n            } else if peer_eligible(&sched, peer_hex, round_start) {\n                if let Some(addr) =\n                    resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs, &ctx.policy.roster)\n                {\n                    match brain.submit_dial(addr) {\n                        Ok(()) => {\n                            pending.insert(peer_hex.clone());\n                        }\n                        // A raw carrier-op TimedOut on the submit IPC itself is a\n                        // genuine broker-IPC desync → bubble → supervised restart.\n                        Err(e) if e.kind() == io::ErrorKind::TimedOut => return Err(e),\n                        // The broker refused the submit (net disabled / bad addr):\n                        // an ordinary per-peer failure — back off, round continues.\n                        Err(e) => {\n                            eprintln!(\n                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\n                                now_ms(),\n                                mono_ms()\n                            );\n                            health.note_failed(peer_hex, \"submit\", conns.len(), now_ms());\n                            let prev = sched.get(peer_hex).copied();\n                            sched.insert(peer_hex.clone(), next_peer_backoff(prev, round_start));\n                        }\n                    }\n                } else {\n                    // No route on any chain leg (REQ-PUMP-STAGE-TRUTH stage\n                    // `address-resolution`). Transition-logged: the first\n                    // unroutable round per peer is loud, repeats are quiet —\n                    // the peer retries on its own cadence like before.\n                    if health.note_failed(\n                        peer_hex,\n                        crate::seedproofx::STAGE_RESOLVE,\n                        conns.len(),\n                        now_ms(),\n                    ) {\n                        eprintln!(\n                            \"PUMP_PEER_FAIL:{peer_hex}:stage={}: no route (cache/roster/discovery all empty) wall_ms={} mono_ms={}\",\n                            crate::seedproofx::STAGE_RESOLVE,\n                            now_ms(),\n                            mono_ms()\n                        );\n                    }\n                }\n            }\n        }\n\n        // Drain dial outcomes on the DEDICATED event carrier until the round\n        // deadline (concurrent dials all land within ~one W1 bound). A CONNECTED\n        // runs that peer's legs in THIS round; a DIAL_FAILED backs the peer off; a\n        // peer unresolved at the deadline simply reschedules — NEVER a whole-round\n        // poison. `TimedOut` = quiet (drain done); any other read error = the\n        // broker stopped answering the stream (dead carrier) → bubble → supervised\n        // restart (the re-pointed REQ-HAZARD-PUMP-IPC-DEADLINE: a dead BROKER still\n        // restarts the pump; a dead PEER never does).\n        // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n        let round_deadline = round_start + PEER_REPLY_READ_BUDGET + ROUND_DRAIN_MARGIN;\n        loop {\n            let read_deadline = if pending.is_empty() {\n                Instant::now() // no outstanding submits — sweep only what is queued\n            } else {\n                round_deadline\n            };\n            match classify_drain_read(events.read_event_until(Some(read_deadline))) {\n                DrainStep::Apply(ev) => handle_presence_event(\n                    ev, &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n                    &mut pending, &peer_subnets, &ctx, &paths.peer_addrs, &mut health, round_start,\n                )?,\n                DrainStep::Skip => continue, // the event carrier only carries presence\n                DrainStep::RoundDone => break, // quiet (TimedOut) — drain done\n                DrainStep::Restart(e) => return Err(e), // dead broker/carrier → restart\n            }\n        }\n\n        // Persist the round's reachability truth for the status surfaces\n        // (REQ-PUMP-STAGE-TRUTH: the acceptance surface of this contract).\n        health.save_to(&health_path);\n\n        // Mark-after-round with a POST-round instant (ADR-0018 §V4: the\n        // stagger — ran = ATTEMPTED, not succeeded; the next due is measured\n        // from here, not from the round's start).\n        mark_ran(&mut last, &due_flags, Instant::now());\n    }\n    Ok(())\n}\n\n/// Compute each worker's due flag for this tick (the pure scheduler step):\n/// poll EVERY worker's wake first — even a not-otherwise-due leg, so a marker\n/// is never stranded — then the [`due`] kernel over its RAM-only last-ran\n/// instant. Returns the per-worker flags (parallel to `workers`) and whether\n/// ANY leg is due (an all-idle tick sleeps without loading a round).\nfn compute_due(\n    workers: &mut [&mut dyn PumpWorker],\n    last: &[Option<Instant>],\n    now: Instant,\n) -> (Vec<bool>, bool) {\n    let mut flags = vec![false; workers.len()];\n    let mut any = false;\n    for i in 0..workers.len() {\n        let wake = workers[i].poll_wake();\n        flags[i] = due(last[i], wake, now, workers[i].cadence());\n        any |= flags[i];\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,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,null,172,null,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,null,752]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\mod.rs"}}},"isError":false,"timestamp":1784271495552}}
{"type":"message","id":"d6835c08","parentId":"4aadcabf","timestamp":"2026-07-17T06:58:15.554Z","message":{"role":"toolResult","toolCallId":"call_JK2M1qUTMs8tFNp5yxXDvaab|fc_038f28d3e93bbfd4016a59d287723881969bd1bc6cbe525bd4","toolName":"grep","content":[{"type":"text","text":"# crates/spt-live/src/\n## context.rs\n 76|pub fn read_marker(path: &Path) -> Option<Marker> {\n*77|    let s = std::fs::read_to_string(path).ok()?;\n 78|    Marker::parse_content(&s)\n 79|}\n 80|\n...\n 210|            assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });\n*211|            let content = std::fs::read_to_string(&path).unwrap();\n 212|            assert!(content.contains(\"DIRECT\") && !content.contains(\"STALE\"));\n 213|        });\n 214|    }\n## echo.rs\n 220|        assert_eq!(res.drop_path, dir.path().join(\"doyle-commune.md\"));\n*221|        let written = std::fs::read_to_string(&res.drop_path).unwrap();\n 222|        // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.\n 223|        assert!(\n 224|            written.starts_with(\"Source: echo-commune\\n\"),\n## history.rs\n 127|            let log = resolve_perch_path(id, hint).join(\"history.log\");\n*128|            match std::fs::read_to_string(&log) {\n 129|                Ok(s) => Ok(records(&s)),\n 130|                // No log yet ⇒ a fresh perch with no recorded history.\n 131|                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n## ingest.rs\n 201|// [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\n*202|pub fn ingest_drops(\n 203|    drop_dir: &Path,\n 204|    id: &str,\n 205|    project_id: &str,\n...\n 217|        }\n*218|        let body = std::fs::read_to_string(&drop_path)?;\n 219|        // A dropped commune/signoff is an LLM-authored snapshot.\n 220|        let slices = parse_two_slice(&body);\n 221|        let writes = route_slices(\n...\n 257|            // only on a state change, never per retry tick.\n*258|            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\n 259|            if current != pending {\n 260|                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\n 261|                eprintln!(\n...\n 303|    fn read_live(id: &str) -> String {\n*304|        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()\n 305|    }\n 306|    fn read_project(pid: &str, id: &str) -> String {\n*307|        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()\n 308|    }\n 309|\n 310|    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.\n...\n 397|            // The durable role is UNTOUCHED — the smuggled slice changed nothing.\n*398|            let role = std::fs::read_to_string(spt_store::contextstore::live_role_file(\n 399|                &spt_store::perch::tracked_dir(),\n 400|                \"doyle\",\n 401|            ))\n...\n 552|            // live slice (already committed) and the project slice intact.\n*553|            let pending = std::fs::read_to_string(&drop).unwrap();\n 554|            let reparsed = parse_two_slice(&pending);\n 555|            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 556|            assert_eq!(\n...\n 582|            assert!(first[0].preserved);\n*583|            let pending_after_first = std::fs::read_to_string(&drop).unwrap();\n 584|            let live_after_first = read_live(\"doyle\");\n 585|\n 586|            // Pass 2: STILL empty id ⇒ idempotent. File byte-equal, still\n...\n 595|            assert_eq!(\n*596|                std::fs::read_to_string(&drop).unwrap(),\n 597|                pending_after_first,\n 598|                \"preserved drop is byte-identical across retries (no churn)\"\n 599|            );\n...\n 724|            // The commune-suffix pending is the project-only form.\n*725|            let pending = std::fs::read_to_string(&commune).unwrap();\n 726|            let reparsed = parse_two_slice(&pending);\n 727|            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 728|            assert_eq!(\n...\n 740|            assert_eq!(\n*741|                std::fs::read_to_string(&commune).unwrap(),\n 742|                pending,\n 743|                \"pending byte-identical after the sweep\"\n 744|            );\n...\n 800|            // second and overwrote the commune's pending (documented supersede).\n*801|            let reparsed = parse_two_slice(&std::fs::read_to_string(&commune).unwrap());\n 802|            assert_eq!(reparsed.live, None, \"pending is project-only\");\n 803|            assert_eq!(\n 804|                reparsed.project.as_deref(),\n## inject.rs\n 65|            record_context_injection(\"doyle\", KIND_OWL_MESSAGE, \"from todlando:  hi   there\");\n*66|            let log = std::fs::read_to_string(perch.join(\"digest.log\")).unwrap();\n 67|            let line = log.lines().next().unwrap();\n 68|            assert!(line.contains(r#\"\"context_kind\":\"owl_message\"\"#), \"discriminator: {line}\");\n 69|            assert!(\n## pulse.rs\n 197|            assert!(\n*198|                std::fs::read_to_string(&live_path)\n 199|                    .unwrap()\n 200|                    .contains(\"perimeter agent\"),\n 201|                \"live context surfaced on the first pulse\"\n...\n 219|            assert!(\n*220|                std::fs::read_to_string(&project_path)\n 221|                    .unwrap()\n 222|                    .contains(\"ingest hardening\"),\n 223|                \"project context landed in tracked/projects/<pid>/<id>/project-context.md\"\n...\n 225|            // Nothing lost: both slices durably present at the end of the cycle.\n*226|            assert!(std::fs::read_to_string(&live_path)\n 227|                .unwrap()\n 228|                .contains(\"perimeter agent\"));\n 229|        });\n## reconcile.rs\n 166|    let file_path = worktree.join(file_rel);\n*167|    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();\n 168|    let (local_marker, local_body) = split_marker(&local_raw);\n 169|\n 170|    // Join every parent's vector — the merge base that makes the reconciled\n...\n 177|    for a in &artifacts {\n*178|        let raw = std::fs::read_to_string(a)?;\n 179|        let (m, body) = split_marker(&raw);\n 180|        if let Some(m) = &m {\n 181|            join = join.join(&m.vector);\n...\n 307|            // Merged content landed; artifacts gone.\n*308|            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 309|            assert!(content.contains(\"MERGED:\"), \"{content:?}\");\n 310|            assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());\n 311|\n...\n 327|            let wt = seed_conflict(&cs);\n*328|            let before_local = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 329|\n 330|            let out = reconcile_file(\n 331|                &rt(FAILER),\n...\n 347|            assert_eq!(\n*348|                std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap(),\n 349|                before_local,\n 350|                \"local untouched\"\n 351|            );\n...\n 392|            );\n*393|            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 394|            assert!(\n 395|                content.contains(\"FRESH DIRECT\"),\n 396|                \"direct write stays supreme\"\n## resume.rs\n 64|    let root = tracked_dir();\n*65|    let role = std::fs::read_to_string(live_role_file(&root, id)).ok();\n*66|    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();\n*67|    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();\n 68|    let mut out = String::new();\n 69|    // Role FIRST — the start-transition injection order is mechanical, not\n 70|    // incidental (a test pins role → live-context → project-context).\n...\n 110|    let Some(path) = file else { return };\n*111|    let Ok(body) = std::fs::read_to_string(path) else { return };\n 112|    // Strip the checkpoint sentinel before presenting (pre-synthesis strip point):\n 113|    // the marker is spt-core control metadata, never agent context. The inter-marker\n 114|    // text is kept. [impl->REQ-RESUME-CONTEXT-PULL]\n## signoff.rs\n 264|            );\n*265|            let written = std::fs::read_to_string(&live).unwrap();\n 266|            assert!(\n 267|                written.contains(\"Source: self-commune\"),\n 268|                \"provenance: {written:?}\"\n...\n 286|            );\n*287|            assert!(std::fs::read_to_string(&proj)\n 288|                .unwrap()\n 289|                .contains(\"Source: self-commune\"));\n 290|        });"}],"details":{"scopePath":"crates/spt-live/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-live\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":32,"fileCount":9,"files":["crates/spt-live/src/context.rs","crates/spt-live/src/echo.rs","crates/spt-live/src/history.rs","crates/spt-live/src/ingest.rs","crates/spt-live/src/inject.rs","crates/spt-live/src/pulse.rs","crates/spt-live/src/reconcile.rs","crates/spt-live/src/resume.rs","crates/spt-live/src/signoff.rs"],"fileMatches":[{"path":"crates/spt-live/src/context.rs","count":2},{"path":"crates/spt-live/src/echo.rs","count":1},{"path":"crates/spt-live/src/history.rs","count":1},{"path":"crates/spt-live/src/ingest.rs","count":12},{"path":"crates/spt-live/src/inject.rs","count":1},{"path":"crates/spt-live/src/pulse.rs","count":3},{"path":"crates/spt-live/src/reconcile.rs","count":6},{"path":"crates/spt-live/src/resume.rs","count":4},{"path":"crates/spt-live/src/signoff.rs","count":2}],"truncated":false,"displayContent":"# crates/spt-live/src/\n## context.rs\n  76│pub fn read_marker(path: &Path) -> Option<Marker> {\n *77│    let s = std::fs::read_to_string(path).ok()?;\n  78│    Marker::parse_content(&s)\n  79│}\n  80│\n    │...\n 210│            assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });\n*211│            let content = std::fs::read_to_string(&path).unwrap();\n 212│            assert!(content.contains(\"DIRECT\") && !content.contains(\"STALE\"));\n 213│        });\n 214│    }\n## echo.rs\n 220│        assert_eq!(res.drop_path, dir.path().join(\"doyle-commune.md\"));\n*221│        let written = std::fs::read_to_string(&res.drop_path).unwrap();\n 222│        // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.\n 223│        assert!(\n 224│            written.starts_with(\"Source: echo-commune\\n\"),\n## history.rs\n 127│            let log = resolve_perch_path(id, hint).join(\"history.log\");\n*128│            match std::fs::read_to_string(&log) {\n 129│                Ok(s) => Ok(records(&s)),\n 130│                // No log yet ⇒ a fresh perch with no recorded history.\n 131│                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n## ingest.rs\n 201│// [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\n*202│pub fn ingest_drops(\n 203│    drop_dir: &Path,\n 204│    id: &str,\n 205│    project_id: &str,\n    │...\n 217│        }\n*218│        let body = std::fs::read_to_string(&drop_path)?;\n 219│        // A dropped commune/signoff is an LLM-authored snapshot.\n 220│        let slices = parse_two_slice(&body);\n 221│        let writes = route_slices(\n    │...\n 257│            // only on a state change, never per retry tick.\n*258│            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\n 259│            if current != pending {\n 260│                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\n 261│                eprintln!(\n    │...\n 303│    fn read_live(id: &str) -> String {\n*304│        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()\n 305│    }\n 306│    fn read_project(pid: &str, id: &str) -> String {\n*307│        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()\n 308│    }\n 309│\n 310│    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.\n    │...\n 397│            // The durable role is UNTOUCHED — the smuggled slice changed nothing.\n*398│            let role = std::fs::read_to_string(spt_store::contextstore::live_role_file(\n 399│                &spt_store::perch::tracked_dir(),\n 400│                \"doyle\",\n 401│            ))\n    │...\n 552│            // live slice (already committed) and the project slice intact.\n*553│            let pending = std::fs::read_to_string(&drop).unwrap();\n 554│            let reparsed = parse_two_slice(&pending);\n 555│            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 556│            assert_eq!(\n    │...\n 582│            assert!(first[0].preserved);\n*583│            let pending_after_first = std::fs::read_to_string(&drop).unwrap();\n 584│            let live_after_first = read_live(\"doyle\");\n 585│\n 586│            // Pass 2: STILL empty id ⇒ idempotent. File byte-equal, still\n    │...\n 595│            assert_eq!(\n*596│                std::fs::read_to_string(&drop).unwrap(),\n 597│                pending_after_first,\n 598│                \"preserved drop is byte-identical across retries (no churn)\"\n 599│            );\n    │...\n 724│            // The commune-suffix pending is the project-only form.\n*725│            let pending = std::fs::read_to_string(&commune).unwrap();\n 726│            let reparsed = parse_two_slice(&pending);\n 727│            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 728│            assert_eq!(\n    │...\n 740│            assert_eq!(\n*741│                std::fs::read_to_string(&commune).unwrap(),\n 742│                pending,\n 743│                \"pending byte-identical after the sweep\"\n 744│            );\n    │...\n 800│            // second and overwrote the commune's pending (documented supersede).\n*801│            let reparsed = parse_two_slice(&std::fs::read_to_string(&commune).unwrap());\n 802│            assert_eq!(reparsed.live, None, \"pending is project-only\");\n 803│            assert_eq!(\n 804│                reparsed.project.as_deref(),\n## inject.rs\n 65│            record_context_injection(\"doyle\", KIND_OWL_MESSAGE, \"from todlando:  hi   there\");\n*66│            let log = std::fs::read_to_string(perch.join(\"digest.log\")).unwrap();\n 67│            let line = log.lines().next().unwrap();\n 68│            assert!(line.contains(r#\"\"context_kind\":\"owl_message\"\"#), \"discriminator: {line}\");\n 69│            assert!(\n## pulse.rs\n 197│            assert!(\n*198│                std::fs::read_to_string(&live_path)\n 199│                    .unwrap()\n 200│                    .contains(\"perimeter agent\"),\n 201│                \"live context surfaced on the first pulse\"\n    │...\n 219│            assert!(\n*220│                std::fs::read_to_string(&project_path)\n 221│                    .unwrap()\n 222│                    .contains(\"ingest hardening\"),\n 223│                \"project context landed in tracked/projects/<pid>/<id>/project-context.md\"\n    │...\n 225│            // Nothing lost: both slices durably present at the end of the cycle.\n*226│            assert!(std::fs::read_to_string(&live_path)\n 227│                .unwrap()\n 228│                .contains(\"perimeter agent\"));\n 229│        });\n## reconcile.rs\n 166│    let file_path = worktree.join(file_rel);\n*167│    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();\n 168│    let (local_marker, local_body) = split_marker(&local_raw);\n 169│\n 170│    // Join every parent's vector — the merge base that makes the reconciled\n    │...\n 177│    for a in &artifacts {\n*178│        let raw = std::fs::read_to_string(a)?;\n 179│        let (m, body) = split_marker(&raw);\n 180│        if let Some(m) = &m {\n 181│            join = join.join(&m.vector);\n    │...\n 307│            // Merged content landed; artifacts gone.\n*308│            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 309│            assert!(content.contains(\"MERGED:\"), \"{content:?}\");\n 310│            assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());\n 311│\n    │...\n 327│            let wt = seed_conflict(&cs);\n*328│            let before_local = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 329│\n 330│            let out = reconcile_file(\n 331│                &rt(FAILER),\n    │...\n 347│            assert_eq!(\n*348│                std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap(),\n 349│                before_local,\n 350│                \"local untouched\"\n 351│            );\n    │...\n 392│            );\n*393│            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 394│            assert!(\n 395│                content.contains(\"FRESH DIRECT\"),\n 396│                \"direct write stays supreme\"\n## resume.rs\n  64│    let root = tracked_dir();\n *65│    let role = std::fs::read_to_string(live_role_file(&root, id)).ok();\n *66│    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();\n *67│    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();\n  68│    let mut out = String::new();\n  69│    // Role FIRST — the start-transition injection order is mechanical, not\n  70│    // incidental (a test pins role → live-context → project-context).\n    │...\n 110│    let Some(path) = file else { return };\n*111│    let Ok(body) = std::fs::read_to_string(path) else { return };\n 112│    // Strip the checkpoint sentinel before presenting (pre-synthesis strip point):\n 113│    // the marker is spt-core control metadata, never agent context. The inter-marker\n 114│    // text is kept. [impl->REQ-RESUME-CONTEXT-PULL]\n## signoff.rs\n 264│            );\n*265│            let written = std::fs::read_to_string(&live).unwrap();\n 266│            assert!(\n 267│                written.contains(\"Source: self-commune\"),\n 268│                \"provenance: {written:?}\"\n    │...\n 286│            );\n*287│            assert!(std::fs::read_to_string(&proj)\n 288│                .unwrap()\n 289│                .contains(\"Source: self-commune\"));\n 290│        });"},"isError":false,"timestamp":1784271495554}}
{"type":"message","id":"bea15a68","parentId":"d6835c08","timestamp":"2026-07-17T06:58:15.556Z","message":{"role":"toolResult","toolCallId":"call_BPYnns2KYIvMnga565MhTnlo|fc_038f28d3e93bbfd4016a59d287723c81969c7bfd9a0811547b","toolName":"grep","content":[{"type":"text","text":"# crates/spt-live/src/\n## context.rs\n 76|pub fn read_marker(path: &Path) -> Option<Marker> {\n*77|    let s = std::fs::read_to_string(path).ok()?;\n 78|    Marker::parse_content(&s)\n 79|}\n 80|\n...\n 210|            assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });\n*211|            let content = std::fs::read_to_string(&path).unwrap();\n 212|            assert!(content.contains(\"DIRECT\") && !content.contains(\"STALE\"));\n 213|        });\n 214|    }\n## digest.rs\n 123|                .map_err(DigestExtractError::Fill)?;\n*124|            let raw = std::fs::read(&path).map_err(DigestExtractError::Read)?;\n 125|\n 126|            // The extractor sees the located path as `{source}` and the bytes on stdin.\n 127|            let mut ekeys = keys.clone();\n## echo.rs\n 220|        assert_eq!(res.drop_path, dir.path().join(\"doyle-commune.md\"));\n*221|        let written = std::fs::read_to_string(&res.drop_path).unwrap();\n 222|        // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.\n 223|        assert!(\n 224|            written.starts_with(\"Source: echo-commune\\n\"),\n## history.rs\n 83|/// (a fresh perch), not an error.\n*84|pub fn fetch_history(\n 85|    history: &History,\n 86|    keys: &BTreeMap<String, String>,\n 87|    id: &str,\n...\n 121|            let path = spt_runtime::runtime::fill_template(locate, keys)?;\n*122|            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\n 123|            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\n 124|            ok_or_nonzero(out).map(|s| records(&s))\n 125|        }\n...\n 127|            let log = resolve_perch_path(id, hint).join(\"history.log\");\n*128|            match std::fs::read_to_string(&log) {\n 129|                Ok(s) => Ok(records(&s)),\n 130|                // No log yet ⇒ a fresh perch with no recorded history.\n 131|                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n## ingest.rs\n 217|        }\n*218|        let body = std::fs::read_to_string(&drop_path)?;\n 219|        // A dropped commune/signoff is an LLM-authored snapshot.\n 220|        let slices = parse_two_slice(&body);\n 221|        let writes = route_slices(\n...\n 257|            // only on a state change, never per retry tick.\n*258|            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\n 259|            if current != pending {\n 260|                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\n 261|                eprintln!(\n...\n 303|    fn read_live(id: &str) -> String {\n*304|        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()\n 305|    }\n 306|    fn read_project(pid: &str, id: &str) -> String {\n*307|        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()\n 308|    }\n 309|\n 310|    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.\n...\n 397|            // The durable role is UNTOUCHED — the smuggled slice changed nothing.\n*398|            let role = std::fs::read_to_string(spt_store::contextstore::live_role_file(\n 399|                &spt_store::perch::tracked_dir(),\n 400|                \"doyle\",\n 401|            ))\n...\n 552|            // live slice (already committed) and the project slice intact.\n*553|            let pending = std::fs::read_to_string(&drop).unwrap();\n 554|            let reparsed = parse_two_slice(&pending);\n 555|            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 556|            assert_eq!(\n...\n 582|            assert!(first[0].preserved);\n*583|            let pending_after_first = std::fs::read_to_string(&drop).unwrap();\n 584|            let live_after_first = read_live(\"doyle\");\n 585|\n 586|            // Pass 2: STILL empty id ⇒ idempotent. File byte-equal, still\n...\n 595|            assert_eq!(\n*596|                std::fs::read_to_string(&drop).unwrap(),\n 597|                pending_after_first,\n 598|                \"preserved drop is byte-identical across retries (no churn)\"\n 599|            );\n...\n 724|            // The commune-suffix pending is the project-only form.\n*725|            let pending = std::fs::read_to_string(&commune).unwrap();\n 726|            let reparsed = parse_two_slice(&pending);\n 727|            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 728|            assert_eq!(\n...\n 740|            assert_eq!(\n*741|                std::fs::read_to_string(&commune).unwrap(),\n 742|                pending,\n 743|                \"pending byte-identical after the sweep\"\n 744|            );\n...\n 800|            // second and overwrote the commune's pending (documented supersede).\n*801|            let reparsed = parse_two_slice(&std::fs::read_to_string(&commune).unwrap());\n 802|            assert_eq!(reparsed.live, None, \"pending is project-only\");\n 803|            assert_eq!(\n 804|                reparsed.project.as_deref(),\n## inject.rs\n 65|            record_context_injection(\"doyle\", KIND_OWL_MESSAGE, \"from todlando:  hi   there\");\n*66|            let log = std::fs::read_to_string(perch.join(\"digest.log\")).unwrap();\n 67|            let line = log.lines().next().unwrap();\n 68|            assert!(line.contains(r#\"\"context_kind\":\"owl_message\"\"#), \"discriminator: {line}\");\n 69|            assert!(\n## pulse.rs\n 197|            assert!(\n*198|                std::fs::read_to_string(&live_path)\n 199|                    .unwrap()\n 200|                    .contains(\"perimeter agent\"),\n 201|                \"live context surfaced on the first pulse\"\n...\n 219|            assert!(\n*220|                std::fs::read_to_string(&project_path)\n 221|                    .unwrap()\n 222|                    .contains(\"ingest hardening\"),\n 223|                \"project context landed in tracked/projects/<pid>/<id>/project-context.md\"\n...\n 225|            // Nothing lost: both slices durably present at the end of the cycle.\n*226|            assert!(std::fs::read_to_string(&live_path)\n 227|                .unwrap()\n 228|                .contains(\"perimeter agent\"));\n 229|        });\n## reconcile.rs\n 166|    let file_path = worktree.join(file_rel);\n*167|    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();\n 168|    let (local_marker, local_body) = split_marker(&local_raw);\n 169|\n 170|    // Join every parent's vector — the merge base that makes the reconciled\n...\n 177|    for a in &artifacts {\n*178|        let raw = std::fs::read_to_string(a)?;\n 179|        let (m, body) = split_marker(&raw);\n 180|        if let Some(m) = &m {\n 181|            join = join.join(&m.vector);\n...\n 307|            // Merged content landed; artifacts gone.\n*308|            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 309|            assert!(content.contains(\"MERGED:\"), \"{content:?}\");\n 310|            assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());\n 311|\n...\n 327|            let wt = seed_conflict(&cs);\n*328|            let before_local = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 329|\n 330|            let out = reconcile_file(\n 331|                &rt(FAILER),\n...\n 347|            assert_eq!(\n*348|                std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap(),\n 349|                before_local,\n 350|                \"local untouched\"\n 351|            );\n...\n 392|            );\n*393|            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 394|            assert!(\n 395|                content.contains(\"FRESH DIRECT\"),\n 396|                \"direct write stays supreme\"\n## resume.rs\n 64|    let root = tracked_dir();\n*65|    let role = std::fs::read_to_string(live_role_file(&root, id)).ok();\n*66|    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();\n*67|    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();\n 68|    let mut out = String::new();\n 69|    // Role FIRST — the start-transition injection order is mechanical, not\n 70|    // incidental (a test pins role → live-context → project-context).\n...\n 110|    let Some(path) = file else { return };\n*111|    let Ok(body) = std::fs::read_to_string(path) else { return };\n 112|    // Strip the checkpoint sentinel before presenting (pre-synthesis strip point):\n 113|    // the marker is spt-core control metadata, never agent context. The inter-marker\n 114|    // text is kept. [impl->REQ-RESUME-CONTEXT-PULL]\n## signoff.rs\n 264|            );\n*265|            let written = std::fs::read_to_string(&live).unwrap();\n 266|            assert!(\n 267|                written.contains(\"Source: self-commune\"),\n 268|                \"provenance: {written:?}\"\n...\n 286|            );\n*287|            assert!(std::fs::read_to_string(&proj)\n 288|                .unwrap()\n 289|                .contains(\"Source: self-commune\"));\n 290|        });"}],"details":{"scopePath":"crates/spt-live/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-live\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":34,"fileCount":10,"files":["crates/spt-live/src/context.rs","crates/spt-live/src/digest.rs","crates/spt-live/src/echo.rs","crates/spt-live/src/history.rs","crates/spt-live/src/ingest.rs","crates/spt-live/src/inject.rs","crates/spt-live/src/pulse.rs","crates/spt-live/src/reconcile.rs","crates/spt-live/src/resume.rs","crates/spt-live/src/signoff.rs"],"fileMatches":[{"path":"crates/spt-live/src/context.rs","count":2},{"path":"crates/spt-live/src/digest.rs","count":1},{"path":"crates/spt-live/src/echo.rs","count":1},{"path":"crates/spt-live/src/history.rs","count":3},{"path":"crates/spt-live/src/ingest.rs","count":11},{"path":"crates/spt-live/src/inject.rs","count":1},{"path":"crates/spt-live/src/pulse.rs","count":3},{"path":"crates/spt-live/src/reconcile.rs","count":6},{"path":"crates/spt-live/src/resume.rs","count":4},{"path":"crates/spt-live/src/signoff.rs","count":2}],"truncated":false,"displayContent":"# crates/spt-live/src/\n## context.rs\n  76│pub fn read_marker(path: &Path) -> Option<Marker> {\n *77│    let s = std::fs::read_to_string(path).ok()?;\n  78│    Marker::parse_content(&s)\n  79│}\n  80│\n    │...\n 210│            assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });\n*211│            let content = std::fs::read_to_string(&path).unwrap();\n 212│            assert!(content.contains(\"DIRECT\") && !content.contains(\"STALE\"));\n 213│        });\n 214│    }\n## digest.rs\n 123│                .map_err(DigestExtractError::Fill)?;\n*124│            let raw = std::fs::read(&path).map_err(DigestExtractError::Read)?;\n 125│\n 126│            // The extractor sees the located path as `{source}` and the bytes on stdin.\n 127│            let mut ekeys = keys.clone();\n## echo.rs\n 220│        assert_eq!(res.drop_path, dir.path().join(\"doyle-commune.md\"));\n*221│        let written = std::fs::read_to_string(&res.drop_path).unwrap();\n 222│        // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.\n 223│        assert!(\n 224│            written.starts_with(\"Source: echo-commune\\n\"),\n## history.rs\n  83│/// (a fresh perch), not an error.\n *84│pub fn fetch_history(\n  85│    history: &History,\n  86│    keys: &BTreeMap<String, String>,\n  87│    id: &str,\n    │...\n 121│            let path = spt_runtime::runtime::fill_template(locate, keys)?;\n*122│            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\n 123│            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\n 124│            ok_or_nonzero(out).map(|s| records(&s))\n 125│        }\n    │...\n 127│            let log = resolve_perch_path(id, hint).join(\"history.log\");\n*128│            match std::fs::read_to_string(&log) {\n 129│                Ok(s) => Ok(records(&s)),\n 130│                // No log yet ⇒ a fresh perch with no recorded history.\n 131│                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n## ingest.rs\n 217│        }\n*218│        let body = std::fs::read_to_string(&drop_path)?;\n 219│        // A dropped commune/signoff is an LLM-authored snapshot.\n 220│        let slices = parse_two_slice(&body);\n 221│        let writes = route_slices(\n    │...\n 257│            // only on a state change, never per retry tick.\n*258│            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\n 259│            if current != pending {\n 260│                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\n 261│                eprintln!(\n    │...\n 303│    fn read_live(id: &str) -> String {\n*304│        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()\n 305│    }\n 306│    fn read_project(pid: &str, id: &str) -> String {\n*307│        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()\n 308│    }\n 309│\n 310│    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.\n    │...\n 397│            // The durable role is UNTOUCHED — the smuggled slice changed nothing.\n*398│            let role = std::fs::read_to_string(spt_store::contextstore::live_role_file(\n 399│                &spt_store::perch::tracked_dir(),\n 400│                \"doyle\",\n 401│            ))\n    │...\n 552│            // live slice (already committed) and the project slice intact.\n*553│            let pending = std::fs::read_to_string(&drop).unwrap();\n 554│            let reparsed = parse_two_slice(&pending);\n 555│            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 556│            assert_eq!(\n    │...\n 582│            assert!(first[0].preserved);\n*583│            let pending_after_first = std::fs::read_to_string(&drop).unwrap();\n 584│            let live_after_first = read_live(\"doyle\");\n 585│\n 586│            // Pass 2: STILL empty id ⇒ idempotent. File byte-equal, still\n    │...\n 595│            assert_eq!(\n*596│                std::fs::read_to_string(&drop).unwrap(),\n 597│                pending_after_first,\n 598│                \"preserved drop is byte-identical across retries (no churn)\"\n 599│            );\n    │...\n 724│            // The commune-suffix pending is the project-only form.\n*725│            let pending = std::fs::read_to_string(&commune).unwrap();\n 726│            let reparsed = parse_two_slice(&pending);\n 727│            assert_eq!(reparsed.live, None, \"live slice stripped from pending form\");\n 728│            assert_eq!(\n    │...\n 740│            assert_eq!(\n*741│                std::fs::read_to_string(&commune).unwrap(),\n 742│                pending,\n 743│                \"pending byte-identical after the sweep\"\n 744│            );\n    │...\n 800│            // second and overwrote the commune's pending (documented supersede).\n*801│            let reparsed = parse_two_slice(&std::fs::read_to_string(&commune).unwrap());\n 802│            assert_eq!(reparsed.live, None, \"pending is project-only\");\n 803│            assert_eq!(\n 804│                reparsed.project.as_deref(),\n## inject.rs\n 65│            record_context_injection(\"doyle\", KIND_OWL_MESSAGE, \"from todlando:  hi   there\");\n*66│            let log = std::fs::read_to_string(perch.join(\"digest.log\")).unwrap();\n 67│            let line = log.lines().next().unwrap();\n 68│            assert!(line.contains(r#\"\"context_kind\":\"owl_message\"\"#), \"discriminator: {line}\");\n 69│            assert!(\n## pulse.rs\n 197│            assert!(\n*198│                std::fs::read_to_string(&live_path)\n 199│                    .unwrap()\n 200│                    .contains(\"perimeter agent\"),\n 201│                \"live context surfaced on the first pulse\"\n    │...\n 219│            assert!(\n*220│                std::fs::read_to_string(&project_path)\n 221│                    .unwrap()\n 222│                    .contains(\"ingest hardening\"),\n 223│                \"project context landed in tracked/projects/<pid>/<id>/project-context.md\"\n    │...\n 225│            // Nothing lost: both slices durably present at the end of the cycle.\n*226│            assert!(std::fs::read_to_string(&live_path)\n 227│                .unwrap()\n 228│                .contains(\"perimeter agent\"));\n 229│        });\n## reconcile.rs\n 166│    let file_path = worktree.join(file_rel);\n*167│    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();\n 168│    let (local_marker, local_body) = split_marker(&local_raw);\n 169│\n 170│    // Join every parent's vector — the merge base that makes the reconciled\n    │...\n 177│    for a in &artifacts {\n*178│        let raw = std::fs::read_to_string(a)?;\n 179│        let (m, body) = split_marker(&raw);\n 180│        if let Some(m) = &m {\n 181│            join = join.join(&m.vector);\n    │...\n 307│            // Merged content landed; artifacts gone.\n*308│            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 309│            assert!(content.contains(\"MERGED:\"), \"{content:?}\");\n 310│            assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());\n 311│\n    │...\n 327│            let wt = seed_conflict(&cs);\n*328│            let before_local = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 329│\n 330│            let out = reconcile_file(\n 331│                &rt(FAILER),\n    │...\n 347│            assert_eq!(\n*348│                std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap(),\n 349│                before_local,\n 350│                \"local untouched\"\n 351│            );\n    │...\n 392│            );\n*393│            let content = std::fs::read_to_string(wt.join(\"live-context.md\")).unwrap();\n 394│            assert!(\n 395│                content.contains(\"FRESH DIRECT\"),\n 396│                \"direct write stays supreme\"\n## resume.rs\n  64│    let root = tracked_dir();\n *65│    let role = std::fs::read_to_string(live_role_file(&root, id)).ok();\n *66│    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();\n *67│    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();\n  68│    let mut out = String::new();\n  69│    // Role FIRST — the start-transition injection order is mechanical, not\n  70│    // incidental (a test pins role → live-context → project-context).\n    │...\n 110│    let Some(path) = file else { return };\n*111│    let Ok(body) = std::fs::read_to_string(path) else { return };\n 112│    // Strip the checkpoint sentinel before presenting (pre-synthesis strip point):\n 113│    // the marker is spt-core control metadata, never agent context. The inter-marker\n 114│    // text is kept. [impl->REQ-RESUME-CONTEXT-PULL]\n## signoff.rs\n 264│            );\n*265│            let written = std::fs::read_to_string(&live).unwrap();\n 266│            assert!(\n 267│                written.contains(\"Source: self-commune\"),\n 268│                \"provenance: {written:?}\"\n    │...\n 286│            );\n*287│            assert!(std::fs::read_to_string(&proj)\n 288│                .unwrap()\n 289│                .contains(\"Source: self-commune\"));\n 290│        });"},"isError":false,"timestamp":1784271495556}}
{"type":"message","id":"ce4dfc76","parentId":"bea15a68","timestamp":"2026-07-17T06:58:15.578Z","message":{"role":"toolResult","toolCallId":"call_4cTWWurnuzwys1XLYXsCAHfV|fc_038f28d3e93bbfd4016a59d28772308196a804527b6caaf968","toolName":"read","content":[{"type":"text","text":"    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n…\n    ) {\n…\n        while !stop.load(Ordering::Relaxed) {\n…\n            match self.pulse_tick(session_id) {\n                Ok(report) => {\n…\n                    on_tick(&report);\n                }\n                Err(_) => { /* a transient tick error must not kill the loop */ }\n            }\n            // Advance past the FIRED grid point AND past `now` (doyle amendment\n            // 7): strictly-after-fired keeps the double-fire guard (a same-ms tick\n            // steps exactly one grid); skip-past-now collapses a stall (an\n            // overrunning tick, or a suspend/resume that left `deadline` many\n            // grids behind) to ONE catch-up fire instead of a back-to-back storm.\n            deadline = anchor.advance_after_fire(deadline, now_ms());\n        }\n    }\n\n    /// Fetch history per the manifest `[history]` strategy (empty if none).\n    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n        // history fetch below), so a live-update refresh never waits on it.\n        let manifest = self.cell.manifest_snapshot();\n        let Some(h) = &manifest.history else {\n            return Vec::new();\n        };\n        fetch_history(\n            h,\n            &self.base_keys(session_id),\n            &self.id,\n            ParentHint::Infer,\n            LIVE_BOUNDED_TIMEOUT,\n        )\n        .unwrap_or_default()\n    }\n\n    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the\n    /// production transition host wiring [`crate::resting::apply_event`]'s two\n    /// effect seams to the real machinery:\n    ///\n    /// - **active → (dormant|suspended)**: the transition echo commune\n    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n    ///   outgoing instance's final context delta, BEFORE the flip persists\n    ///   (KH 3.3) — it syncs to whichever instance activates next.\n    /// - **wake (→ active)**: re-activation in place catches up immediately —\n    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8\n    ///   deferral, real now that the edge exists) + the freshness-pull marker\n    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not\n    ///   at the next anti-entropy cadence).\n    ///\n    /// Registry advertisement follows on the pump's next `advertise_local`\n    /// scan — the persisted record is what it reads, epoch-bumped per row.\n    // [impl->REQ-INST-3]\n    // [impl->REQ-INST-4]\n    pub fn rest_event(\n        &self,\n        session_id: Option<&str>,\n        event: crate::resting::RestEvent,\n    ) -> Result<Option<crate::resting::EdgeReport>, String> {\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let report = crate::resting::apply_event(\n            &perch_path,\n            event,\n            self.cfg.auto_suspend_after_ms,\n            now_ms(),\n            || self.fire_echo(session_id),\n            || self.fire_wake_effects(),\n        )?;\n        // The shell leg of the edge (M5-D4a): suspend closes online shells,\n        // wake relaunches persistent ones — same cascade as the daemon-side\n        // host, so the two transition hosts can never drift.\n        // [impl->REQ-SHELL-2]\n        if let Some(r) = report {\n            crate::resting::cascade_shells_on_edge(&self.id, &r);\n            // A real rest edge changes the advertised state — wake the\n            // pump's registry leg (M8 decision 15, REQ-CONV-2; the same\n            // drop as the daemon-side host, so the two hosts never drift).\n            // [impl->REQ-CONV-2]\n            crate::registryhost::request_advertise_now();\n        }\n        Ok(report)\n    }\n\n    /// The wake-edge effects — the shared daemon glue\n    /// ([`crate::resting::fire_wake_effects`]): resurface at the Wake\n    /// boundary + the freshness-pull marker for the peer pump.\n    // [impl->REQ-INST-3]\n    fn fire_wake_effects(&self) {\n        crate::resting::fire_wake_effects(&self.id);\n    }\n\n    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.\n    ///\n    /// LIFECYCLE-TRUTH W1 skips (both Ok — a skip is a named configuration fact,\n    /// never a strike):\n    /// - **no `[session.echo_commune]` role** — the role is OPTIONAL per the\n    ///   published contract; skip with a once-per-endpoint note\n    ///   (REQ-PSYCHE-ROLE-OPTIONAL-SKIP — pre-fix this hard-failed every fire and\n    ///   3-strike-stamped a healthy host).\n    /// - **relative `commune_dir` with no recorded endpoint cwd** — skip LOUD, never\n    ///   a raw relative write (REQ-ECHO-DROP-DIR-RESOLVE — pre-fix the raw manifest\n    ///   dir went straight to `run_echo_commune`, which under the WMI-launched\n    ///   daemon's System32 cwd = deterministic os error 5).\n    ///\n    /// The dir routes through [`resolve_endpoint_drop_dir`] — the SAME resolver the\n    /// ingest leg and `psyche_drop_file` already use — and the summarizer spawn gets\n    /// the endpoint's cwd as its default working dir (a role-declared cwd wins).\n    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n        let Some(raw) = &self.commune_dir else {\n            return Ok(());\n        };\n        // [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n        if self.cell.manifest_snapshot().session.echo_commune.is_none() {\n            note_echo_role_absent_once(&self.id);\n            return Ok(());\n        }\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\n        let Some(dir) = resolve_endpoint_drop_dir(raw, endpoint_cwd.as_deref().map(Path::new))\n        else {\n            warn_echo_no_cwd_once(&self.id, raw);\n            return Ok(());\n        };\n        let history = self.history(session_id);\n        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n        // dir so the summarizer child runs where the endpoint lives, never in the\n        // daemon's own process cwd (REQ-ECHO-DROP-DIR-RESOLVE rider).\n        let runtime = self\n            .cell\n            .runtime_snapshot()\n            .with_spawn_cwd(endpoint_cwd.map(PathBuf::from));\n        run_echo_commune(\n            &runtime,\n            &self.id,\n            &self.base_keys(session_id),\n            &history,\n            &dir,\n            LIVE_BOUNDED_TIMEOUT,\n        )\n        .map_err(|e| e.to_string())?;\n        // Every successful psyche operation clears a stale fault stamp — this\n        // covers the checkpoint/rest-edge transition echo, the signoff echo, and\n        // the pulse commune-sync (the event-turn leg clears via\n        // `note_turn_outcome`'s Ok arm). A SKIP above deliberately does NOT clear:\n        // nothing ran, so nothing was proven healthy.\n        // [impl->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS]\n        self.clear_psyche_fault_stamp();\n        Ok(())\n    }\n…\n    /// ── PSYCHE-TURN CONTRACT (adapter FREEZE point — perri's `psyche_resume` role) ──\n    ///\n    /// Keys IN (`base_keys` + the W2 custody overrides — all daemon-filled, the child\n    /// self-resolves NONE):\n    /// - `{id}` = the parent live-endpoint id.\n    /// - `{session_id}` = **the PSYCHE's OWN session id (F-030 W2 custody)**, a\n    ///   core-MINTED canonical UUID-v4 that the harness ADOPTS (FORK-1c) — NEVER the\n    ///   parent's (W1's custody bug). It is stored in the nested `{id}-psyche` custody\n    ///   record ([`spt_store::psyche_custody`]) and a parent `/clear` boundary\n    ///   (which rotates the PARENT's sid) does NOT touch it — the psyche's\n    ///   conversational thread survives parent resets.\n    /// - `{parent_session_id}` = the parent's sid, an OWN explicit key (never aliased),\n    ///   present when the host was started with one — for a template that needs it.\n    /// - `{psyche_context_file}` (F-030 W4) = the PATH to the core-written mind FILE in\n    ///   the nested `{id}-psyche` perch dir — ALWAYS filled (referencing is always\n    ///   legal). The mind rides this FILE, never the shim argv (a real ~20KB mind on\n    ///   argv exceeds the win32 CreateProcess ~32k cap and BRICKS the spawn). Core\n    ///   writes the file BEFORE each spawn (overwrite in place, persists between turns,\n    ///   never deleted): a FIRST (FreshWithPreload) turn writes it NON-EMPTY (the\n    ///   composed mind `download_psyche_context`: role/live/project tiers + any pending\n    ///   commune/signoff drop, or the `<fresh-psyche/>` marker when the mind composes\n    ///   empty); a LATER (ContinueExisting) turn writes it EXACTLY 0 bytes (same\n    ///   `{session_id}`, no re-preload — perri PIN 1: NO sentinel EVER). The shim\n    ///   reads-file-then-trims and discriminates fresh-vs-continue on the FILE CONTENT\n    ///   being NON-EMPTY-vs-EMPTY (replaces the W3 `{psyche_context}` body key).\n    /// - `{node}` = this node's advertised label (absent ⇒ a `{node}` reference fails\n    ///   LOUD, never an empty token — the REQ-MANIFEST-NODE-KEY precedent). `{subnet}` =\n    ///   the endpoint's home-subnet label (`own_subnet`, `local` when unhomed; same\n    ///   insert-when-known / loud-if-absent contract, F-030 W4). NO `{home}` key (doyle\n    ///   W4 Q1).\n    ///\n    /// Custody record rule (FORK-2): the nested `{id}-psyche` custody record stamps\n    /// **NO status and NO pid, EVER** — the psyche is turn-scoped, never an instance-\n    /// state participant; a phantom status/pid would render it a cold instance to\n    /// endpoint readers. It is a dedicated `psyche-custody.json` (`{\"session_id\":\"…\"}`),\n    /// NOT an `info.json`, so the perch scans (which key off `info.json`) never see it.\n    ///\n    /// Reseed (FORK-3; W3 amendment): a turn that fails with the RESERVED EXIT CODE 95\n    /// (the psyche's own harness session is gone/expired) is the ONLY failure that\n    /// reseeds — it clears the custody record + fires the LOUD `PSYCHE_RESEED:{id}`, so\n    /// the NEXT fire naturally takes the first-turn (mint + FreshWithPreload) path. Exit\n    /// 95 is the SOLE discriminator; the human-readable stderr line is never parsed.\n    /// EVERY OTHER failure KEEPS custody (a transient harness error must never wipe the\n    /// psyche's thread). Both paths count a strike vs W1's budget.\n    ///\n    /// stdin = the EVENT/COMMUNE payload for THIS turn only. A bare periodic pulse (no\n    /// pending event) feeds the minimal `<pulse/>` standing prompt. The composed mind\n    /// does NOT ride stdin — it rides the `{psyche_context_file}` FILE, whose path is\n    /// ALWAYS filled: the shim reads-then-trims it every turn and the fresh/continue\n    /// discriminator is the FILE CONTENT (non-empty = a fresh/reseed turn carrying the\n    /// composed mind; 0-byte = a continue turn re-using the same `{session_id}`).\n    ///\n    /// stdout = the psyche's SOLE outbound channel. MUST be NON-EMPTY: zero stdout is\n    /// indistinguishable from the KNOWN-HAZARDS 7.3 null-stdout bug and is treated as a\n    /// turn FAILURE (a strike). A turn with nothing to relay must still emit a non-empty\n    /// ack (any prose is fine — prose is simply parsed to zero intents). Intent grammar\n    /// (parsed by `parse_psyche_intents`; the daemon re-stamps `from=`/routing, so the\n    /// psyche NEVER names them): `<EVENT type=\"reply\">…body…</EVENT>` is answered ONLY\n    /// to the inbound sender (on a pulse fire, `reply_to=None`, a reply is DROPPED fail-\n    /// closed); `<EVENT type=\"notify\">…body…</EVENT>` reaches the agent's own user via\n    /// the notif primitive, scoped to the agent's own (`home_subnet`) subnet. Bodies\n    /// over [`crate::psyrelay::PSYCHE_BODY_CAP`] and empty bodies drop typed.\n    // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n    // [impl->REQ-PSYCHE-SID-CUSTODY]\n    fn run_psyche_event_turn(\n        &self,\n        session_id: Option<&str>,\n        stdin: &[u8],\n        reply_to: Option<&str>,\n    ) -> Result<(), String> {\n        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded turn.\n        // A bounded per-event turn holds the psyche image only for its duration, so it\n        // needs no `.live-bin` own-copy (unlike the retired resident spawn) — the\n        // install-dir/PATH snapshot is correct here (same class as translate/digest).\n        //\n        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n        // e.g. a ccs-relocated CLAUDE_CONFIG_DIR) into the psyche_resume spawn env. The\n        // daemon spawns from BARE env, so without this a profile-relocated parent's\n        // psyche lands in the DEFAULT account root → headless \"Not logged in\" → strike\n        // loop (flynn's field death). Harness-agnostic: whatever was captured is\n        // forwarded verbatim; core knows no var by name.\n        let parent_read_env = spt_store::info::read_info(&perch::resolve_perch_path(\n            &self.id,\n            ParentHint::Infer,\n        ))\n        .map(|r| r.read_env)\n        .unwrap_or_default();\n        let runtime = self.cell.runtime_snapshot().with_spawn_env(parent_read_env);\n        let psyche_id = format!(\"{}-psyche\", self.id);\n        // The psyche's OWN nested perch — where its custody sid lives (never the\n        // parent's info.json, so a parent boundary can never rotate it).\n        let psyche_perch = perch::resolve_perch_path(&psyche_id, ParentHint::Explicit(&self.id));\n        let timeout = self.event_turn_timeout();\n        // F-030 W2 — psyche-own sid custody. `base_keys`' `{session_id}` is the PARENT's\n        // (echo/history/signoff keep that fill); this turn OVERRIDES it to the psyche's\n        // OWN sid, LOCAL to the turn. Some(custody) → a LATER (ContinueExisting) turn:\n        // `{session_id}` = the stored sid; the `{psyche_context_file}` is written EXACTLY\n        // 0 bytes (continue). None → a FIRST (FreshWithPreload) turn: mint a UUID, persist\n        // it, and write the `{psyche_context_file}` NON-EMPTY with the composed mind. The\n        // path is ALWAYS filled either way (the fresh/continue signal is the FILE CONTENT,\n        // never key-presence). A reseed (session-not-found, below) clears custody so the\n        // next fire naturally takes this first-turn path.\n        // [impl->REQ-PSYCHE-SID-CUSTODY]\n        let base = self.base_keys(session_id);\n        // F-030 W4 (REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): CORE writes the mind FILE\n        // before the spawn and fills `{psyche_context_file}` = its path — the mind\n        // rides the FILE, never the shim argv (argv-cap-immune). Write failure is LOUD\n        // but non-fatal (mirrors the custody-write policy): the key still carries the\n        // path so the referenced key never faults; the shim treats an unreadable file\n        // as a generic (non-95) fail. On a write error we fall back to the (unwritten)\n        // path so the fill stays legal.\n        let write_ctx = |content: &str| -> PathBuf {\n            write_psyche_context_file(&psyche_perch, content).unwrap_or_else(|e| {\n                eprintln!(\"PSYCHE_CONTEXT_WRITE_FAIL:{}: {e}\", self.id);\n                psyche_perch.join(PSYCHE_CONTEXT_FILE)\n            })\n        };\n        let keys = match spt_store::psyche_custody::read_psyche_sid(&psyche_perch) {\n            Some(sid) => {\n                // ContinueExisting: the psyche resumes its OWN thread — write the\n                // context file EXACTLY 0 bytes (perri PIN 1: NO sentinel EVER, else her\n                // non-empty=fresh discriminator misfires a spurious `--session-id` adopt).\n                let ctx = write_ctx(\"\");\n                psyche_turn_keys(base, session_id, &sid, &ctx)\n            }\n            None => {\n                let minted = spt_store::psyche_custody::mint_uuid_v4();\n                if let Err(e) = spt_store::psyche_custody::write_psyche_sid(&psyche_perch, &minted) {\n                    // Loud but non-fatal: the turn still runs on the minted id; a next\n                    // fire re-mints (custody still None). No first-ever-mint PSYCHE_RESEED —\n                    // that marker fires ONLY at a real reseed (custody-clear on\n                    // session-not-found), never on the initial seed.\n                    eprintln!(\"PSYCHE_CUSTODY_WRITE_FAIL:{}: {e}\", self.id);\n                }\n                let preload = self.psyche_download_context();\n                // First/FreshWithPreload turn MUST write the context file NON-EMPTY so\n                // the empty-vs-nonempty (FILE-content) fresh/continue discriminator\n                // holds: a zero-context agent (download → None) would otherwise read as\n                // ContinueExisting → reseed churn (see first_turn_psyche_context).\n                let seed = first_turn_psyche_context(preload.as_deref());\n                let ctx = write_ctx(seed);\n                psyche_turn_keys(base, session_id, &minted, &ctx)\n            }\n        };\n        // The notify leg's daemon-supplied scope (brainproc.rs NotifCtx pattern +\n        // the agent's own subnet). Assembled fresh per turn like `fire_rollback_notif`.\n        // The notif epoch is minted from the DISTINCT psyche-ops counter, NOT the\n        // pump's shared `epoch` file — the brain's pulse thread and its pump thread\n        // would otherwise race a cross-minter RMW on one counter (the KH 4.8 lost-\n        // update / duplicate-epoch collision); a per-minter counter kills that race\n        // outright (doyle ruling — a distinct minter, not a hand-guard).\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n        let store = NotifStore::open().map_err(|e| e.to_string())?;\n        let mut epochs = EpochSource::load_from(&perch::psyche_ops_epoch_file());\n        let policy = NotifSurfacePolicy::load();\n        let node_hex = crate::presence::local_node_hex();\n        let subnet = self.own_subnet();\n        let owlery = perch::owlery_dir();\n        let mut ctx = PsycheNotifCtx {\n            store: &store,\n            epochs: &mut epochs,\n            node_hex: &node_hex,\n            subnet: &subnet,\n            policy: &policy,\n            now_ms: now_ms(),\n        };\n        // F-030 W2/W3 reseed discrimination: ONLY the reserved EXIT CODE 95 clears the\n        // custody record + fires the LOUD `PSYCHE_RESEED` — the next fire then reseeds\n        // (first-turn mint + FreshWithPreload). Every OTHER failure KEEPS custody. Both a\n        // reseed and a plain failure return `Err`, so BOTH count a strike vs the W1 budget\n        // (`run_pulse_loop` feeds the outcome to `note_turn_outcome`).\n        // [impl->REQ-PSYCHE-SID-CUSTODY]\n        match psyche_turn_and_relay(\n            &runtime, &keys, stdin, reply_to, &psyche_id, &mut ctx, &owlery, timeout,\n        ) {\n            Ok(_outcomes) => Ok(()),\n            Err(e) => {\n                if is_psyche_session_not_found(&e) {\n                    let _ = spt_store::psyche_custody::clear_psyche_sid(&psyche_perch);\n                    eprintln!(\"PSYCHE_RESEED:{}\", self.id);\n                }\n                Err(e.to_string())\n            }\n        }\n    }\n\n    /// Compose the psyche-download written into the `{psyche_context_file}` on a FIRST\n    /// (FreshWithPreload) psyche turn (F-030 W2/W4): the durable role/live/project tiers\n    /// plus any not-yet-synthesized commune/signoff drop, resolved against the parent\n    /// endpoint's bound cwd — the SAME composition `spt api psyche-download` emits.\n    /// `None` when nothing is stored yet (a role-less/context-less fresh endpoint —\n    /// the first turn then simply seeds from the minted `{session_id}` alone, the file\n    /// written with the non-empty `<fresh-psyche/>` marker so it still reads as fresh).\n    // [impl->REQ-PSYCHE-SID-CUSTODY]\n    fn psyche_download_context(&self) -> Option<String> {\n        let parent_perch = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let endpoint_cwd = spt_store::info::read_info(&parent_perch).and_then(|i| i.cwd);\n        let owlery = perch::owlery_dir();\n        // Project routing mirrors `pulse_tick`: an owlery-internal (psyche-host) cwd\n        // routes NO project; a real cwd routes its project tier; no cwd → no project.\n        let project_id = match endpoint_cwd.as_deref() {\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\n                spt_store::project::project_id_for_dir(Path::new(cwd))\n            }\n            _ => String::new(),\n        };\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\n        let commune_file = self.psyche_drop_file(self.commune_dir.as_deref(), cwd_path, \"commune\");\n        let signoff_file = self.psyche_drop_file(self.signoff_dir.as_deref(), cwd_path, \"signoff\");\n        download_psyche_context(\n            &self.id,\n            &project_id,\n            commune_file.as_deref(),\n            signoff_file.as_deref(),\n        )\n    }\n\n    /// Resolve a psyche-download drop FILE (`<dir>/<id>-<kind>.md`) from a raw manifest\n    /// drop dir against the endpoint's cwd — the same dir-resolution rule as\n    /// [`resolve_endpoint_drop_dir`] (absolute as-is; relative joined under the cwd;\n    /// relative-with-no-cwd → `None`) and the same filename convention as `spt`'s\n    /// `resolve_filedrops`, so the mind seeded into the `{psyche_context_file}` sees the\n    /// SAME pending drops the `psyche-download` CLI would.\n    // [impl->REQ-PSYCHE-SID-CUSTODY]\n    fn psyche_drop_file(\n        &self,\n        raw_dir: Option<&Path>,\n        cwd: Option<&Path>,\n        kind: &str,\n    ) -> Option<PathBuf> {\n…\n    }"}],"details":{"displayContent":{"text":"    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n…\n    ) {\n…\n        while !stop.load(Ordering::Relaxed) {\n…\n            match self.pulse_tick(session_id) {\n                Ok(report) => {\n…\n                    on_tick(&report);\n                }\n                Err(_) => { /* a transient tick error must not kill the loop */ }\n            }\n            // Advance past the FIRED grid point AND past `now` (doyle amendment\n            // 7): strictly-after-fired keeps the double-fire guard (a same-ms tick\n            // steps exactly one grid); skip-past-now collapses a stall (an\n            // overrunning tick, or a suspend/resume that left `deadline` many\n            // grids behind) to ONE catch-up fire instead of a back-to-back storm.\n            deadline = anchor.advance_after_fire(deadline, now_ms());\n        }\n    }\n\n    /// Fetch history per the manifest `[history]` strategy (empty if none).\n    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n        // history fetch below), so a live-update refresh never waits on it.\n        let manifest = self.cell.manifest_snapshot();\n        let Some(h) = &manifest.history else {\n            return Vec::new();\n        };\n        fetch_history(\n            h,\n            &self.base_keys(session_id),\n            &self.id,\n            ParentHint::Infer,\n            LIVE_BOUNDED_TIMEOUT,\n        )\n        .unwrap_or_default()\n    }\n\n    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the\n    /// production transition host wiring [`crate::resting::apply_event`]'s two\n    /// effect seams to the real machinery:\n    ///\n    /// - **active → (dormant|suspended)**: the transition echo commune\n    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n    ///   outgoing instance's final context delta, BEFORE the flip persists\n    ///   (KH 3.3) — it syncs to whichever instance activates next.\n    /// - **wake (→ active)**: re-activation in place catches up immediately —\n    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8\n    ///   deferral, real now that the edge exists) + the freshness-pull marker\n    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not\n    ///   at the next anti-entropy cadence).\n    ///\n    /// Registry advertisement follows on the pump's next `advertise_local`\n    /// scan — the persisted record is what it reads, epoch-bumped per row.\n    // [impl->REQ-INST-3]\n    // [impl->REQ-INST-4]\n    pub fn rest_event(\n        &self,\n        session_id: Option<&str>,\n        event: crate::resting::RestEvent,\n    ) -> Result<Option<crate::resting::EdgeReport>, String> {\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let report = crate::resting::apply_event(\n            &perch_path,\n            event,\n            self.cfg.auto_suspend_after_ms,\n            now_ms(),\n            || self.fire_echo(session_id),\n            || self.fire_wake_effects(),\n        )?;\n        // The shell leg of the edge (M5-D4a): suspend closes online shells,\n        // wake relaunches persistent ones — same cascade as the daemon-side\n        // host, so the two transition hosts can never drift.\n        // [impl->REQ-SHELL-2]\n        if let Some(r) = report {\n            crate::resting::cascade_shells_on_edge(&self.id, &r);\n            // A real rest edge changes the advertised state — wake the\n            // pump's registry leg (M8 decision 15, REQ-CONV-2; the same\n            // drop as the daemon-side host, so the two hosts never drift).\n            // [impl->REQ-CONV-2]\n            crate::registryhost::request_advertise_now();\n        }\n        Ok(report)\n    }\n\n    /// The wake-edge effects — the shared daemon glue\n    /// ([`crate::resting::fire_wake_effects`]): resurface at the Wake\n    /// boundary + the freshness-pull marker for the peer pump.\n    // [impl->REQ-INST-3]\n    fn fire_wake_effects(&self) {\n        crate::resting::fire_wake_effects(&self.id);\n    }\n\n    /// Run the echo-commune into the commune dir (T3). No-op (Ok) without a dir.\n    ///\n    /// LIFECYCLE-TRUTH W1 skips (both Ok — a skip is a named configuration fact,\n    /// never a strike):\n    /// - **no `[session.echo_commune]` role** — the role is OPTIONAL per the\n    ///   published contract; skip with a once-per-endpoint note\n    ///   (REQ-PSYCHE-ROLE-OPTIONAL-SKIP — pre-fix this hard-failed every fire and\n    ///   3-strike-stamped a healthy host).\n    /// - **relative `commune_dir` with no recorded endpoint cwd** — skip LOUD, never\n    ///   a raw relative write (REQ-ECHO-DROP-DIR-RESOLVE — pre-fix the raw manifest\n    ///   dir went straight to `run_echo_commune`, which under the WMI-launched\n    ///   daemon's System32 cwd = deterministic os error 5).\n    ///\n    /// The dir routes through [`resolve_endpoint_drop_dir`] — the SAME resolver the\n    /// ingest leg and `psyche_drop_file` already use — and the summarizer spawn gets\n    /// the endpoint's cwd as its default working dir (a role-declared cwd wins).\n    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n        let Some(raw) = &self.commune_dir else {\n            return Ok(());\n        };\n        // [impl->REQ-PSYCHE-ROLE-OPTIONAL-SKIP]\n        if self.cell.manifest_snapshot().session.echo_commune.is_none() {\n            note_echo_role_absent_once(&self.id);\n            return Ok(());\n        }\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\n        let Some(dir) = resolve_endpoint_drop_dir(raw, endpoint_cwd.as_deref().map(Path::new))\n        else {\n            warn_echo_no_cwd_once(&self.id, raw);\n            return Ok(());\n        };\n        let history = self.history(session_id);\n        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n        // dir so the summarizer child runs where the endpoint lives, never in the\n        // daemon's own process cwd (REQ-ECHO-DROP-DIR-RESOLVE rider).\n        let runtime = self\n            .cell\n            .runtime_snapshot()\n            .with_spawn_cwd(endpoint_cwd.map(PathBuf::from));\n        run_echo_commune(\n            &runtime,\n            &self.id,\n            &self.base_keys(session_id),\n            &history,\n            &dir,\n            LIVE_BOUNDED_TIMEOUT,\n        )\n        .map_err(|e| e.to_string())?;\n        // Every successful psyche operation clears a stale fault stamp — this\n        // covers the checkpoint/rest-edge transition echo, the signoff echo, and\n        // the pulse commune-sync (the event-turn leg clears via\n        // `note_turn_outcome`'s Ok arm). A SKIP above deliberately does NOT clear:\n        // nothing ran, so nothing was proven healthy.\n        // [impl->REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS]\n        self.clear_psyche_fault_stamp();\n        Ok(())\n    }\n…\n    /// ── PSYCHE-TURN CONTRACT (adapter FREEZE point — perri's `psyche_resume` role) ──\n    ///\n    /// Keys IN (`base_keys` + the W2 custody overrides — all daemon-filled, the child\n    /// self-resolves NONE):\n    /// - `{id}` = the parent live-endpoint id.\n    /// - `{session_id}` = **the PSYCHE's OWN session id (F-030 W2 custody)**, a\n    ///   core-MINTED canonical UUID-v4 that the harness ADOPTS (FORK-1c) — NEVER the\n    ///   parent's (W1's custody bug). It is stored in the nested `{id}-psyche` custody\n    ///   record ([`spt_store::psyche_custody`]) and a parent `/clear` boundary\n    ///   (which rotates the PARENT's sid) does NOT touch it — the psyche's\n    ///   conversational thread survives parent resets.\n    /// - `{parent_session_id}` = the parent's sid, an OWN explicit key (never aliased),\n    ///   present when the host was started with one — for a template that needs it.\n    /// - `{psyche_context_file}` (F-030 W4) = the PATH to the core-written mind FILE in\n    ///   the nested `{id}-psyche` perch dir — ALWAYS filled (referencing is always\n    ///   legal). The mind rides this FILE, never the shim argv (a real ~20KB mind on\n    ///   argv exceeds the win32 CreateProcess ~32k cap and BRICKS the spawn). Core\n    ///   writes the file BEFORE each spawn (overwrite in place, persists between turns,\n    ///   never deleted): a FIRST (FreshWithPreload) turn writes it NON-EMPTY (the\n    ///   composed mind `download_psyche_context`: role/live/project tiers + any pending\n    ///   commune/signoff drop, or the `<fresh-psyche/>` marker when the mind composes\n    ///   empty); a LATER (ContinueExisting) turn writes it EXACTLY 0 bytes (same\n    ///   `{session_id}`, no re-preload — perri PIN 1: NO sentinel EVER). The shim\n    ///   reads-file-then-trims and discriminates fresh-vs-continue on the FILE CONTENT\n    ///   being NON-EMPTY-vs-EMPTY (replaces the W3 `{psyche_context}` body key).\n    /// - `{node}` = this node's advertised label (absent ⇒ a `{node}` reference fails\n    ///   LOUD, never an empty token — the REQ-MANIFEST-NODE-KEY precedent). `{subnet}` =\n    ///   the endpoint's home-subnet label (`own_subnet`, `local` when unhomed; same\n    ///   insert-when-known / loud-if-absent contract, F-030 W4). NO `{home}` key (doyle\n    ///   W4 Q1).\n    ///\n    /// Custody record rule (FORK-2): the nested `{id}-psyche` custody record stamps\n    /// **NO status and NO pid, EVER** — the psyche is turn-scoped, never an instance-\n    /// state participant; a phantom status/pid would render it a cold instance to\n    /// endpoint readers. It is a dedicated `psyche-custody.json` (`{\"session_id\":\"…\"}`),\n    /// NOT an `info.json`, so the perch scans (which key off `info.json`) never see it.\n    ///\n    /// Reseed (FORK-3; W3 amendment): a turn that fails with the RESERVED EXIT CODE 95\n    /// (the psyche's own harness session is gone/expired) is the ONLY failure that\n    /// reseeds — it clears the custody record + fires the LOUD `PSYCHE_RESEED:{id}`, so\n    /// the NEXT fire naturally takes the first-turn (mint + FreshWithPreload) path. Exit\n    /// 95 is the SOLE discriminator; the human-readable stderr line is never parsed.\n    /// EVERY OTHER failure KEEPS custody (a transient harness error must never wipe the\n    /// psyche's thread). Both paths count a strike vs W1's budget.\n    ///\n    /// stdin = the EVENT/COMMUNE payload for THIS turn only. A bare periodic pulse (no\n    /// pending event) feeds the minimal `<pulse/>` standing prompt. The composed mind\n    /// does NOT ride stdin — it rides the `{psyche_context_file}` FILE, whose path is\n    /// ALWAYS filled: the shim reads-then-trims it every turn and the fresh/continue\n    /// discriminator is the FILE CONTENT (non-empty = a fresh/reseed turn carrying the\n    /// composed mind; 0-byte = a continue turn re-using the same `{session_id}`).\n    ///\n    /// stdout = the psyche's SOLE outbound channel. MUST be NON-EMPTY: zero stdout is\n    /// indistinguishable from the KNOWN-HAZARDS 7.3 null-stdout bug and is treated as a\n    /// turn FAILURE (a strike). A turn with nothing to relay must still emit a non-empty\n    /// ack (any prose is fine — prose is simply parsed to zero intents). Intent grammar\n    /// (parsed by `parse_psyche_intents`; the daemon re-stamps `from=`/routing, so the\n    /// psyche NEVER names them): `<EVENT type=\"reply\">…body…</EVENT>` is answered ONLY\n    /// to the inbound sender (on a pulse fire, `reply_to=None`, a reply is DROPPED fail-\n    /// closed); `<EVENT type=\"notify\">…body…</EVENT>` reaches the agent's own user via\n    /// the notif primitive, scoped to the agent's own (`home_subnet`) subnet. Bodies\n    /// over [`crate::psyrelay::PSYCHE_BODY_CAP`] and empty bodies drop typed.\n    // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n    // [impl->REQ-PSYCHE-SID-CUSTODY]\n    fn run_psyche_event_turn(\n        &self,\n        session_id: Option<&str>,\n        stdin: &[u8],\n        reply_to: Option<&str>,\n    ) -> Result<(), String> {\n        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded turn.\n        // A bounded per-event turn holds the psyche image only for its duration, so it\n        // needs no `.live-bin` own-copy (unlike the retired resident spawn) — the\n        // install-dir/PATH snapshot is correct here (same class as translate/digest).\n        //\n        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n        // e.g. a ccs-relocated CLAUDE_CONFIG_DIR) into the psyche_resume spawn env. The\n        // daemon spawns from BARE env, so without this a profile-relocated parent's\n        // psyche lands in the DEFAULT account root → headless \"Not logged in\" → strike\n        // loop (flynn's field death). Harness-agnostic: whatever was captured is\n        // forwarded verbatim; core knows no var by name.\n        let parent_read_env = spt_store::info::read_info(&perch::resolve_perch_path(\n            &self.id,\n            ParentHint::Infer,\n        ))\n        .map(|r| r.read_env)\n        .unwrap_or_default();\n        let runtime = self.cell.runtime_snapshot().with_spawn_env(parent_read_env);\n        let psyche_id = format!(\"{}-psyche\", self.id);\n        // The psyche's OWN nested perch — where its custody sid lives (never the\n        // parent's info.json, so a parent boundary can never rotate it).\n        let psyche_perch = perch::resolve_perch_path(&psyche_id, ParentHint::Explicit(&self.id));\n        let timeout = self.event_turn_timeout();\n        // F-030 W2 — psyche-own sid custody. `base_keys`' `{session_id}` is the PARENT's\n        // (echo/history/signoff keep that fill); this turn OVERRIDES it to the psyche's\n        // OWN sid, LOCAL to the turn. Some(custody) → a LATER (ContinueExisting) turn:\n        // `{session_id}` = the stored sid; the `{psyche_context_file}` is written EXACTLY\n        // 0 bytes (continue). None → a FIRST (FreshWithPreload) turn: mint a UUID, persist\n        // it, and write the `{psyche_context_file}` NON-EMPTY with the composed mind. The\n        // path is ALWAYS filled either way (the fresh/continue signal is the FILE CONTENT,\n        // never key-presence). A reseed (session-not-found, below) clears custody so the\n        // next fire naturally takes this first-turn path.\n        // [impl->REQ-PSYCHE-SID-CUSTODY]\n        let base = self.base_keys(session_id);\n        // F-030 W4 (REQ-PSYCHE-CONTEXT-FILE-INDIRECTION): CORE writes the mind FILE\n        // before the spawn and fills `{psyche_context_file}` = its path — the mind\n        // rides the FILE, never the shim argv (argv-cap-immune). Write failure is LOUD\n        // but non-fatal (mirrors the custody-write policy): the key still carries the\n        // path so the referenced key never faults; the shim treats an unreadable file\n        // as a generic (non-95) fail. On a write error we fall back to the (unwritten)\n        // path so the fill stays legal.\n        let write_ctx = |content: &str| -> PathBuf {\n            write_psyche_context_file(&psyche_perch, content).unwrap_or_else(|e| {\n                eprintln!(\"PSYCHE_CONTEXT_WRITE_FAIL:{}: {e}\", self.id);\n                psyche_perch.join(PSYCHE_CONTEXT_FILE)\n            })\n        };\n        let keys = match spt_store::psyche_custody::read_psyche_sid(&psyche_perch) {\n            Some(sid) => {\n                // ContinueExisting: the psyche resumes its OWN thread — write the\n                // context file EXACTLY 0 bytes (perri PIN 1: NO sentinel EVER, else her\n                // non-empty=fresh discriminator misfires a spurious `--session-id` adopt).\n                let ctx = write_ctx(\"\");\n                psyche_turn_keys(base, session_id, &sid, &ctx)\n            }\n            None => {\n                let minted = spt_store::psyche_custody::mint_uuid_v4();\n                if let Err(e) = spt_store::psyche_custody::write_psyche_sid(&psyche_perch, &minted) {\n                    // Loud but non-fatal: the turn still runs on the minted id; a next\n                    // fire re-mints (custody still None). No first-ever-mint PSYCHE_RESEED —\n                    // that marker fires ONLY at a real reseed (custody-clear on\n                    // session-not-found), never on the initial seed.\n                    eprintln!(\"PSYCHE_CUSTODY_WRITE_FAIL:{}: {e}\", self.id);\n                }\n                let preload = self.psyche_download_context();\n                // First/FreshWithPreload turn MUST write the context file NON-EMPTY so\n                // the empty-vs-nonempty (FILE-content) fresh/continue discriminator\n                // holds: a zero-context agent (download → None) would otherwise read as\n                // ContinueExisting → reseed churn (see first_turn_psyche_context).\n                let seed = first_turn_psyche_context(preload.as_deref());\n                let ctx = write_ctx(seed);\n                psyche_turn_keys(base, session_id, &minted, &ctx)\n            }\n        };\n        // The notify leg's daemon-supplied scope (brainproc.rs NotifCtx pattern +\n        // the agent's own subnet). Assembled fresh per turn like `fire_rollback_notif`.\n        // The notif epoch is minted from the DISTINCT psyche-ops counter, NOT the\n        // pump's shared `epoch` file — the brain's pulse thread and its pump thread\n        // would otherwise race a cross-minter RMW on one counter (the KH 4.8 lost-\n        // update / duplicate-epoch collision); a per-minter counter kills that race\n        // outright (doyle ruling — a distinct minter, not a hand-guard).\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n        let store = NotifStore::open().map_err(|e| e.to_string())?;\n        let mut epochs = EpochSource::load_from(&perch::psyche_ops_epoch_file());\n        let policy = NotifSurfacePolicy::load();\n        let node_hex = crate::presence::local_node_hex();\n        let subnet = self.own_subnet();\n        let owlery = perch::owlery_dir();\n        let mut ctx = PsycheNotifCtx {\n            store: &store,\n            epochs: &mut epochs,\n            node_hex: &node_hex,\n            subnet: &subnet,\n            policy: &policy,\n            now_ms: now_ms(),\n        };\n        // F-030 W2/W3 reseed discrimination: ONLY the reserved EXIT CODE 95 clears the\n        // custody record + fires the LOUD `PSYCHE_RESEED` — the next fire then reseeds\n        // (first-turn mint + FreshWithPreload). Every OTHER failure KEEPS custody. Both a\n        // reseed and a plain failure return `Err`, so BOTH count a strike vs the W1 budget\n        // (`run_pulse_loop` feeds the outcome to `note_turn_outcome`).\n        // [impl->REQ-PSYCHE-SID-CUSTODY]\n        match psyche_turn_and_relay(\n            &runtime, &keys, stdin, reply_to, &psyche_id, &mut ctx, &owlery, timeout,\n        ) {\n            Ok(_outcomes) => Ok(()),\n            Err(e) => {\n                if is_psyche_session_not_found(&e) {\n                    let _ = spt_store::psyche_custody::clear_psyche_sid(&psyche_perch);\n                    eprintln!(\"PSYCHE_RESEED:{}\", self.id);\n                }\n                Err(e.to_string())\n            }\n        }\n    }\n\n    /// Compose the psyche-download written into the `{psyche_context_file}` on a FIRST\n    /// (FreshWithPreload) psyche turn (F-030 W2/W4): the durable role/live/project tiers\n    /// plus any not-yet-synthesized commune/signoff drop, resolved against the parent\n    /// endpoint's bound cwd — the SAME composition `spt api psyche-download` emits.\n    /// `None` when nothing is stored yet (a role-less/context-less fresh endpoint —\n    /// the first turn then simply seeds from the minted `{session_id}` alone, the file\n    /// written with the non-empty `<fresh-psyche/>` marker so it still reads as fresh).\n    // [impl->REQ-PSYCHE-SID-CUSTODY]\n    fn psyche_download_context(&self) -> Option<String> {\n        let parent_perch = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let endpoint_cwd = spt_store::info::read_info(&parent_perch).and_then(|i| i.cwd);\n        let owlery = perch::owlery_dir();\n        // Project routing mirrors `pulse_tick`: an owlery-internal (psyche-host) cwd\n        // routes NO project; a real cwd routes its project tier; no cwd → no project.\n        let project_id = match endpoint_cwd.as_deref() {\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\n                spt_store::project::project_id_for_dir(Path::new(cwd))\n            }\n            _ => String::new(),\n        };\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\n        let commune_file = self.psyche_drop_file(self.commune_dir.as_deref(), cwd_path, \"commune\");\n        let signoff_file = self.psyche_drop_file(self.signoff_dir.as_deref(), cwd_path, \"signoff\");\n        download_psyche_context(\n            &self.id,\n            &project_id,\n            commune_file.as_deref(),\n            signoff_file.as_deref(),\n        )\n    }\n\n    /// Resolve a psyche-download drop FILE (`<dir>/<id>-<kind>.md`) from a raw manifest\n    /// drop dir against the endpoint's cwd — the same dir-resolution rule as\n    /// [`resolve_endpoint_drop_dir`] (absolute as-is; relative joined under the cwd;\n    /// relative-with-no-cwd → `None`) and the same filename convention as `spt`'s\n    /// `resolve_filedrops`, so the mind seeded into the `{psyche_context_file}` sees the\n    /// SAME pending drops the `psyche-download` CLI would.\n    // [impl->REQ-PSYCHE-SID-CUSTODY]\n    fn psyche_drop_file(\n        &self,\n        raw_dir: Option<&Path>,\n        cwd: Option<&Path>,\n        kind: &str,\n    ) -> Option<PathBuf> {\n…\n    }","startLine":692,"lineNumbers":[692,null,698,null,723,null,734,735,null,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,null,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,null,1163]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\lifecycle.rs"}}},"isError":false,"timestamp":1784271495578}}
{"type":"message","id":"8aeb82e1","parentId":"ce4dfc76","timestamp":"2026-07-17T06:58:15.587Z","message":{"role":"toolResult","toolCallId":"call_RDBZlnIL6p1he5tXM5iOlJ5f|fc_038f28d3e93bbfd4016a59d28772448196bfcace825584ebec","toolName":"read","content":[{"type":"text","text":"//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /\n//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/\n//! sessions.log`.\n//!\n//! A live agent that `/clear`s or `/compact`s keeps its perch identity but\n//! rotates its harness `session_id` (a new transcript file). The single\n//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*\n//! the thread but cannot *span* it. This ledger records each rotation so the\n//! digest can enumerate the **last K sessions** and bridge a boundary\n//! (ADR-0019 §thread-spanning).\n//!\n//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is\n//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew\n//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates\n//! the oldest rows out. A re-bind of the *same* session is not a new boundary and\n//! is deduped.\n// [impl->REQ-TERM-6]\n\nuse std::io;\nuse std::path::Path;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::atomic::atomic_write_string;\n\n/// The ledger filename under a perch.\npub const LEDGER_FILE: &str = \"sessions.log\";\n\n/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on\n/// append so the file can never grow without bound (risk #2).\npub const MAX_LEDGER: usize = 64;\n\n/// Why a session became active.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum SessionTrigger {\n    /// First bind of the perch (`api listen` / revive).\n    Boot,\n    /// A `/clear` boundary (`api boundary clear`).\n    Clear,\n    /// A `/compact` boundary (`api boundary compact`).\n    Compact,\n}\n\nimpl SessionTrigger {\n    /// The lowercase token (matches the serde wire form) — used as the boundary\n    /// marker `kind` in the spanned digest.\n    pub fn token(self) -> &'static str {\n        match self {\n            SessionTrigger::Boot => \"boot\",\n            SessionTrigger::Clear => \"clear\",\n            SessionTrigger::Compact => \"compact\",\n        }\n    }\n}\n\n/// One ledger row: a `session_id` became active at `ts` via `trigger`.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct SessionEntry {\n    /// RFC3339-UTC instant (see [`crate::timefmt`]).\n    pub ts: String,\n    /// The harness session id that became active.\n    pub session_id: String,\n    /// What caused the rotation.\n    pub trigger: SessionTrigger,\n    /// The project working directory the session was active in (the W3\n    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a\n    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves\n    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history\n    /// (cross-project rows) restores the right dir from here.\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub cwd: Option<String>,\n    /// A **persisted, monotonic** ledger ordinal — the stable per-row sequence the\n    /// digest cursor anchors to (REQ-DIGEST-CURSOR). The ledger is bounded\n    /// ([`MAX_LEDGER`]): a `read_all` index renumbers survivors when the oldest row\n    /// prunes, which would re-number a consumer's cursor under it. A persisted\n    /// ordinal is stamped once at [`append`] and never moves, so a pruned survivor\n    /// keeps its number. **Additive serde-default**: a pre-migration row missing\n    /// `ordinal` deserializes as `None` and is backfilled to its concrete index on\n    /// the next append (so a migrated ledger is fully ordinal-stamped).\n    // [impl->REQ-DIGEST-CURSOR]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub ordinal: Option<u64>,\n    /// The adapter[:profile] the session ran under (D-2, REQ-SESSION-ADAPTER-RECORDED).\n    /// A resume-from-history restores THIS harness — the one that authored the\n    /// transcript — not merely the endpoint's *current* stamp (which a B-2\n    /// ChangeAdapter or a fork may have moved since). **Additive serde-default**: a\n    /// pre-migration row missing `adapter` deserializes as `None`; `None` omits the\n    /// key on serialize (byte-identical to old rows). `None` is a benign degrade —\n    /// resume falls back to the endpoint's current adapter.\n    // [impl->REQ-SESSION-ADAPTER-RECORDED]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub adapter: Option<String>,\n}\n\n/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable\n/// line is skipped, the rest still load (the degrade-don't-crash store stance).\npub fn read_all(perch: &Path) -> Vec<SessionEntry> {\n    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\n        return Vec::new();\n    };\n    s.lines()\n        .filter(|l| !l.trim().is_empty())\n        .filter_map(|l| serde_json::from_str(l).ok())\n        .collect()\n}\n\n/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\npub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\n    if k == 0 {\n        return Vec::new();\n    }\n    let all = read_all(perch);\n    let start = all.len().saturating_sub(k);\n    all[start..].to_vec()\n}\n\n/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\n/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\n/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\n/// `read_all` index (not the slice index) so it stays comparable across sessions.\n/// The absolute index of returned entry `i` is `(all.len() - returned.len()) + i`.\n// [impl->REQ-DIGEST-CURSOR]\npub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\n    if k == 0 {\n        return Vec::new();\n    }\n    let all = read_all(perch);\n    let start = all.len().saturating_sub(k);\n    all[start..]\n        .iter()\n        .enumerate()\n        .map(|(i, e)| {\n            let mut e = e.clone();\n            // Some from disk; else the absolute read_all index as the fallback.\n            e.ordinal = Some(e.ordinal.unwrap_or((start + i) as u64));\n            e\n        })\n        .collect()\n}\n\n/// Append `entry`, deduping an immediate repeat of the same `session_id` (a\n/// re-bind of the live session is not a new boundary) and bounding the ledger to\n/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten\n/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids\n/// partial-line corruption). Returns `true` if a row was written, `false` on a\n/// same-session dedup.\n// [impl->REQ-TERM-6]\npub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {\n    let mut entries = read_all(perch);\n    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {\n        return Ok(false); // same session re-bound — not a new boundary\n    }\n    // BACKFILL: stamp any pre-migration (`ordinal == None`) row with its concrete\n    // read_all index, so post-migration the ledger is fully ordinal-stamped and no\n    // survivor renumbers on a later prune (REQ-DIGEST-CURSOR).\n    for (i, e) in entries.iter_mut().enumerate() {\n        if e.ordinal.is_none() {\n            e.ordinal = Some(i as u64);\n        }\n    }\n    // The new row's ordinal is one past the highest existing (monotonic, persisted).\n    // With the backfill above every existing row is now Some, so `last()` carries\n    // the max; an empty ledger starts at 0.\n    let next = entries\n        .last()\n        .and_then(|e| e.ordinal)\n        .map(|o| o + 1)\n        .unwrap_or(0);\n    let mut entry = entry.clone();\n    entry.ordinal = Some(next);\n    entries.push(entry);\n    let start = entries.len().saturating_sub(MAX_LEDGER);\n    let body: String = entries[start..]\n        .iter()\n        .map(|e| serde_json::to_string(e).unwrap_or_default())\n        .map(|l| l + \"\\n\")\n        .collect();\n    std::fs::create_dir_all(perch)?;\n    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;\n    Ok(true)\n}\n\n[Showing lines 1-183 of 478. Use :184 to continue]"}],"details":{"truncation":{"content":"//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /\r\n//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/\r\n//! sessions.log`.\r\n//!\r\n//! A live agent that `/clear`s or `/compact`s keeps its perch identity but\r\n//! rotates its harness `session_id` (a new transcript file). The single\r\n//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*\r\n//! the thread but cannot *span* it. This ledger records each rotation so the\r\n//! digest can enumerate the **last K sessions** and bridge a boundary\r\n//! (ADR-0019 §thread-spanning).\r\n//!\r\n//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is\r\n//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew\r\n//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates\r\n//! the oldest rows out. A re-bind of the *same* session is not a new boundary and\r\n//! is deduped.\r\n// [impl->REQ-TERM-6]\r\n\r\nuse std::io;\r\nuse std::path::Path;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::atomic::atomic_write_string;\r\n\r\n/// The ledger filename under a perch.\r\npub const LEDGER_FILE: &str = \"sessions.log\";\r\n\r\n/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on\r\n/// append so the file can never grow without bound (risk #2).\r\npub const MAX_LEDGER: usize = 64;\r\n\r\n/// Why a session became active.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(rename_all = \"lowercase\")]\r\npub enum SessionTrigger {\r\n    /// First bind of the perch (`api listen` / revive).\r\n    Boot,\r\n    /// A `/clear` boundary (`api boundary clear`).\r\n    Clear,\r\n    /// A `/compact` boundary (`api boundary compact`).\r\n    Compact,\r\n}\r\n\r\nimpl SessionTrigger {\r\n    /// The lowercase token (matches the serde wire form) — used as the boundary\r\n    /// marker `kind` in the spanned digest.\r\n    pub fn token(self) -> &'static str {\r\n        match self {\r\n            SessionTrigger::Boot => \"boot\",\r\n            SessionTrigger::Clear => \"clear\",\r\n            SessionTrigger::Compact => \"compact\",\r\n        }\r\n    }\r\n}\r\n\r\n/// One ledger row: a `session_id` became active at `ts` via `trigger`.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct SessionEntry {\r\n    /// RFC3339-UTC instant (see [`crate::timefmt`]).\r\n    pub ts: String,\r\n    /// The harness session id that became active.\r\n    pub session_id: String,\r\n    /// What caused the rotation.\r\n    pub trigger: SessionTrigger,\r\n    /// The project working directory the session was active in (the W3\r\n    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a\r\n    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves\r\n    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history\r\n    /// (cross-project rows) restores the right dir from here.\r\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n    /// A **persisted, monotonic** ledger ordinal — the stable per-row sequence the\r\n    /// digest cursor anchors to (REQ-DIGEST-CURSOR). The ledger is bounded\r\n    /// ([`MAX_LEDGER`]): a `read_all` index renumbers survivors when the oldest row\r\n    /// prunes, which would re-number a consumer's cursor under it. A persisted\r\n    /// ordinal is stamped once at [`append`] and never moves, so a pruned survivor\r\n    /// keeps its number. **Additive serde-default**: a pre-migration row missing\r\n    /// `ordinal` deserializes as `None` and is backfilled to its concrete index on\r\n    /// the next append (so a migrated ledger is fully ordinal-stamped).\r\n    // [impl->REQ-DIGEST-CURSOR]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub ordinal: Option<u64>,\r\n    /// The adapter[:profile] the session ran under (D-2, REQ-SESSION-ADAPTER-RECORDED).\r\n    /// A resume-from-history restores THIS harness — the one that authored the\r\n    /// transcript — not merely the endpoint's *current* stamp (which a B-2\r\n    /// ChangeAdapter or a fork may have moved since). **Additive serde-default**: a\r\n    /// pre-migration row missing `adapter` deserializes as `None`; `None` omits the\r\n    /// key on serialize (byte-identical to old rows). `None` is a benign degrade —\r\n    /// resume falls back to the endpoint's current adapter.\r\n    // [impl->REQ-SESSION-ADAPTER-RECORDED]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub adapter: Option<String>,\r\n}\r\n\r\n/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable\r\n/// line is skipped, the rest still load (the degrade-don't-crash store stance).\r\npub fn read_all(perch: &Path) -> Vec<SessionEntry> {\r\n    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\r\n        return Vec::new();\r\n    };\r\n    s.lines()\r\n        .filter(|l| !l.trim().is_empty())\r\n        .filter_map(|l| serde_json::from_str(l).ok())\r\n        .collect()\r\n}\r\n\r\n/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\r\npub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..].to_vec()\r\n}\r\n\r\n/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\r\n/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\r\n/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\r\n/// `read_all` index (not the slice index) so it stays comparable across sessions.\r\n/// The absolute index of returned entry `i` is `(all.len() - returned.len()) + i`.\r\n// [impl->REQ-DIGEST-CURSOR]\r\npub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..]\r\n        .iter()\r\n        .enumerate()\r\n        .map(|(i, e)| {\r\n            let mut e = e.clone();\r\n            // Some from disk; else the absolute read_all index as the fallback.\r\n            e.ordinal = Some(e.ordinal.unwrap_or((start + i) as u64));\r\n            e\r\n        })\r\n        .collect()\r\n}\r\n\r\n/// Append `entry`, deduping an immediate repeat of the same `session_id` (a\r\n/// re-bind of the live session is not a new boundary) and bounding the ledger to\r\n/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten\r\n/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids\r\n/// partial-line corruption). Returns `true` if a row was written, `false` on a\r\n/// same-session dedup.\r\n// [impl->REQ-TERM-6]\r\npub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {\r\n    let mut entries = read_all(perch);\r\n    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {\r\n        return Ok(false); // same session re-bound — not a new boundary\r\n    }\r\n    // BACKFILL: stamp any pre-migration (`ordinal == None`) row with its concrete\r\n    // read_all index, so post-migration the ledger is fully ordinal-stamped and no\r\n    // survivor renumbers on a later prune (REQ-DIGEST-CURSOR).\r\n    for (i, e) in entries.iter_mut().enumerate() {\r\n        if e.ordinal.is_none() {\r\n            e.ordinal = Some(i as u64);\r\n        }\r\n    }\r\n    // The new row's ordinal is one past the highest existing (monotonic, persisted).\r\n    // With the backfill above every existing row is now Some, so `last()` carries\r\n    // the max; an empty ledger starts at 0.\r\n    let next = entries\r\n        .last()\r\n        .and_then(|e| e.ordinal)\r\n        .map(|o| o + 1)\r\n        .unwrap_or(0);\r\n    let mut entry = entry.clone();\r\n    entry.ordinal = Some(next);\r\n    entries.push(entry);\r\n    let start = entries.len().saturating_sub(MAX_LEDGER);\r\n    let body: String = entries[start..]\r\n        .iter()\r\n        .map(|e| serde_json::to_string(e).unwrap_or_default())\r\n        .map(|l| l + \"\\n\")\r\n        .collect();\r\n    std::fs::create_dir_all(perch)?;\r\n    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;\r\n    Ok(true)\r\n}\r","truncated":true,"truncatedBy":"lines","totalLines":478,"totalBytes":8207,"outputLines":183,"outputBytes":8207,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /\r\n//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/\r\n//! sessions.log`.\r\n//!\r\n//! A live agent that `/clear`s or `/compact`s keeps its perch identity but\r\n//! rotates its harness `session_id` (a new transcript file). The single\r\n//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*\r\n//! the thread but cannot *span* it. This ledger records each rotation so the\r\n//! digest can enumerate the **last K sessions** and bridge a boundary\r\n//! (ADR-0019 §thread-spanning).\r\n//!\r\n//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is\r\n//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew\r\n//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates\r\n//! the oldest rows out. A re-bind of the *same* session is not a new boundary and\r\n//! is deduped.\r\n// [impl->REQ-TERM-6]\r\n\r\nuse std::io;\r\nuse std::path::Path;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::atomic::atomic_write_string;\r\n\r\n/// The ledger filename under a perch.\r\npub const LEDGER_FILE: &str = \"sessions.log\";\r\n\r\n/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on\r\n/// append so the file can never grow without bound (risk #2).\r\npub const MAX_LEDGER: usize = 64;\r\n\r\n/// Why a session became active.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(rename_all = \"lowercase\")]\r\npub enum SessionTrigger {\r\n    /// First bind of the perch (`api listen` / revive).\r\n    Boot,\r\n    /// A `/clear` boundary (`api boundary clear`).\r\n    Clear,\r\n    /// A `/compact` boundary (`api boundary compact`).\r\n    Compact,\r\n}\r\n\r\nimpl SessionTrigger {\r\n    /// The lowercase token (matches the serde wire form) — used as the boundary\r\n    /// marker `kind` in the spanned digest.\r\n    pub fn token(self) -> &'static str {\r\n        match self {\r\n            SessionTrigger::Boot => \"boot\",\r\n            SessionTrigger::Clear => \"clear\",\r\n            SessionTrigger::Compact => \"compact\",\r\n        }\r\n    }\r\n}\r\n\r\n/// One ledger row: a `session_id` became active at `ts` via `trigger`.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct SessionEntry {\r\n    /// RFC3339-UTC instant (see [`crate::timefmt`]).\r\n    pub ts: String,\r\n    /// The harness session id that became active.\r\n    pub session_id: String,\r\n    /// What caused the rotation.\r\n    pub trigger: SessionTrigger,\r\n    /// The project working directory the session was active in (the W3\r\n    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a\r\n    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves\r\n    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history\r\n    /// (cross-project rows) restores the right dir from here.\r\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n    /// A **persisted, monotonic** ledger ordinal — the stable per-row sequence the\r\n    /// digest cursor anchors to (REQ-DIGEST-CURSOR). The ledger is bounded\r\n    /// ([`MAX_LEDGER`]): a `read_all` index renumbers survivors when the oldest row\r\n    /// prunes, which would re-number a consumer's cursor under it. A persisted\r\n    /// ordinal is stamped once at [`append`] and never moves, so a pruned survivor\r\n    /// keeps its number. **Additive serde-default**: a pre-migration row missing\r\n    /// `ordinal` deserializes as `None` and is backfilled to its concrete index on\r\n    /// the next append (so a migrated ledger is fully ordinal-stamped).\r\n    // [impl->REQ-DIGEST-CURSOR]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub ordinal: Option<u64>,\r\n    /// The adapter[:profile] the session ran under (D-2, REQ-SESSION-ADAPTER-RECORDED).\r\n    /// A resume-from-history restores THIS harness — the one that authored the\r\n    /// transcript — not merely the endpoint's *current* stamp (which a B-2\r\n    /// ChangeAdapter or a fork may have moved since). **Additive serde-default**: a\r\n    /// pre-migration row missing `adapter` deserializes as `None`; `None` omits the\r\n    /// key on serialize (byte-identical to old rows). `None` is a benign degrade —\r\n    /// resume falls back to the endpoint's current adapter.\r\n    // [impl->REQ-SESSION-ADAPTER-RECORDED]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub adapter: Option<String>,\r\n}\r\n\r\n/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable\r\n/// line is skipped, the rest still load (the degrade-don't-crash store stance).\r\npub fn read_all(perch: &Path) -> Vec<SessionEntry> {\r\n    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\r\n        return Vec::new();\r\n    };\r\n    s.lines()\r\n        .filter(|l| !l.trim().is_empty())\r\n        .filter_map(|l| serde_json::from_str(l).ok())\r\n        .collect()\r\n}\r\n\r\n/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\r\npub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..].to_vec()\r\n}\r\n\r\n/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\r\n/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\r\n/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\r\n/// `read_all` index (not the slice index) so it stays comparable across sessions.\r\n/// The absolute index of returned entry `i` is `(all.len() - returned.len()) + i`.\r\n// [impl->REQ-DIGEST-CURSOR]\r\npub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..]\r\n        .iter()\r\n        .enumerate()\r\n        .map(|(i, e)| {\r\n            let mut e = e.clone();\r\n            // Some from disk; else the absolute read_all index as the fallback.\r\n            e.ordinal = Some(e.ordinal.unwrap_or((start + i) as u64));\r\n            e\r\n        })\r\n        .collect()\r\n}\r\n\r\n/// Append `entry`, deduping an immediate repeat of the same `session_id` (a\r\n/// re-bind of the live session is not a new boundary) and bounding the ledger to\r\n/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten\r\n/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids\r\n/// partial-line corruption). Returns `true` if a row was written, `false` on a\r\n/// same-session dedup.\r\n// [impl->REQ-TERM-6]\r\npub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {\r\n    let mut entries = read_all(perch);\r\n    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {\r\n        return Ok(false); // same session re-bound — not a new boundary\r\n    }\r\n    // BACKFILL: stamp any pre-migration (`ordinal == None`) row with its concrete\r\n    // read_all index, so post-migration the ledger is fully ordinal-stamped and no\r\n    // survivor renumbers on a later prune (REQ-DIGEST-CURSOR).\r\n    for (i, e) in entries.iter_mut().enumerate() {\r\n        if e.ordinal.is_none() {\r\n            e.ordinal = Some(i as u64);\r\n        }\r\n    }\r\n    // The new row's ordinal is one past the highest existing (monotonic, persisted).\r\n    // With the backfill above every existing row is now Some, so `last()` carries\r\n    // the max; an empty ledger starts at 0.\r\n    let next = entries\r\n        .last()\r\n        .and_then(|e| e.ordinal)\r\n        .map(|o| o + 1)\r\n        .unwrap_or(0);\r\n    let mut entry = entry.clone();\r\n    entry.ordinal = Some(next);\r\n    entries.push(entry);\r\n    let start = entries.len().saturating_sub(MAX_LEDGER);\r\n    let body: String = entries[start..]\r\n        .iter()\r\n        .map(|e| serde_json::to_string(e).unwrap_or_default())\r\n        .map(|l| l + \"\\n\")\r\n        .collect();\r\n    std::fs::create_dir_all(perch)?;\r\n    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;\r\n    Ok(true)\r\n}\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\sessions.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":478,"totalBytes":8207,"outputLines":183,"outputBytes":8207,"shownRange":{"start":1,"end":183},"nextOffset":184}}},"isError":false,"timestamp":1784271495587}}
{"type":"message","id":"3f817c46","parentId":"8aeb82e1","timestamp":"2026-07-17T06:58:15.607Z","message":{"role":"toolResult","toolCallId":"call_yO4rphDc4BMNY3yxn0ZA2EK3|fc_038f28d3e93bbfd4016a59d28772248196abf0babfd40477e5","toolName":"read","content":[{"type":"text","text":"\n    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\n    /// or `None` when there is no pinned install dir / the file can't be read.\n    /// The W3c reload-on-dirty change detector. (W3c)\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    fn manifest_disk_hash(&self) -> Option<String> {\n        let dir = self.install_dir.as_ref()?;\n        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n        Some(crate::release::sha256_hex(&bytes))\n    }\n\n    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\n    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\n    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\n    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\n    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\n    /// child) are SEPARATE PROCESSES with no shared memory, so the apply cannot\n    /// push an `Arc` handle across — the brain PULLS the change from disk here\n    /// (corrects ADR-0025 step 3 + the W3 verdict's \"broker registers the brain's\n    /// handle\" caution). Re-parses the SAME bytes it hashed; W3b's atomic rename\n    /// guarantees a whole old-or-new file (never a torn read). A transient\n    /// unparseable manifest leaves the cell on the last-good one and retries next\n    /// pulse (does not advance `last`), and never kills the loop. (W3c, ADR-0025)\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    fn reload_manifest_if_changed(&self, last: &mut Option<String>) {\n        let Some(dir) = self.install_dir.as_ref() else {\n            return;\n        };\n        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n            return;\n        };\n        let hash = crate::release::sha256_hex(&bytes);\n        if last.as_deref() == Some(hash.as_str()) {\n            return;\n        }\n        match std::str::from_utf8(&bytes)\n            .ok()\n            .and_then(|s| Manifest::from_toml_str(s).ok())\n        {\n            Some(new) => {\n                self.cell.refresh(new, Some(dir));\n                *last = Some(hash);\n            }\n            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\n        }\n    }\n\n    /// The configured pulse period (config-driven, not a constant).\n    pub fn pulse_period(&self) -> Duration {\n        self.cfg.pulse_period\n    }\n\n    /// Base substitution keys spt-core fills for the lifecycle templates.\n    fn base_keys(&self, session_id: Option<&str>) -> BTreeMap<String, String> {\n        let mut k = BTreeMap::new();\n        k.insert(\"id\".to_string(), self.id.clone());\n        if let Some(sid) = session_id {\n            k.insert(\"session_id\".to_string(), sid.to_string());\n        }\n        // `{node}` (REQ-MANIFEST-NODE-KEY): the startup-resolved advertised label,\n        // inserted ONLY when known — absent → a `{node}` reference fails LOUD, never\n        // an empty token.\n        if let Some(node) = self.node_label.as_deref().filter(|s| !s.is_empty()) {\n            k.insert(\"node\".to_string(), node.to_string());\n        }\n        // `{subnet}` (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): the endpoint's\n        // home-subnet label (`own_subnet` — the parent info.json `home_subnet`, `local`\n        // when unhomed). Inserted ONLY when non-empty — the SAME insert-when-known,\n        // loud-if-absent contract as `{node}` (a referenced-but-absent key fails LOUD,\n        // never an empty token). `own_subnet` is TOTAL (`local` fallback), so this is\n        // always present in practice; the guard preserves the loud-absent invariant for\n        // any future caller that builds keys without it. NO `{home}` key (doyle W4 Q1:\n        // `home:` is already the subnet-name qualifier position — a `{home}` key would\n        // invite meaning-drift).\n        let subnet = self.own_subnet();\n        if !subnet.is_empty() {\n            k.insert(\"subnet\".to_string(), subnet);\n        }\n        k\n    }\n\n    /// The unique watched drop dirs (commune + signoff; usually the same dir).\n    fn drop_dirs(&self) -> Vec<PathBuf> {\n        let mut dirs: Vec<PathBuf> = Vec::new();\n        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\n            .into_iter()\n            .flatten()\n        {\n            if !dirs.contains(d) {\n                dirs.push(d.clone());\n            }\n        }\n        dirs\n    }\n\n    /// Sweep a stale signoff sentinel on start (3.2). No-op without a signoff dir.\n    pub fn sweep_on_start(&self) {\n        if let Some(dir) = &self.signoff_dir {\n            let _ = sweep_stale_signoff(dir, &self.id);\n        }\n    }\n\n    /// Mark a daemon-hosted perch `offline` — the teardown counterpart of the\n    /// `online` stamp a daemon-hosted perch carries (a hosted psyche perch, an\n    /// spt-hosted Self). From this point the single liveness resolver reports the\n    /// perch dead regardless of any pid still recorded in its `info.json` (2.5).\n    /// Best-effort: a missing record (perch already gone) is not an error.\n    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n    pub fn mark_offline(perch_path: &std::path::Path) {\n        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);\n    }\n\n    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the\n    /// signal the consent gate resolves \"most-recently-active live session\"\n    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted\n    /// perch that has not been bound yet (no `info.json`) is silently skipped, so\n    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.\n    // [impl->REQ-UPD-4]\n    pub fn touch_active(&self) {\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let _ = spt_store::info::set_last_active(&perch_path, now_ms());\n    }\n\n    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n    /// echo-commune (T3).\n    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n        let now = now_ms();\n        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n        // `last_active_ms` — a resting seat ticking its pulse must never win\n        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n        // the notif first-fire). A dormant seat instead checks the opt-in\n        // auto-suspend threshold (counted from its dormancy-onset anchor);\n        // a suspended seat just keeps ingesting.\n        // [impl->REQ-INST-3]\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        match crate::resting::read_rest(&perch_path).map(|r| r.state) {\n            None | Some(crate::resting::RestState::Active) => self.touch_active(),\n            Some(crate::resting::RestState::Dormant) => {\n                let _ = self.rest_event(\n                    session_id,\n                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },\n                );\n            }\n            Some(crate::resting::RestState::Suspended) => {}\n        }\n        // Resolve drop dirs + project routing AT INGEST TIME against the endpoint's\n        // FRESH info.cwd (REQ-STORE-CONTEXT-BRANCH-FILL). A relative manifest drop dir\n        // (claude-spt's `.claude`) resolves against the endpoint's cwd — NEVER the\n        // daemon's process cwd (the pre-fix bug that left the two-tier store pristine).\n        // The project tier routes under the cwd-derived project_id, but an owlery-internal\n        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\n        // the live/agent slice still commits. A relative dir with no recorded cwd is\n        // SKIPPED loud-once, never guessed.\n        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\n        let owlery = perch::owlery_dir();\n        let project_id = match endpoint_cwd.as_deref() {\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\n                spt_store::project::project_id_for_dir(Path::new(cwd))\n            }\n            _ => String::new(),\n        };\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\n        let mut ingested = Vec::new();\n        for raw in self.drop_dirs() {\n            match resolve_endpoint_drop_dir(&raw, cwd_path) {\n                Some(dir) => {\n                    let mut got =\n                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\n                            .map_err(|e| e.to_string())?;\n                    ingested.append(&mut got);\n                }\n                None => warn_no_cwd_once(&self.id, &raw),\n            }\n        }\n        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\n        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\n        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\n        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\n        // below can never void an already-relayed turn. The commune-sync is NON-\n        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\n        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\n        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\n        // the composed mind rides the W2 reseed path, never every pulse.\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n        let (echo_fired, turn_outcome) =\n            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\n                let turn_res =\n                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\n                let commune_res = self.fire_echo(session_id);\n                if let Err(e) = &commune_res {\n                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\n                }\n                // A strike if EITHER leg failed; the reason names the failing leg.\n                // The turn's relay already happened above regardless of the commune.\n                let outcome = match (turn_res, commune_res) {\n                    (Ok(()), Ok(())) => Ok(()),\n                    (Err(e), _) => Err(format!(\"turn: {e}\")),\n                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\n                };\n                (true, Some(outcome))\n            } else {\n                (false, None)\n            };\n        Ok(TickReport {\n            ingested,\n            echo_fired,\n            turn_outcome,\n        })\n    }\n\n    /// Run the scheduled pulse loop until `stop` is set, calling `on_tick` after\n    /// each tick (so a caller — or a test — observes cadence and can decide to\n    /// stop). The period comes from the durable config, never a constant.\n    ///\n    /// **Restoration D5-2 (ADR-0018 Q4/V4): the cadence is a disk-anchored\n    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n    /// restart instant on every restart. Now the loop rehydrates a\n    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\n    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\n    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\n    /// deadline is derived functionally — **no per-fire write**.\n    ///\n    /// The loop still holds no work-state beyond the cursor on disk (a fresh loop\n    /// re-reads the drop dirs and gate), so it is freely killable + respawnable;\n    /// D5 makes its *phase* durable too.\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-1]\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n        &self,\n        session_id: Option<&str>,\n        stop: &AtomicBool,\n        reason: StartReason,\n        mut on_tick: F,\n    ) {\n        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\n        // cross-clobber. `None` (the single daemon pulse) → the \"pulse\" key.\n        let key = session_id.unwrap_or(\"pulse\");\n        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\n        // A disk error must not stop the pulse: degrade to an in-memory anchor at\n        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\n        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\n            .unwrap_or(DeadlineAnchor {\n                anchor_ms: now_ms(),\n                interval_ms: interval_ms.max(1),\n            });\n        // The next fire at/after now. A fresh anchor (cold/crash) sits on-grid at\n        // `now` → fires immediately (the old loop's tick-first behavior); an\n        // update anchor lands on the next grid point (phase preserved).\n        let mut deadline = anchor.next_fire(now_ms());\n        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n        // the initial state.\n        let mut manifest_hash = self.manifest_disk_hash();\n        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n        // in the loop's local state — the loop is per-endpoint + single-threaded, so\n        // this is the simplest correct home (no lock, no cross-endpoint bleed).\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n        let mut turn_strikes: u32 = 0;\n        while !stop.load(Ordering::Relaxed) {\n            let now = now_ms();\n            if now < deadline {\n                // Sleep toward the deadline in small slices so a stop is honored\n                // promptly rather than after a full (possibly multi-second) wait.\n                sleep_interruptible(Duration::from_millis(deadline - now), stop);\n                continue;\n            }\n            // Pull any live-update manifest swap from disk before the tick so the\n            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\n            self.reload_manifest_if_changed(&mut manifest_hash);\n            match self.pulse_tick(session_id) {\n                Ok(report) => {\n                    // F-030 W1: feed a fired turn's outcome to the failure budget.\n                    if let Some(outcome) = &report.turn_outcome {\n                        self.note_turn_outcome(&mut turn_strikes, outcome);\n                    }\n                    on_tick(&report);\n                }\n                Err(_) => { /* a transient tick error must not kill the loop */ }\n            }\n…\n        }\n    }\n\n[Showing lines 459-743 of 2890. Use :744 to continue]"}],"details":{"truncation":{"content":"\r\n    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\r\n    /// or `None` when there is no pinned install dir / the file can't be read.\r\n    /// The W3c reload-on-dirty change detector. (W3c)\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    fn manifest_disk_hash(&self) -> Option<String> {\r\n        let dir = self.install_dir.as_ref()?;\r\n        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\r\n        Some(crate::release::sha256_hex(&bytes))\r\n    }\r\n\r\n    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\r\n    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\r\n    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\r\n    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\r\n    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\r\n    /// child) are SEPARATE PROCESSES with no shared memory, so the apply cannot\r\n    /// push an `Arc` handle across — the brain PULLS the change from disk here\r\n    /// (corrects ADR-0025 step 3 + the W3 verdict's \"broker registers the brain's\r\n    /// handle\" caution). Re-parses the SAME bytes it hashed; W3b's atomic rename\r\n    /// guarantees a whole old-or-new file (never a torn read). A transient\r\n    /// unparseable manifest leaves the cell on the last-good one and retries next\r\n    /// pulse (does not advance `last`), and never kills the loop. (W3c, ADR-0025)\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    fn reload_manifest_if_changed(&self, last: &mut Option<String>) {\r\n        let Some(dir) = self.install_dir.as_ref() else {\r\n            return;\r\n        };\r\n        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\r\n            return;\r\n        };\r\n        let hash = crate::release::sha256_hex(&bytes);\r\n        if last.as_deref() == Some(hash.as_str()) {\r\n            return;\r\n        }\r\n        match std::str::from_utf8(&bytes)\r\n            .ok()\r\n            .and_then(|s| Manifest::from_toml_str(s).ok())\r\n        {\r\n            Some(new) => {\r\n                self.cell.refresh(new, Some(dir));\r\n                *last = Some(hash);\r\n            }\r\n            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\r\n        }\r\n    }\r\n\r\n    /// The configured pulse period (config-driven, not a constant).\r\n    pub fn pulse_period(&self) -> Duration {\r\n        self.cfg.pulse_period\r\n    }\r\n\r\n    /// Base substitution keys spt-core fills for the lifecycle templates.\r\n    fn base_keys(&self, session_id: Option<&str>) -> BTreeMap<String, String> {\r\n        let mut k = BTreeMap::new();\r\n        k.insert(\"id\".to_string(), self.id.clone());\r\n        if let Some(sid) = session_id {\r\n            k.insert(\"session_id\".to_string(), sid.to_string());\r\n        }\r\n        // `{node}` (REQ-MANIFEST-NODE-KEY): the startup-resolved advertised label,\r\n        // inserted ONLY when known — absent → a `{node}` reference fails LOUD, never\r\n        // an empty token.\r\n        if let Some(node) = self.node_label.as_deref().filter(|s| !s.is_empty()) {\r\n            k.insert(\"node\".to_string(), node.to_string());\r\n        }\r\n        // `{subnet}` (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): the endpoint's\r\n        // home-subnet label (`own_subnet` — the parent info.json `home_subnet`, `local`\r\n        // when unhomed). Inserted ONLY when non-empty — the SAME insert-when-known,\r\n        // loud-if-absent contract as `{node}` (a referenced-but-absent key fails LOUD,\r\n        // never an empty token). `own_subnet` is TOTAL (`local` fallback), so this is\r\n        // always present in practice; the guard preserves the loud-absent invariant for\r\n        // any future caller that builds keys without it. NO `{home}` key (doyle W4 Q1:\r\n        // `home:` is already the subnet-name qualifier position — a `{home}` key would\r\n        // invite meaning-drift).\r\n        let subnet = self.own_subnet();\r\n        if !subnet.is_empty() {\r\n            k.insert(\"subnet\".to_string(), subnet);\r\n        }\r\n        k\r\n    }\r\n\r\n    /// The unique watched drop dirs (commune + signoff; usually the same dir).\r\n    fn drop_dirs(&self) -> Vec<PathBuf> {\r\n        let mut dirs: Vec<PathBuf> = Vec::new();\r\n        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\r\n            .into_iter()\r\n            .flatten()\r\n        {\r\n            if !dirs.contains(d) {\r\n                dirs.push(d.clone());\r\n            }\r\n        }\r\n        dirs\r\n    }\r\n\r\n    /// Sweep a stale signoff sentinel on start (3.2). No-op without a signoff dir.\r\n    pub fn sweep_on_start(&self) {\r\n        if let Some(dir) = &self.signoff_dir {\r\n            let _ = sweep_stale_signoff(dir, &self.id);\r\n        }\r\n    }\r\n\r\n    /// Mark a daemon-hosted perch `offline` — the teardown counterpart of the\r\n    /// `online` stamp a daemon-hosted perch carries (a hosted psyche perch, an\r\n    /// spt-hosted Self). From this point the single liveness resolver reports the\r\n    /// perch dead regardless of any pid still recorded in its `info.json` (2.5).\r\n    /// Best-effort: a missing record (perch already gone) is not an error.\r\n    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n    pub fn mark_offline(perch_path: &std::path::Path) {\r\n        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);\r\n    }\r\n\r\n    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the\r\n    /// signal the consent gate resolves \"most-recently-active live session\"\r\n    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted\r\n    /// perch that has not been bound yet (no `info.json`) is silently skipped, so\r\n    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.\r\n    // [impl->REQ-UPD-4]\r\n    pub fn touch_active(&self) {\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        let _ = spt_store::info::set_last_active(&perch_path, now_ms());\r\n    }\r\n\r\n    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\r\n    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\r\n    /// echo-commune (T3).\r\n    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\r\n        let now = now_ms();\r\n        // Recency follows the resting model (D9-2 — the D8 gap closed): only\r\n        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\r\n        // `last_active_ms` — a resting seat ticking its pulse must never win\r\n        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\r\n        // the notif first-fire). A dormant seat instead checks the opt-in\r\n        // auto-suspend threshold (counted from its dormancy-onset anchor);\r\n        // a suspended seat just keeps ingesting.\r\n        // [impl->REQ-INST-3]\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        match crate::resting::read_rest(&perch_path).map(|r| r.state) {\r\n            None | Some(crate::resting::RestState::Active) => self.touch_active(),\r\n            Some(crate::resting::RestState::Dormant) => {\r\n                let _ = self.rest_event(\r\n                    session_id,\r\n                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },\r\n                );\r\n            }\r\n            Some(crate::resting::RestState::Suspended) => {}\r\n        }\r\n        // Resolve drop dirs + project routing AT INGEST TIME against the endpoint's\r\n        // FRESH info.cwd (REQ-STORE-CONTEXT-BRANCH-FILL). A relative manifest drop dir\r\n        // (claude-spt's `.claude`) resolves against the endpoint's cwd — NEVER the\r\n        // daemon's process cwd (the pre-fix bug that left the two-tier store pristine).\r\n        // The project tier routes under the cwd-derived project_id, but an owlery-internal\r\n        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\r\n        // the live/agent slice still commits. A relative dir with no recorded cwd is\r\n        // SKIPPED loud-once, never guessed.\r\n        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\r\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\r\n        let owlery = perch::owlery_dir();\r\n        let project_id = match endpoint_cwd.as_deref() {\r\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\r\n                spt_store::project::project_id_for_dir(Path::new(cwd))\r\n            }\r\n            _ => String::new(),\r\n        };\r\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\r\n        let mut ingested = Vec::new();\r\n        for raw in self.drop_dirs() {\r\n            match resolve_endpoint_drop_dir(&raw, cwd_path) {\r\n                Some(dir) => {\r\n                    let mut got =\r\n                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\r\n                            .map_err(|e| e.to_string())?;\r\n                    ingested.append(&mut got);\r\n                }\r\n                None => warn_no_cwd_once(&self.id, &raw),\r\n            }\r\n        }\r\n        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\r\n        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\r\n        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\r\n        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\r\n        // below can never void an already-relayed turn. The commune-sync is NON-\r\n        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\r\n        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\r\n        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\r\n        // the composed mind rides the W2 reseed path, never every pulse.\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let (echo_fired, turn_outcome) =\r\n            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\r\n                let turn_res =\r\n                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\r\n                let commune_res = self.fire_echo(session_id);\r\n                if let Err(e) = &commune_res {\r\n                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\r\n                }\r\n                // A strike if EITHER leg failed; the reason names the failing leg.\r\n                // The turn's relay already happened above regardless of the commune.\r\n                let outcome = match (turn_res, commune_res) {\r\n                    (Ok(()), Ok(())) => Ok(()),\r\n                    (Err(e), _) => Err(format!(\"turn: {e}\")),\r\n                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\r\n                };\r\n                (true, Some(outcome))\r\n            } else {\r\n                (false, None)\r\n            };\r\n        Ok(TickReport {\r\n            ingested,\r\n            echo_fired,\r\n            turn_outcome,\r\n        })\r\n    }\r\n\r\n    /// Run the scheduled pulse loop until `stop` is set, calling `on_tick` after\r\n    /// each tick (so a caller — or a test — observes cadence and can decide to\r\n    /// stop). The period comes from the durable config, never a constant.\r\n    ///\r\n    /// **Restoration D5-2 (ADR-0018 Q4/V4): the cadence is a disk-anchored\r\n    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\r\n    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\r\n    /// restart instant on every restart. Now the loop rehydrates a\r\n    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\r\n    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\r\n    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\r\n    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\r\n    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\r\n    /// deadline is derived functionally — **no per-fire write**.\r\n    ///\r\n    /// The loop still holds no work-state beyond the cursor on disk (a fresh loop\r\n    /// re-reads the drop dirs and gate), so it is freely killable + respawnable;\r\n    /// D5 makes its *phase* durable too.\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-1]\r\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\r\n        &self,\r\n        session_id: Option<&str>,\r\n        stop: &AtomicBool,\r\n        reason: StartReason,\r\n        mut on_tick: F,\r\n    ) {\r\n        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\r\n        // cross-clobber. `None` (the single daemon pulse) → the \"pulse\" key.\r\n        let key = session_id.unwrap_or(\"pulse\");\r\n        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\r\n        // A disk error must not stop the pulse: degrade to an in-memory anchor at\r\n        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\r\n        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\r\n            .unwrap_or(DeadlineAnchor {\r\n                anchor_ms: now_ms(),\r\n                interval_ms: interval_ms.max(1),\r\n            });\r\n        // The next fire at/after now. A fresh anchor (cold/crash) sits on-grid at\r\n        // `now` → fires immediately (the old loop's tick-first behavior); an\r\n        // update anchor lands on the next grid point (phase preserved).\r\n        let mut deadline = anchor.next_fire(now_ms());\r\n        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\r\n        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\r\n        // the initial state.\r\n        let mut manifest_hash = self.manifest_disk_hash();\r\n        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\r\n        // in the loop's local state — the loop is per-endpoint + single-threaded, so\r\n        // this is the simplest correct home (no lock, no cross-endpoint bleed).\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let mut turn_strikes: u32 = 0;\r\n        while !stop.load(Ordering::Relaxed) {\r\n            let now = now_ms();\r\n            if now < deadline {\r\n                // Sleep toward the deadline in small slices so a stop is honored\r\n                // promptly rather than after a full (possibly multi-second) wait.\r\n                sleep_interruptible(Duration::from_millis(deadline - now), stop);\r\n                continue;\r\n            }\r\n            // Pull any live-update manifest swap from disk before the tick so the\r\n            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\r\n            self.reload_manifest_if_changed(&mut manifest_hash);\r\n            match self.pulse_tick(session_id) {\r\n                Ok(report) => {\r\n                    // F-030 W1: feed a fired turn's outcome to the failure budget.\r\n                    if let Some(outcome) = &report.turn_outcome {\r\n                        self.note_turn_outcome(&mut turn_strikes, outcome);\r\n                    }\r\n                    on_tick(&report);\r\n                }\r\n                Err(_) => { /* a transient tick error must not kill the loop */ }\r\n            }\r","truncated":true,"truncatedBy":"lines","totalLines":2432,"totalBytes":15296,"outputLines":285,"outputBytes":15296,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),\r\n    /// or `None` when there is no pinned install dir / the file can't be read.\r\n    /// The W3c reload-on-dirty change detector. (W3c)\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    fn manifest_disk_hash(&self) -> Option<String> {\r\n        let dir = self.install_dir.as_ref()?;\r\n        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\r\n        Some(crate::release::sha256_hex(&bytes))\r\n    }\r\n\r\n    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,\r\n    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in\r\n    /// place — so a daemon-coordinated adapter update (W3d) that swapped the\r\n    /// install dir takes effect within one pulse, WITHOUT restarting the brain or\r\n    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`\r\n    /// child) are SEPARATE PROCESSES with no shared memory, so the apply cannot\r\n    /// push an `Arc` handle across — the brain PULLS the change from disk here\r\n    /// (corrects ADR-0025 step 3 + the W3 verdict's \"broker registers the brain's\r\n    /// handle\" caution). Re-parses the SAME bytes it hashed; W3b's atomic rename\r\n    /// guarantees a whole old-or-new file (never a torn read). A transient\r\n    /// unparseable manifest leaves the cell on the last-good one and retries next\r\n    /// pulse (does not advance `last`), and never kills the loop. (W3c, ADR-0025)\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    fn reload_manifest_if_changed(&self, last: &mut Option<String>) {\r\n        let Some(dir) = self.install_dir.as_ref() else {\r\n            return;\r\n        };\r\n        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\r\n            return;\r\n        };\r\n        let hash = crate::release::sha256_hex(&bytes);\r\n        if last.as_deref() == Some(hash.as_str()) {\r\n            return;\r\n        }\r\n        match std::str::from_utf8(&bytes)\r\n            .ok()\r\n            .and_then(|s| Manifest::from_toml_str(s).ok())\r\n        {\r\n            Some(new) => {\r\n                self.cell.refresh(new, Some(dir));\r\n                *last = Some(hash);\r\n            }\r\n            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }\r\n        }\r\n    }\r\n\r\n    /// The configured pulse period (config-driven, not a constant).\r\n    pub fn pulse_period(&self) -> Duration {\r\n        self.cfg.pulse_period\r\n    }\r\n\r\n    /// Base substitution keys spt-core fills for the lifecycle templates.\r\n    fn base_keys(&self, session_id: Option<&str>) -> BTreeMap<String, String> {\r\n        let mut k = BTreeMap::new();\r\n        k.insert(\"id\".to_string(), self.id.clone());\r\n        if let Some(sid) = session_id {\r\n            k.insert(\"session_id\".to_string(), sid.to_string());\r\n        }\r\n        // `{node}` (REQ-MANIFEST-NODE-KEY): the startup-resolved advertised label,\r\n        // inserted ONLY when known — absent → a `{node}` reference fails LOUD, never\r\n        // an empty token.\r\n        if let Some(node) = self.node_label.as_deref().filter(|s| !s.is_empty()) {\r\n            k.insert(\"node\".to_string(), node.to_string());\r\n        }\r\n        // `{subnet}` (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): the endpoint's\r\n        // home-subnet label (`own_subnet` — the parent info.json `home_subnet`, `local`\r\n        // when unhomed). Inserted ONLY when non-empty — the SAME insert-when-known,\r\n        // loud-if-absent contract as `{node}` (a referenced-but-absent key fails LOUD,\r\n        // never an empty token). `own_subnet` is TOTAL (`local` fallback), so this is\r\n        // always present in practice; the guard preserves the loud-absent invariant for\r\n        // any future caller that builds keys without it. NO `{home}` key (doyle W4 Q1:\r\n        // `home:` is already the subnet-name qualifier position — a `{home}` key would\r\n        // invite meaning-drift).\r\n        let subnet = self.own_subnet();\r\n        if !subnet.is_empty() {\r\n            k.insert(\"subnet\".to_string(), subnet);\r\n        }\r\n        k\r\n    }\r\n\r\n    /// The unique watched drop dirs (commune + signoff; usually the same dir).\r\n    fn drop_dirs(&self) -> Vec<PathBuf> {\r\n        let mut dirs: Vec<PathBuf> = Vec::new();\r\n        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\r\n            .into_iter()\r\n            .flatten()\r\n        {\r\n            if !dirs.contains(d) {\r\n                dirs.push(d.clone());\r\n            }\r\n        }\r\n        dirs\r\n    }\r\n\r\n    /// Sweep a stale signoff sentinel on start (3.2). No-op without a signoff dir.\r\n    pub fn sweep_on_start(&self) {\r\n        if let Some(dir) = &self.signoff_dir {\r\n            let _ = sweep_stale_signoff(dir, &self.id);\r\n        }\r\n    }\r\n\r\n    /// Mark a daemon-hosted perch `offline` — the teardown counterpart of the\r\n    /// `online` stamp a daemon-hosted perch carries (a hosted psyche perch, an\r\n    /// spt-hosted Self). From this point the single liveness resolver reports the\r\n    /// perch dead regardless of any pid still recorded in its `info.json` (2.5).\r\n    /// Best-effort: a missing record (perch already gone) is not an error.\r\n    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n    pub fn mark_offline(perch_path: &std::path::Path) {\r\n        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);\r\n    }\r\n\r\n    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the\r\n    /// signal the consent gate resolves \"most-recently-active live session\"\r\n    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted\r\n    /// perch that has not been bound yet (no `info.json`) is silently skipped, so\r\n    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.\r\n    // [impl->REQ-UPD-4]\r\n    pub fn touch_active(&self) {\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        let _ = spt_store::info::set_last_active(&perch_path, now_ms());\r\n    }\r\n\r\n    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\r\n    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\r\n    /// echo-commune (T3).\r\n    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\r\n        let now = now_ms();\r\n        // Recency follows the resting model (D9-2 — the D8 gap closed): only\r\n        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\r\n        // `last_active_ms` — a resting seat ticking its pulse must never win\r\n        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\r\n        // the notif first-fire). A dormant seat instead checks the opt-in\r\n        // auto-suspend threshold (counted from its dormancy-onset anchor);\r\n        // a suspended seat just keeps ingesting.\r\n        // [impl->REQ-INST-3]\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        match crate::resting::read_rest(&perch_path).map(|r| r.state) {\r\n            None | Some(crate::resting::RestState::Active) => self.touch_active(),\r\n            Some(crate::resting::RestState::Dormant) => {\r\n                let _ = self.rest_event(\r\n                    session_id,\r\n                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },\r\n                );\r\n            }\r\n            Some(crate::resting::RestState::Suspended) => {}\r\n        }\r\n        // Resolve drop dirs + project routing AT INGEST TIME against the endpoint's\r\n        // FRESH info.cwd (REQ-STORE-CONTEXT-BRANCH-FILL). A relative manifest drop dir\r\n        // (claude-spt's `.claude`) resolves against the endpoint's cwd — NEVER the\r\n        // daemon's process cwd (the pre-fix bug that left the two-tier store pristine).\r\n        // The project tier routes under the cwd-derived project_id, but an owlery-internal\r\n        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\r\n        // the live/agent slice still commits. A relative dir with no recorded cwd is\r\n        // SKIPPED loud-once, never guessed.\r\n        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\r\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\r\n        let owlery = perch::owlery_dir();\r\n        let project_id = match endpoint_cwd.as_deref() {\r\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\r\n                spt_store::project::project_id_for_dir(Path::new(cwd))\r\n            }\r\n            _ => String::new(),\r\n        };\r\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\r\n        let mut ingested = Vec::new();\r\n        for raw in self.drop_dirs() {\r\n            match resolve_endpoint_drop_dir(&raw, cwd_path) {\r\n                Some(dir) => {\r\n                    let mut got =\r\n                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\r\n                            .map_err(|e| e.to_string())?;\r\n                    ingested.append(&mut got);\r\n                }\r\n                None => warn_no_cwd_once(&self.id, &raw),\r\n            }\r\n        }\r\n        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\r\n        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\r\n        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\r\n        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\r\n        // below can never void an already-relayed turn. The commune-sync is NON-\r\n        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\r\n        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\r\n        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\r\n        // the composed mind rides the W2 reseed path, never every pulse.\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let (echo_fired, turn_outcome) =\r\n            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\r\n                let turn_res =\r\n                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\r\n                let commune_res = self.fire_echo(session_id);\r\n                if let Err(e) = &commune_res {\r\n                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\r\n                }\r\n                // A strike if EITHER leg failed; the reason names the failing leg.\r\n                // The turn's relay already happened above regardless of the commune.\r\n                let outcome = match (turn_res, commune_res) {\r\n                    (Ok(()), Ok(())) => Ok(()),\r\n                    (Err(e), _) => Err(format!(\"turn: {e}\")),\r\n                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\r\n                };\r\n                (true, Some(outcome))\r\n            } else {\r\n                (false, None)\r\n            };\r\n        Ok(TickReport {\r\n            ingested,\r\n            echo_fired,\r\n            turn_outcome,\r\n        })\r\n    }\r\n\r\n    /// Run the scheduled pulse loop until `stop` is set, calling `on_tick` after\r\n    /// each tick (so a caller — or a test — observes cadence and can decide to\r\n    /// stop). The period comes from the durable config, never a constant.\r\n    ///\r\n    /// **Restoration D5-2 (ADR-0018 Q4/V4): the cadence is a disk-anchored\r\n    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\r\n    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\r\n    /// restart instant on every restart. Now the loop rehydrates a\r\n    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\r\n    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\r\n    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\r\n    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\r\n    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\r\n    /// deadline is derived functionally — **no per-fire write**.\r\n    ///\r\n    /// The loop still holds no work-state beyond the cursor on disk (a fresh loop\r\n    /// re-reads the drop dirs and gate), so it is freely killable + respawnable;\r\n    /// D5 makes its *phase* durable too.\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-1]\r\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\r\n        &self,\r\n        session_id: Option<&str>,\r\n        stop: &AtomicBool,\r\n        reason: StartReason,\r\n        mut on_tick: F,\r\n    ) {\r\n        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\r\n        // cross-clobber. `None` (the single daemon pulse) → the \"pulse\" key.\r\n        let key = session_id.unwrap_or(\"pulse\");\r\n        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\r\n        // A disk error must not stop the pulse: degrade to an in-memory anchor at\r\n        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\r\n        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\r\n            .unwrap_or(DeadlineAnchor {\r\n                anchor_ms: now_ms(),\r\n                interval_ms: interval_ms.max(1),\r\n            });\r\n        // The next fire at/after now. A fresh anchor (cold/crash) sits on-grid at\r\n        // `now` → fires immediately (the old loop's tick-first behavior); an\r\n        // update anchor lands on the next grid point (phase preserved).\r\n        let mut deadline = anchor.next_fire(now_ms());\r\n        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\r\n        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\r\n        // the initial state.\r\n        let mut manifest_hash = self.manifest_disk_hash();\r\n        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\r\n        // in the loop's local state — the loop is per-endpoint + single-threaded, so\r\n        // this is the simplest correct home (no lock, no cross-endpoint bleed).\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let mut turn_strikes: u32 = 0;\r\n        while !stop.load(Ordering::Relaxed) {\r\n            let now = now_ms();\r\n            if now < deadline {\r\n                // Sleep toward the deadline in small slices so a stop is honored\r\n                // promptly rather than after a full (possibly multi-second) wait.\r\n                sleep_interruptible(Duration::from_millis(deadline - now), stop);\r\n                continue;\r\n            }\r\n            // Pull any live-update manifest swap from disk before the tick so the\r\n            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\r\n            self.reload_manifest_if_changed(&mut manifest_hash);\r\n            match self.pulse_tick(session_id) {\r\n                Ok(report) => {\r\n                    // F-030 W1: feed a fired turn's outcome to the failure budget.\r\n                    if let Some(outcome) = &report.turn_outcome {\r\n                        self.note_turn_outcome(&mut turn_strikes, outcome);\r\n                    }\r\n                    on_tick(&report);\r\n                }\r\n                Err(_) => { /* a transient tick error must not kill the loop */ }\r\n            }\r\n…\n        }\n    }","startLine":459,"lineNumbers":[459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,null,750,751]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\lifecycle.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2890,"totalBytes":15296,"outputLines":285,"outputBytes":15296,"shownRange":{"start":459,"end":743},"nextOffset":744}}},"isError":false,"timestamp":1784271495606}}
{"type":"message","id":"23c1062a","parentId":"3f817c46","timestamp":"2026-07-17T06:58:37.729Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing session echo gate behavior**\n\n**Inspecting registry and project index scanning**\n\n**Planning live read-only process probes**"},{"type":"thinking","thinking":"**Planning code inspection for read pump workers**"},{"type":"toolCall","id":"call_8XzFl3nM6QRRCTH0VZ9EXzJU|fc_038f28d3e93bbfd4016a59d29da85081969a6950fce6d4217b","name":"read","arguments":{"i":"Reading registry pump scans","path":"crates/spt-daemon/src/pump/registry.rs","selector":"1-260"}},{"type":"toolCall","id":"call_sEz7C6FAwMhyda2o1CpSwWag|fc_038f28d3e93bbfd4016a59d29da86481969c17e051b28e6f39","name":"read","arguments":{"i":"Reading notif pump reads","path":"crates/spt-daemon/src/pump/notif.rs","selector":"1-240"}},{"type":"toolCall","id":"call_Uio5bfCf7olO8ZU9HFyIkZVl|fc_038f28d3e93bbfd4016a59d29da8688196b5510c2aa5aa49d0","name":"read","arguments":{"i":"Reading sync pump reads","path":"crates/spt-daemon/src/pump/sync.rs","selector":"1-280"}},{"type":"toolCall","id":"call_4eL5w3pKG1bwWBOvJ8zxRFO7|fc_038f28d3e93bbfd4016a59d29da8708196afe328de9ce5088f","name":"read","arguments":{"i":"Reading update pump reads","path":"crates/spt-daemon/src/pump/update.rs","selector":"1-220"}},{"type":"toolCall","id":"call_j3XWHrjDkfLEwU8SQgZ01UrO|fc_038f28d3e93bbfd4016a59d29da8748196aa28565d2e4c32a7","name":"grep","arguments":{"i":"Locating pump path loads","pattern":"struct RoundCtx|impl RoundCtx|struct PumpPaths|impl PumpPaths|fn poll_wake|fn pre_round|fn peer_step","path":"crates/spt-daemon/src/pump","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_NQWvnk1u0IF8Gmhf1OCgr9Ar|fc_038f28d3e93bbfd4016a59d29da8788196abb04f6cbd1a9c8b","name":"grep","arguments":{"i":"Finding registry scan internals","pattern":"fn advertise_local|pub fn advertise_local|list_self_perch_ids|read_info|read_dir","path":"crates/spt-daemon/src/registryhost.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":22404,"output":931,"cacheRead":118272,"cacheWrite":0,"totalTokens":141607,"reasoningTokens":612,"cost":{"input":0.11202000000000001,"output":0.02793,"cacheRead":0.059135999999999994,"cacheWrite":0,"total":0.199086}},"stopReason":"toolUse","timestamp":1784271495750,"responseId":"resp_038f28d3e93bbfd4016a59d287fb0481968282741ec20670cb","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d28cb6448196bea857a27b946f41","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdKYeQ41s7WpkF7iZwcnKbFLRa421LBkJQB4oxmHqB2i9r1ejv5WlC6pfIm0bYlkhh1wjActAcVebzj7IVOMkah-016J2uqoggWy9hp4xDnmu3m4ifAxO-gQ19SgSS5R31Xp81ANKrx9F2D-mpOzX7ojUeGKPclFdeV7H0KREKncc2g5gWe5Jbhs-G2zEJPDwynce9TDYOc6wUSzCp9aTqVvJT7zmY5JMmRv3vmjHV9u8C4kdPwyC_Tubq4RzjHlV2sjXqWAIHp56aHup6aA37ZoLCZ5xRx_HI0h1T5-0NSl6gjg8-mx2d1C3ABv_kQz5vvp-fOqJBvz6vuO1GC2Zo46hMLiG7_IMMxvPODUvPN-apwskd_JAu6vrAPWiQiLU7Nt6k7ltRcc38JoAMh2C53GJgyXxs2RQFC_V0jNB9nAicgllanyaSdPY33AAgSdUyslpJk0EBY3qopNlQF-H5VR4_jqdgrzSDS7vQC3qHdCVnegd0Del1XA6dfa7jwqyX1pkr31MAZLyAQlYnBbYBMegKYGCsXc891Uosps2aRH8imX9Gci9_HQA3tUIrhQxEoLHThYXJQX3VktEz-LsByOGNzq3TES3i8UFSfHIslNCp-4CoETIPpcSvDswNX4FS0KIuso1TcO4ODMcbqJmPsRGSoM4Fqt_biqiRmL2If2I04uSFURn7LVwztknqNYHDjjoWP4jCP38JKdUDAEdvvCr6xfUwSU3KZCfBGIbQfRlVojh-b_B8EMZvdaxNuPbAr_Ap-Qyu9JQLmz0ImJlw5ZfCjmzW_vyAGk3iglPinL7v26lvOaDLDA9s7XUmGgkUMuS5nh-261QO-B6otbNa1V8qxcl-3P4T2opw4Vf_sHmamvpN8Rey9GdMSJVQZ6wpcTI6I5D_OX1OB3FxYefCVUxBz5a3osmHiMBcQAwjBONW4XvJafBwsGTl_VfRmcKMQHkJ8JFpPia3kY9Vymw57UekxXAP53U4M50pE_ia1J0SkkO-PwPanAxh9JTavRy4hYJU_OAXyUoTPinON2A56KFS679Zx8I61Yt3xAIYw1hraFLw87ZhCFp01rb4EP4zYHfOTZr5ZOoPKrpFjJcUpkUFZeryIz4H82mPVX29K1miiHsbaIr-bMBLnpdSqUZAPeQbbwxlznqkIo6UXMjsW4lvi0NyxUWsIpQ5aR-L9p_U9WE5HeLH2Nj_AZ1nMQsrmEM7SUGU3XXNoyQcco8mUcT4Lme1jUYk0XPcA6LKRP-HXC9QuRkaQwglffPz1Iv9wpqLhoi28VEuDdQOMphsXnlEt0YuZHIf-SFiU5_gzm1ARG7v_2rlNnterKcdKObawlIpenNPQCugMUB95cwm3Z0vC_9JBf2_XUuM6z_wClGgiVZMItBjNLUBOf6kVn7lCw4TvaCg__tcS6d-rw-l7otAZcYBdM8uaWLuAxqZgujwXzOPApwp0WKOYESuzQgQOUkpgWDeDGlQLlRAXhGHLLfsIv2_PGn_vfATZmTqVluazPAJUJ82Gg21B9jNvlApgRLx4EnTizggsZjWlNGcFxrrOyVvi10EjwPEXvvcSDRqiPuNSliSUB2GY53W_8zsbP6U7ZfQs_mkbHy6joNaA6iFUMXCSpojFPIQoz456CR98SONog1fS-hxOHaYgnPRVTlTk0NG-gWm9Rh0XKrNzBZCSXSTSXwOU4iPj93jYeGcGO7v6qwPyg3Ul8y1G6TtowOb0VGOBwBW7wyGTxIFFG5LHo_Zaw4inxxinH0m9VTAIs6jHlF3raIxaX6p5WQNcWtoroQRPFrDb6zBx4ObUpaly3biJSsXGbpuWasjeSLs-yzlrCznIBZ0fOnBNeVe-kFE4owhTpOT-gUpsuL24S0n3UAV7VPCA6K9_kHitVKzIatzlaRY9wR0oDPw6BSqwhuLbA-u0U-BjLlpGeu3_06wa1uejlcb_QVzgXtanJ_-C5frFII01epTvj8HBhbRyrjz_J2jYGHrGQewLWJYxma1PT_CewkUnUpk3UyDdH6Kd_PWjp8fsnH6oCu2urE_9bUxu2I3I3GBMO4JH-BCHJ_E5FIyyyi0YWpiHqh_1VVUpuFMgCmz0x6i0ydm6JmrQ4GKejaUng8O2czkFJCPwPvoCKlmuIAfy_mB0fxFg7VngW9X8w0T9QsZ8pu3Kucmrmww9OJaLaYuvX-eR631FkaukJTbwtLkMZjLU7jAJ97uxh0geGQfTHT_Q9XTFTtFPUNqs1Ip8UKd46nYpptdIYIU43j3bhy6T975kpdzhWsaSyGm2ULxfgABCi8-zTN3VUm-MgicmIEcnsKPFl0PIeHpWkz58BihyAEJwDY1k7_34ndRxDKRUTBXMICYa4hRSrnzo4n3V54qnKcSC90j_U-srlHl4wVztaQUG3ldGYjuBiWQ65scE3fJwfCZoOXyNUG61mYOSeyWFtokdYG0M83A7Qob6Cqj-hGzncgZ_BLvITLbK3bEbXp2u2soKv0oyJUlxX_cTtitUh4nrSKLyfUZSadx2YKWrOhNHxN_jDnxqlgLmpwZHPI77EDtPbIkU3x1qBO9bodSvPuLuzJyVnmrHEzeXTA3ULg3heoh_73jb9MSL8l2aPZDowLH222UjLn8ettzSSwKRftoA8YNkg74Y3ya9rPVJqr97sLoPWaB0gwErjFcdr_GvI7RzBNWGfKObtvEdYo64BAmO6ubjCsgzsIl0ZpmylRtAhgCXLYxlrVsgQ2TuBBvJUialAxCdAeHNIDSXRcAaBoLH9E8kbNQ-MThxEHA-ILKVVlpvAOP3T_45NcQvLdhuziIyGCAhTCta5dbrjKyUpw0tGIUa3kHQ9BuzPISnmOUIDodO7BWMdtsqIF_Hz8oaEY72JPGw1LPaTNM5gbWfCrdyPf5NkqPzb2-q31lY8I0EUHLHvpydaSSLbkxl7joKtkHtFM84-8cONV8kbvMv_3stQ8NcYKn_oeSgjtZtMYAisR79rF3k1U5Ow7M4VnjO4kVajyhtx6MWwpjaRMpL3cjprR0Gd7UwsmWfJTdjovD_09kerYqx8R1Y-uPg3M2wlN7q3u6iC3caWA6IsZCAYmbWWcUs2Gzu8rf66SK5l6nbOmZNDjyWreFnsgadj49NU-OXYda0pDB2TyeoCwYCBJmcZiRclUgkapP9qiM8ewu9JPih4Y7kj0lggvzWMCkZe7fqk-NJpKSdnK_-wg9dI0h-mlmmgj4Yp7Y9Mil6xzgZ3ijyk6cMDmunYMAnNHJpYHfnGsYihHdRvBvW_wFP_8ldcoKx82J_kBl6BBptuqOKveosrEZlYaxhttMMKnxaZqnVA4yNK3rU9-367u8mIHAnG7h5BvvZuuHSlw14CKsSNaNyrFOkPqP139x4uAKuoya2eMydGAIi58HmEVkIIX5RKfrc6_C0KWJ2Lxo3RFit2jXAoQsArZB8-akT60jOfNcQS5RNvJnEjVVOGe7QvlntT7v7I-uZenJkmdG50sPhfypUZlSH8k4Zhi1KAyZdPT23VEwcyT-xpy7G57bkFd_Wb62s8WcVOHgT1FJjgpGdjZtzPFBoE75YvpqjsGZoRxiktGJdjPxeAMTzgRXUUSI1Nm6ZLav0w7Ca983rFHSBO2Wuhzt1Y49e55O4remd-gMVg09RwqizQd8KBA6eDs7RtcYFlwxol9NFemAXygWU2SwQkdr8_Xrr0kF6ivU6qBTBQSrA1-7ubct-Ei96c5Li3aUCnH42peyoR8wGY_ic-V6ueXPENDkuqwpNLtl0uFp6ILaSKuDf2ac70dDbZ8RY9KiTKZXrPtbgvVckywNIs4INIfEICZQ5cTcWTi64tc2RykZPMESkn_2aGaZeM5ZD2cofKg_AMXGVgB5r9ENIHpzG0Jkt0SSJEqbiEfeJvIgBtKlKyNh2y9rXdwFswMxhVUO2-FsqS3p5LdQmIxPqQWwT41Uvy8f8jjswcCVa3u24TtadG","summary":[{"type":"summary_text","text":"**Analyzing session echo gate behavior**"},{"type":"summary_text","text":"**Inspecting registry and project index scanning**"},{"type":"summary_text","text":"**Planning live read-only process probes**"}]},{"id":"rs_038f28d3e93bbfd4016a59d29839988196bc3574b5ac2fd94c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdKdrsNcsXlWUYvw_1tdFvcnYBg_LYEHF244uRjussqgCawsNz43PxSHPfJ1wq4CEw2vuCHzg8RDknXl0Evos6Y1nB1QhWG6HQWoN-LqrRzB0yRPVcWIw-U_m-1DizP1v7AXoMu2VNopSNtbhS08SVjSV5HinHFsWYiSERIUOsdIE99TQ4UtmzYzm_bnhNTX5VYXpGxWykg1Tu5QHRyRotSzLJMvOLKSvqSzsZcGqpXnZowhZp1skuvtunSRLrE38Anlk9HzWfyJ13v8Xrhblvw6YJQrA3Sa-V62ZV4eo8qjNtd9KF_kpBbBupwzQPTiOsK6pIJkGE3AT6ivRX5fivqYPuA0h_He_AHDpOiDR1X1msMl80QI-88UKcsHy-qNR_7z7WQn6mDhIzw7cNn8TQb-3sb5-zRrXacMUQHB1JRNxqbpdqOCaSqBxnggpPT4_cYd43889m0V6zD2yXRpFLVEW7fC7m4-ne-n_5BhDNwpkilSCqJTC8EkkEuYN0mLn2FnWUCUWC31_ywyxRY6n4Z-7MOf5JNXtBXnkWchChIo4ODc7V--2L4m2H9kMOlfAODTnZWf6lt4KUJ8g0hQpMngmK4M3XDn4bQkfRI6_HH9EbTiKf_pf4P3-Rg_6YiOedV3eEZkbCp0lWLmK5utH2zWe0suqWUxBgmWYsVALvOPbfJLX5b28m1X-887oRw4Ny241xEM0AWFfOeFjte8dT9-xDvYj4sd6O_rPyWB6SmVOM0gjau9vagJuufYOqJcAKKo5U86UNn1erRHcaUgMl55MYUcJU0ZqX5-5xvxXoj0R4PfL0id12vS3DNJbTA18ftvaSm5fCn3Q-mL_Y4xe-LJp-KXAY5FLgiejiZ4oYAR96xZkhVZV-Ktr2evICzQrsi_DMoFteJS_bIjm-1PDjJ0EVTjAK6hcfjCp4vVUqA6nAaK3Dqw7yU7fgTL-uHXnJ9569KoV5vky3UpQ7G6vSkuXgUtce8WfK7mn_sjX5fvpZHwyj73B3hAX9bdLfqd9LWWJFG6jeSO6nT5u9Ask-VJNS7gfanKYte3_A2Gz5IRuhvopPnqrUaGuPj1uD1k_2kXSDosb8FdvTfr3ceNDIc3V7k9e23TX91kWAq1oIVo9MGDGVPiqOIcI5DwZS8x6Om7-eAqd_N2TM0DG6aVqofbgDfNAIVfg5kuqyqKac-4JHJ5Tdg1kQHUKvsP4GtydLAimrqbUtLwijJ_U2g7usRUe2ktNsf1VFS-bfGfFs0wOwz7Z__1-07uqbFTCJaIP2_U2UuyoRN4drT-vPrcyepcMK-UevZ7P19J0xWPaBFwAUQoqfqtZk7pr2809qw98flhiKMiNNgzFUmXEqhjV6-osR7fuQnT46Sdj6r5i_F8ldkrMohBuf3tGJ-M1pfFNbMbZ6a_CA7LoyuTq612SId6JM186VWvNHbpNiL0i2BmB5zzwxHR658kDC1FrF5iscuM1jWguacGqhGV7uFHUr88gQ4SqNCaC2jsbCPubUbrlHs=","summary":[{"type":"summary_text","text":"**Planning code inspection for read pump workers**"}]},{"id":"fc_038f28d3e93bbfd4016a59d29da85081969a6950fce6d4217b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry pump scans\",\"path\":\"crates/spt-daemon/src/pump/registry.rs\",\"selector\":\"1-260\"}","call_id":"call_8XzFl3nM6QRRCTH0VZ9EXzJU","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d29da86481969c17e051b28e6f39","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading notif pump reads\",\"path\":\"crates/spt-daemon/src/pump/notif.rs\",\"selector\":\"1-240\"}","call_id":"call_sEz7C6FAwMhyda2o1CpSwWag","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d29da8688196b5510c2aa5aa49d0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading sync pump reads\",\"path\":\"crates/spt-daemon/src/pump/sync.rs\",\"selector\":\"1-280\"}","call_id":"call_Uio5bfCf7olO8ZU9HFyIkZVl","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d29da8708196afe328de9ce5088f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading update pump reads\",\"path\":\"crates/spt-daemon/src/pump/update.rs\",\"selector\":\"1-220\"}","call_id":"call_4eL5w3pKG1bwWBOvJ8zxRFO7","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d29da8748196aa28565d2e4c32a7","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating pump path loads\",\"pattern\":\"struct RoundCtx|impl RoundCtx|struct PumpPaths|impl PumpPaths|fn poll_wake|fn pre_round|fn peer_step\",\"path\":\"crates/spt-daemon/src/pump\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_j3XWHrjDkfLEwU8SQgZ01UrO","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d29da8788196abb04f6cbd1a9c8b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding registry scan internals\",\"pattern\":\"fn advertise_local|pub fn advertise_local|list_self_perch_ids|read_info|read_dir\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_NQWvnk1u0IF8Gmhf1OCgr9Ar","name":"grep"}]},"duration":21977.853900000453,"ttft":4977.592500001192,"contextSnapshot":{"promptTokens":140676,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8XzFl3nM6QRRCTH0VZ9EXzJU|fc_038f28d3e93bbfd4016a59d29da85081969a6950fce6d4217b","toolName":"read","startedAt":"2026-07-17T06:58:37.729Z","args":{"path":"crates/spt-daemon/src/pump/registry.rs"},"intent":"Reading registry pump scans"},"id":"841c4bda","parentId":"23c1062a","timestamp":"2026-07-17T06:58:37.729Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_sEz7C6FAwMhyda2o1CpSwWag|fc_038f28d3e93bbfd4016a59d29da86481969c17e051b28e6f39","toolName":"read","startedAt":"2026-07-17T06:58:37.729Z","args":{"path":"crates/spt-daemon/src/pump/notif.rs"},"intent":"Reading notif pump reads"},"id":"e6cad1f7","parentId":"841c4bda","timestamp":"2026-07-17T06:58:37.729Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Uio5bfCf7olO8ZU9HFyIkZVl|fc_038f28d3e93bbfd4016a59d29da8688196b5510c2aa5aa49d0","toolName":"read","startedAt":"2026-07-17T06:58:37.729Z","args":{"path":"crates/spt-daemon/src/pump/sync.rs"},"intent":"Reading sync pump reads"},"id":"04137675","parentId":"e6cad1f7","timestamp":"2026-07-17T06:58:37.729Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4eL5w3pKG1bwWBOvJ8zxRFO7|fc_038f28d3e93bbfd4016a59d29da8708196afe328de9ce5088f","toolName":"read","startedAt":"2026-07-17T06:58:37.729Z","args":{"path":"crates/spt-daemon/src/pump/update.rs"},"intent":"Reading update pump reads"},"id":"cc8c0756","parentId":"04137675","timestamp":"2026-07-17T06:58:37.729Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_j3XWHrjDkfLEwU8SQgZ01UrO|fc_038f28d3e93bbfd4016a59d29da8748196aa28565d2e4c32a7","toolName":"grep","startedAt":"2026-07-17T06:58:37.730Z","args":{"path":"crates/spt-daemon/src/pump"},"intent":"Locating pump path loads"},"id":"b342b8eb","parentId":"cc8c0756","timestamp":"2026-07-17T06:58:37.730Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NQWvnk1u0IF8Gmhf1OCgr9Ar|fc_038f28d3e93bbfd4016a59d29da8788196abb04f6cbd1a9c8b","toolName":"grep","startedAt":"2026-07-17T06:58:37.730Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Finding registry scan internals"},"id":"e76d0d82","parentId":"b342b8eb","timestamp":"2026-07-17T06:58:37.730Z"}
{"type":"message","id":"5d700908","parentId":"e76d0d82","timestamp":"2026-07-17T06:58:37.738Z","message":{"role":"toolResult","toolCallId":"call_NQWvnk1u0IF8Gmhf1OCgr9Ar|fc_038f28d3e93bbfd4016a59d29da8788196abb04f6cbd1a9c8b","toolName":"grep","content":[{"type":"text","text":" 372|    // [impl->REQ-INST-7]\n*373|    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n 374|        let subnets = SubnetStore::load();\n 375|        let mut vis = VisibilityStore::load();\n 376|        // First-join home adoption (D9-5, REQ-INST-15): within one\n...\n 412|        let live_ids: std::collections::BTreeSet<String> =\n*413|            perch::list_self_perch_ids(owlery).into_iter().collect();\n 414|\n*415|        for id in perch::list_self_perch_ids(owlery) {\n 416|            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n 417|            // Advertisement follows the resting-state machine (D9-2,\n 418|            // REQ-INST-3): a live perch with a resting record advertises its\n...\n 430|            // [impl->REQ-INST-14]\n*431|            let info = spt_store::info::read_info(&perch_path);\n 432|            let resources = info\n 433|                .as_ref()\n 434|                .and_then(|i| i.resources.clone())\n...\n 1932|    #[test]\n*1933|    fn advertise_local_scans_and_respects_visibility() {\n 1934|        with_home(|home| {\n 1935|            let owlery = home.join(\"owlery\");\n 1936|            for id in [\"vis-ag\", \"hid-ag\"] {\n...\n 2172|    #[test]\n*2173|    fn advertise_local_stamps_the_node_label() {\n 2174|        with_home(|home| {\n 2175|            let owlery = home.join(\"owlery\");\n 2176|            let p = perch::resolve_perch_path_in(&owlery, \"lab-ag\", ParentHint::Infer);"}],"details":{"scopePath":"crates/spt-daemon/src/registryhost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":6,"fileCount":1,"files":["crates/spt-daemon/src/registryhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/registryhost.rs","count":6}],"truncated":false,"displayContent":"  372│    // [impl->REQ-INST-7]\n *373│    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n  374│        let subnets = SubnetStore::load();\n  375│        let mut vis = VisibilityStore::load();\n  376│        // First-join home adoption (D9-5, REQ-INST-15): within one\n     │...\n  412│        let live_ids: std::collections::BTreeSet<String> =\n *413│            perch::list_self_perch_ids(owlery).into_iter().collect();\n  414│\n *415│        for id in perch::list_self_perch_ids(owlery) {\n  416│            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n  417│            // Advertisement follows the resting-state machine (D9-2,\n  418│            // REQ-INST-3): a live perch with a resting record advertises its\n     │...\n  430│            // [impl->REQ-INST-14]\n *431│            let info = spt_store::info::read_info(&perch_path);\n  432│            let resources = info\n  433│                .as_ref()\n  434│                .and_then(|i| i.resources.clone())\n     │...\n 1932│    #[test]\n*1933│    fn advertise_local_scans_and_respects_visibility() {\n 1934│        with_home(|home| {\n 1935│            let owlery = home.join(\"owlery\");\n 1936│            for id in [\"vis-ag\", \"hid-ag\"] {\n     │...\n 2172│    #[test]\n*2173│    fn advertise_local_stamps_the_node_label() {\n 2174│        with_home(|home| {\n 2175│            let owlery = home.join(\"owlery\");\n 2176│            let p = perch::resolve_perch_path_in(&owlery, \"lab-ag\", ParentHint::Infer);"},"isError":false,"timestamp":1784271517737}}
{"type":"message","id":"17969db7","parentId":"5d700908","timestamp":"2026-07-17T06:58:37.738Z","message":{"role":"toolResult","toolCallId":"call_Uio5bfCf7olO8ZU9HFyIkZVl|fc_038f28d3e93bbfd4016a59d29da8688196b5510c2aa5aa49d0","toolName":"read","content":[{"type":"text","text":"//! The sync pump worker (pump-seam P3) — anti-entropy pull for the minds.\n//!\n//! `poll_wake` consumes the wake-edge freshness-pull marker (a dormant→active\n//! transition forces a pull NOW); `peer_step` builds the want-set — the union\n//! of locally-tracked allowed branches ([`select_refs`]) and registry-derived\n//! `a-<id>` bootstrap refs (endpoints known in the shared subnet this node\n//! never held locally), both gated by the same [`crate::sync::SyncPolicy`] the\n//! responder enforces (symmetric scoping) — then pulls via [`request_sync`]\n//! over the shell's open-op counter and reports conflicts through the\n//! construction-captured [`PumpHooks`]. Construction-captured deps (§Frozen-4):\n//! the registry [`Arc`] (snapshot bootstrap refs), the store/scratch/marker\n//! paths, and the hooks.\n\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse spt_store::contextstore::ContextStore;\n\nuse crate::registryhost::RegistryHost;\nuse crate::sync::{request_sync, select_refs};\n\nuse super::{PeerIo, PumpCadence, PumpHooks, PumpPaths, PumpWorker, RoundCtx};\n\n/// The sync leg as a [`PumpWorker`]: anti-entropy pull for the minds.\npub(super) struct SyncWorker {\n    /// Registry snapshot source for the bootstrap-ref union (§Frozen-4: the\n    /// sync worker's own construction-captured Arc, not the shell's).\n    registry: Arc<RegistryHost>,\n    /// The context store root ([`ContextStore::open_or_init_in`]).\n    ctx_root: PathBuf,\n    /// Transient pull scratch.\n    scratch: PathBuf,\n    /// The wake-edge freshness-pull marker\n    /// ([`crate::resting::take_freshness_pull`]).\n    pull_marker: PathBuf,\n    /// This worker's scheduling period (`cadence.sync`).\n    cadence: Duration,\n    /// The conflict-surface hooks (ADR-0013): `on_pull` fires after every\n    /// completed pull. A sync-worker construction dep (the conflict surface).\n    hooks: PumpHooks,\n}\n\nimpl SyncWorker {\n    /// Construction capture (§Frozen-4): the registry Arc, the worker's path\n    /// slices, the cadence, and the conflict-surface hooks.\n    pub(super) fn new(\n        registry: Arc<RegistryHost>,\n        paths: &PumpPaths,\n        cadence: &PumpCadence,\n        hooks: PumpHooks,\n    ) -> SyncWorker {\n        SyncWorker {\n            registry,\n            ctx_root: paths.ctx_root.clone(),\n            scratch: paths.scratch.clone(),\n            pull_marker: paths.pull_marker.clone(),\n            cadence: cadence.sync,\n            hooks,\n        }\n    }\n}\n\nimpl PumpWorker for SyncWorker {\n    fn cadence(&self) -> Duration {\n        self.cadence\n    }\n\n    /// The wake edge's freshness pull (D9-2, REQ-INST-3): a dormant→active\n    /// transition dropped the marker — consume it and pull NOW, the\n    /// catch-up-on-activation latency is the point. The take is the remove, so\n    /// one wake claims exactly one forced round.\n    // [impl->REQ-INST-3]\n    fn poll_wake(&mut self) -> bool {\n        crate::resting::take_freshness_pull(&self.pull_marker)\n    }\n\n    /// Pull anti-entropy from one peer: the want-set is the union of\n    /// [`select_refs`] (locally-tracked branches the policy allows toward the\n    /// peer) and registry-derived `a-<id>` bootstrap refs, both gated by the\n    /// same policy the responder enforces. The pull's `open_op` is durable\n    /// (exactly-once open identity), minted from the shell's sole counter via\n    /// [`PeerIo`].\n    // [impl->REQ-INST-5]\n    fn peer_step(\n        &mut self,\n        io: &mut PeerIo,\n        subnet: &str,\n        peer_hex: &str,\n        ctx: &RoundCtx,\n    ) -> io::Result<()> {\n        let cs = ContextStore::open_or_init_in(&self.ctx_root)?;\n        let mut want = select_refs(&ctx.policy, &cs, peer_hex)?;\n        // Registry-derived bootstrap refs: minds named in the shared subnet\n        // that this node never held locally.\n        if let Some(snap) = self.registry.snapshot(subnet) {\n            for id in snap.endpoint_ids() {\n                let r = format!(\"a-{id}\");\n                if !want.contains(&r) && ctx.policy.allows(&r, peer_hex) {\n                    want.push(r);\n                }\n            }\n        }\n        if !want.is_empty() {\n            let open_op = io.open_op()?;\n            let report = request_sync(&mut *io.brain, io.conn_id, &want, open_op, &cs, &self.scratch)?;\n            (self.hooks.on_pull)(peer_hex, &report);\n        }\n        Ok(())\n    }\n}\n"}],"details":{"displayContent":{"text":"//! The sync pump worker (pump-seam P3) — anti-entropy pull for the minds.\r\n//!\r\n//! `poll_wake` consumes the wake-edge freshness-pull marker (a dormant→active\r\n//! transition forces a pull NOW); `peer_step` builds the want-set — the union\r\n//! of locally-tracked allowed branches ([`select_refs`]) and registry-derived\r\n//! `a-<id>` bootstrap refs (endpoints known in the shared subnet this node\r\n//! never held locally), both gated by the same [`crate::sync::SyncPolicy`] the\r\n//! responder enforces (symmetric scoping) — then pulls via [`request_sync`]\r\n//! over the shell's open-op counter and reports conflicts through the\r\n//! construction-captured [`PumpHooks`]. Construction-captured deps (§Frozen-4):\r\n//! the registry [`Arc`] (snapshot bootstrap refs), the store/scratch/marker\r\n//! paths, and the hooks.\r\n\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::sync::Arc;\r\nuse std::time::Duration;\r\n\r\nuse spt_store::contextstore::ContextStore;\r\n\r\nuse crate::registryhost::RegistryHost;\r\nuse crate::sync::{request_sync, select_refs};\r\n\r\nuse super::{PeerIo, PumpCadence, PumpHooks, PumpPaths, PumpWorker, RoundCtx};\r\n\r\n/// The sync leg as a [`PumpWorker`]: anti-entropy pull for the minds.\r\npub(super) struct SyncWorker {\r\n    /// Registry snapshot source for the bootstrap-ref union (§Frozen-4: the\r\n    /// sync worker's own construction-captured Arc, not the shell's).\r\n    registry: Arc<RegistryHost>,\r\n    /// The context store root ([`ContextStore::open_or_init_in`]).\r\n    ctx_root: PathBuf,\r\n    /// Transient pull scratch.\r\n    scratch: PathBuf,\r\n    /// The wake-edge freshness-pull marker\r\n    /// ([`crate::resting::take_freshness_pull`]).\r\n    pull_marker: PathBuf,\r\n    /// This worker's scheduling period (`cadence.sync`).\r\n    cadence: Duration,\r\n    /// The conflict-surface hooks (ADR-0013): `on_pull` fires after every\r\n    /// completed pull. A sync-worker construction dep (the conflict surface).\r\n    hooks: PumpHooks,\r\n}\r\n\r\nimpl SyncWorker {\r\n    /// Construction capture (§Frozen-4): the registry Arc, the worker's path\r\n    /// slices, the cadence, and the conflict-surface hooks.\r\n    pub(super) fn new(\r\n        registry: Arc<RegistryHost>,\r\n        paths: &PumpPaths,\r\n        cadence: &PumpCadence,\r\n        hooks: PumpHooks,\r\n    ) -> SyncWorker {\r\n        SyncWorker {\r\n            registry,\r\n            ctx_root: paths.ctx_root.clone(),\r\n            scratch: paths.scratch.clone(),\r\n            pull_marker: paths.pull_marker.clone(),\r\n            cadence: cadence.sync,\r\n            hooks,\r\n        }\r\n    }\r\n}\r\n\r\nimpl PumpWorker for SyncWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    /// The wake edge's freshness pull (D9-2, REQ-INST-3): a dormant→active\r\n    /// transition dropped the marker — consume it and pull NOW, the\r\n    /// catch-up-on-activation latency is the point. The take is the remove, so\r\n    /// one wake claims exactly one forced round.\r\n    // [impl->REQ-INST-3]\r\n    fn poll_wake(&mut self) -> bool {\r\n        crate::resting::take_freshness_pull(&self.pull_marker)\r\n    }\r\n\r\n    /// Pull anti-entropy from one peer: the want-set is the union of\r\n    /// [`select_refs`] (locally-tracked branches the policy allows toward the\r\n    /// peer) and registry-derived `a-<id>` bootstrap refs, both gated by the\r\n    /// same policy the responder enforces. The pull's `open_op` is durable\r\n    /// (exactly-once open identity), minted from the shell's sole counter via\r\n    /// [`PeerIo`].\r\n    // [impl->REQ-INST-5]\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        peer_hex: &str,\r\n        ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        let cs = ContextStore::open_or_init_in(&self.ctx_root)?;\r\n        let mut want = select_refs(&ctx.policy, &cs, peer_hex)?;\r\n        // Registry-derived bootstrap refs: minds named in the shared subnet\r\n        // that this node never held locally.\r\n        if let Some(snap) = self.registry.snapshot(subnet) {\r\n            for id in snap.endpoint_ids() {\r\n                let r = format!(\"a-{id}\");\r\n                if !want.contains(&r) && ctx.policy.allows(&r, peer_hex) {\r\n                    want.push(r);\r\n                }\r\n            }\r\n        }\r\n        if !want.is_empty() {\r\n            let open_op = io.open_op()?;\r\n            let report = request_sync(&mut *io.brain, io.conn_id, &want, open_op, &cs, &self.scratch)?;\r\n            (self.hooks.on_pull)(peer_hex, &report);\r\n        }\r\n        Ok(())\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\sync.rs"}}},"isError":false,"timestamp":1784271517738}}
{"type":"message","id":"fdf6ad05","parentId":"17969db7","timestamp":"2026-07-17T06:58:37.739Z","message":{"role":"toolResult","toolCallId":"call_sEz7C6FAwMhyda2o1CpSwWag|fc_038f28d3e93bbfd4016a59d29da86481969c17e051b28e6f39","toolName":"read","content":[{"type":"text","text":"//! The notif pump worker (pump-seam P3) — the full semilattice spool push.\n//!\n//! `peer_step` emits this node's notif feed for the subnet\n//! ([`emit_notif_feed`]) and pushes it over the peer's feed stream; the\n//! receiver's join dedups. No node-local `pre_round` and no wake edge — a\n//! plain cadenced push. Construction-captured dep: the notif spool DB path.\n\nuse std::io;\nuse std::path::PathBuf;\nuse std::time::Duration;\n\nuse spt_store::notif::NotifStore;\n\nuse crate::notifsync::emit_notif_feed;\n\nuse super::{PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n\n/// The notif leg as a [`PumpWorker`]: the full semilattice spool per subnet\n/// ([`emit_notif_feed`]); the receiver's join dedups.\npub(super) struct NotifWorker {\n    /// The notif spool DB ([`NotifStore::open_at`]).\n    notif_db: PathBuf,\n    /// This worker's scheduling period (`cadence.notif`).\n    cadence: Duration,\n}\n\nimpl NotifWorker {\n    /// Construction capture (§Frozen-4): the notif spool DB path + the cadence.\n    pub(super) fn new(paths: &PumpPaths, cadence: &PumpCadence) -> NotifWorker {\n        NotifWorker {\n            notif_db: paths.notif_db.clone(),\n            cadence: cadence.notif,\n        }\n    }\n}\n\nimpl PumpWorker for NotifWorker {\n    fn cadence(&self) -> Duration {\n        self.cadence\n    }\n\n    /// Emit this node's notif feed for `subnet` (the full semilattice spool)\n    /// and push it fire-and-forget; the receiver's join dedups.\n    // [impl->REQ-NOTIF-1]\n    fn peer_step(\n        &mut self,\n        io: &mut PeerIo,\n        subnet: &str,\n        _peer_hex: &str,\n        _ctx: &RoundCtx,\n    ) -> io::Result<()> {\n        let store =\n            NotifStore::open_at(&self.notif_db).map_err(|e| io::Error::other(e.to_string()))?;\n        let feed = emit_notif_feed(&store, subnet).map_err(io::Error::other)?;\n        if !feed.is_empty() {\n            io.push_feed(&feed)?;\n        }\n        Ok(())\n    }\n}\n"}],"details":{"displayContent":{"text":"//! The notif pump worker (pump-seam P3) — the full semilattice spool push.\r\n//!\r\n//! `peer_step` emits this node's notif feed for the subnet\r\n//! ([`emit_notif_feed`]) and pushes it over the peer's feed stream; the\r\n//! receiver's join dedups. No node-local `pre_round` and no wake edge — a\r\n//! plain cadenced push. Construction-captured dep: the notif spool DB path.\r\n\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::time::Duration;\r\n\r\nuse spt_store::notif::NotifStore;\r\n\r\nuse crate::notifsync::emit_notif_feed;\r\n\r\nuse super::{PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\r\n\r\n/// The notif leg as a [`PumpWorker`]: the full semilattice spool per subnet\r\n/// ([`emit_notif_feed`]); the receiver's join dedups.\r\npub(super) struct NotifWorker {\r\n    /// The notif spool DB ([`NotifStore::open_at`]).\r\n    notif_db: PathBuf,\r\n    /// This worker's scheduling period (`cadence.notif`).\r\n    cadence: Duration,\r\n}\r\n\r\nimpl NotifWorker {\r\n    /// Construction capture (§Frozen-4): the notif spool DB path + the cadence.\r\n    pub(super) fn new(paths: &PumpPaths, cadence: &PumpCadence) -> NotifWorker {\r\n        NotifWorker {\r\n            notif_db: paths.notif_db.clone(),\r\n            cadence: cadence.notif,\r\n        }\r\n    }\r\n}\r\n\r\nimpl PumpWorker for NotifWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    /// Emit this node's notif feed for `subnet` (the full semilattice spool)\r\n    /// and push it fire-and-forget; the receiver's join dedups.\r\n    // [impl->REQ-NOTIF-1]\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        _peer_hex: &str,\r\n        _ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        let store =\r\n            NotifStore::open_at(&self.notif_db).map_err(|e| io::Error::other(e.to_string()))?;\r\n        let feed = emit_notif_feed(&store, subnet).map_err(io::Error::other)?;\r\n        if !feed.is_empty() {\r\n            io.push_feed(&feed)?;\r\n        }\r\n        Ok(())\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\notif.rs"}}},"isError":false,"timestamp":1784271517738}}
{"type":"message","id":"ddbdbc10","parentId":"fdf6ad05","timestamp":"2026-07-17T06:58:37.740Z","message":{"role":"toolResult","toolCallId":"call_j3XWHrjDkfLEwU8SQgZ01UrO|fc_038f28d3e93bbfd4016a59d29da8748196aa28565d2e4c32a7","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/pump/\n## mod.rs\n 221|/// roots so two nodes can share one test process.\n*222|pub struct PumpPaths {\n 223|    /// The perch tree (registry advertisement scan).\n 224|    pub owlery: PathBuf,\n 225|    /// The context store root ([`ContextStore::open_or_init_in`]).\n...\n 268|\n*269|impl PumpPaths {\n 270|    /// The canonical SPT_HOME layout.\n 271|    pub fn production() -> PumpPaths {\n 272|        let home = perch::spt_home();\n...\n 378|    /// wake claims exactly one forced round. Default: no wake edge.\n*379|    fn poll_wake(&mut self) -> bool {\n 380|        false\n 381|    }\n 382|\n...\n 384|    /// Default: nothing.\n*385|    fn pre_round(&mut self, _ctx: &RoundCtx) {}\n 386|\n 387|    /// The per-peer step for one `(subnet, peer)` under this round's context. A\n 388|    /// failure aborts the remaining due workers for THIS peer and drops its\n 389|    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\n*390|    fn peer_step(\n 391|        &mut self,\n 392|        io: &mut PeerIo,\n 393|        subnet: &str,\n...\n 404|/// construction-captured by each worker).\n*405|struct RoundCtx {\n 406|    /// `subnet.json` — the member subnets iterated for fan-out.\n 407|    subnets: SubnetStore,\n 408|    /// `attachment.json` — a detached subnet is skipped wholesale (REQ-SUBNET-5).\n...\n 414|\n*415|impl RoundCtx {\n 416|    /// Load the per-round shared stores from the pump's paths (the access-gate\n 417|    /// freshness stance — every config-ish store re-read per round so an edit\n 418|    /// takes effect within one tick, no restart).\n...\n 458|#[cfg(test)]\n*459|impl RoundCtx {\n 460|    /// A minimal empty round context for worker unit tests whose `pre_round` /\n 461|    /// `peer_step` ignore it (the registry leg keys off its own state, not the\n 462|    /// shared loads).\n...\n 755|/// peer_step (the registry sweep + advertise; the other legs default no-op).\n*756|fn pre_rounds(workers: &mut [&mut dyn PumpWorker], due_flags: &[bool], ctx: &RoundCtx) {\n 757|    for i in 0..workers.len() {\n 758|        if due_flags[i] {\n 759|            workers[i].pre_round(ctx);\n...\n 1374|        }\n*1375|        fn poll_wake(&mut self) -> bool {\n 1376|            self.wake\n 1377|        }\n*1378|        fn pre_round(&mut self, _ctx: &RoundCtx) {\n 1379|            self.log.borrow_mut().push(format!(\"pre:{}\", self.tag));\n 1380|        }\n*1381|        fn peer_step(\n 1382|            &mut self,\n 1383|            _io: &mut PeerIo,\n 1384|            _subnet: &str,\n## notif.rs\n 44|    // [impl->REQ-NOTIF-1]\n*45|    fn peer_step(\n 46|        &mut self,\n 47|        io: &mut PeerIo,\n 48|        subnet: &str,\n## registry.rs\n 119|    // [impl->REQ-CONV-2]\n*120|    fn poll_wake(&mut self) -> bool {\n 121|        crate::registryhost::take_advertise_now(&self.advertise_marker)\n 122|    }\n 123|\n...\n 125|    /// (§Frozen-7, order preserved verbatim):\n*126|    fn pre_round(&mut self, _ctx: &RoundCtx) {\n 127|        // Consume the re-pair eviction marker FIRST (M8 decision 13,\n 128|        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\n 129|        // tick's snapshot mirror can resurrect the pairing host's disk-side\n...\n 160|    /// stream per feed.\n*161|    fn peer_step(\n 162|        &mut self,\n 163|        io: &mut PeerIo,\n 164|        subnet: &str,\n...\n 227|    #[test]\n*228|    fn poll_wake_takes_the_advertise_marker_exactly_once() {\n 229|        let dir = tempfile::tempdir().unwrap();\n 230|        let marker = dir.path().join(\"advertise-now\");\n 231|        let mut w = RegistryWorker {\n...\n 257|    #[test]\n*258|    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\n 259|        with_home(|home| {\n 260|            // A served subnet plus one advertisable Self perch.\n 261|            let owlery = home.join(\"owlery\");\n## sync.rs\n 74|    // [impl->REQ-INST-3]\n*75|    fn poll_wake(&mut self) -> bool {\n 76|        crate::resting::take_freshness_pull(&self.pull_marker)\n 77|    }\n 78|\n...\n 85|    // [impl->REQ-INST-5]\n*86|    fn peer_step(\n 87|        &mut self,\n 88|        io: &mut PeerIo,\n 89|        subnet: &str,\n## update.rs\n 84|\n*85|    fn peer_step(\n 86|        &mut self,\n 87|        io: &mut PeerIo,\n 88|        subnet: &str,"}],"details":{"scopePath":"crates/spt-daemon/src/pump","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":21,"fileCount":5,"files":["crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/pump/notif.rs","crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/pump/sync.rs","crates/spt-daemon/src/pump/update.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/pump/mod.rs","count":12},{"path":"crates/spt-daemon/src/pump/notif.rs","count":1},{"path":"crates/spt-daemon/src/pump/registry.rs","count":5},{"path":"crates/spt-daemon/src/pump/sync.rs","count":2},{"path":"crates/spt-daemon/src/pump/update.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/pump/\n## mod.rs\n  221│/// roots so two nodes can share one test process.\n *222│pub struct PumpPaths {\n  223│    /// The perch tree (registry advertisement scan).\n  224│    pub owlery: PathBuf,\n  225│    /// The context store root ([`ContextStore::open_or_init_in`]).\n     │...\n  268│\n *269│impl PumpPaths {\n  270│    /// The canonical SPT_HOME layout.\n  271│    pub fn production() -> PumpPaths {\n  272│        let home = perch::spt_home();\n     │...\n  378│    /// wake claims exactly one forced round. Default: no wake edge.\n *379│    fn poll_wake(&mut self) -> bool {\n  380│        false\n  381│    }\n  382│\n     │...\n  384│    /// Default: nothing.\n *385│    fn pre_round(&mut self, _ctx: &RoundCtx) {}\n  386│\n  387│    /// The per-peer step for one `(subnet, peer)` under this round's context. A\n  388│    /// failure aborts the remaining due workers for THIS peer and drops its\n  389│    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\n *390│    fn peer_step(\n  391│        &mut self,\n  392│        io: &mut PeerIo,\n  393│        subnet: &str,\n     │...\n  404│/// construction-captured by each worker).\n *405│struct RoundCtx {\n  406│    /// `subnet.json` — the member subnets iterated for fan-out.\n  407│    subnets: SubnetStore,\n  408│    /// `attachment.json` — a detached subnet is skipped wholesale (REQ-SUBNET-5).\n     │...\n  414│\n *415│impl RoundCtx {\n  416│    /// Load the per-round shared stores from the pump's paths (the access-gate\n  417│    /// freshness stance — every config-ish store re-read per round so an edit\n  418│    /// takes effect within one tick, no restart).\n     │...\n  458│#[cfg(test)]\n *459│impl RoundCtx {\n  460│    /// A minimal empty round context for worker unit tests whose `pre_round` /\n  461│    /// `peer_step` ignore it (the registry leg keys off its own state, not the\n  462│    /// shared loads).\n     │...\n  755│/// peer_step (the registry sweep + advertise; the other legs default no-op).\n *756│fn pre_rounds(workers: &mut [&mut dyn PumpWorker], due_flags: &[bool], ctx: &RoundCtx) {\n  757│    for i in 0..workers.len() {\n  758│        if due_flags[i] {\n  759│            workers[i].pre_round(ctx);\n     │...\n 1374│        }\n*1375│        fn poll_wake(&mut self) -> bool {\n 1376│            self.wake\n 1377│        }\n*1378│        fn pre_round(&mut self, _ctx: &RoundCtx) {\n 1379│            self.log.borrow_mut().push(format!(\"pre:{}\", self.tag));\n 1380│        }\n*1381│        fn peer_step(\n 1382│            &mut self,\n 1383│            _io: &mut PeerIo,\n 1384│            _subnet: &str,\n## notif.rs\n 44│    // [impl->REQ-NOTIF-1]\n*45│    fn peer_step(\n 46│        &mut self,\n 47│        io: &mut PeerIo,\n 48│        subnet: &str,\n## registry.rs\n 119│    // [impl->REQ-CONV-2]\n*120│    fn poll_wake(&mut self) -> bool {\n 121│        crate::registryhost::take_advertise_now(&self.advertise_marker)\n 122│    }\n 123│\n    │...\n 125│    /// (§Frozen-7, order preserved verbatim):\n*126│    fn pre_round(&mut self, _ctx: &RoundCtx) {\n 127│        // Consume the re-pair eviction marker FIRST (M8 decision 13,\n 128│        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\n 129│        // tick's snapshot mirror can resurrect the pairing host's disk-side\n    │...\n 160│    /// stream per feed.\n*161│    fn peer_step(\n 162│        &mut self,\n 163│        io: &mut PeerIo,\n 164│        subnet: &str,\n    │...\n 227│    #[test]\n*228│    fn poll_wake_takes_the_advertise_marker_exactly_once() {\n 229│        let dir = tempfile::tempdir().unwrap();\n 230│        let marker = dir.path().join(\"advertise-now\");\n 231│        let mut w = RegistryWorker {\n    │...\n 257│    #[test]\n*258│    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\n 259│        with_home(|home| {\n 260│            // A served subnet plus one advertisable Self perch.\n 261│            let owlery = home.join(\"owlery\");\n## sync.rs\n 74│    // [impl->REQ-INST-3]\n*75│    fn poll_wake(&mut self) -> bool {\n 76│        crate::resting::take_freshness_pull(&self.pull_marker)\n 77│    }\n 78│\n   │...\n 85│    // [impl->REQ-INST-5]\n*86│    fn peer_step(\n 87│        &mut self,\n 88│        io: &mut PeerIo,\n 89│        subnet: &str,\n## update.rs\n 84│\n*85│    fn peer_step(\n 86│        &mut self,\n 87│        io: &mut PeerIo,\n 88│        subnet: &str,"},"isError":false,"timestamp":1784271517740}}
{"type":"message","id":"8ac44a02","parentId":"ddbdbc10","timestamp":"2026-07-17T06:58:37.740Z","message":{"role":"toolResult","toolCallId":"call_4eL5w3pKG1bwWBOvJ8zxRFO7|fc_038f28d3e93bbfd4016a59d29da8708196afe328de9ce5088f","toolName":"read","content":[{"type":"text","text":"//! The update pump worker (pump-seam P4) — peer-propagated self-update.\n//!\n//! `peer_step` runs the REQ-UPD-1/4/6 leg under the PRODUCTION gate: the\n//! VerifyPolicy floor arithmetic (keys + channel pin from `release-keys.json`,\n//! version floor = the staged release as the rollback gate, wall clock for\n//! expiry only), the [`request_update`] pull, and — on a fresh stage — the\n//! consent-notif production minted from the registry's CANONICAL epoch\n//! ([`RegistryHost::with_epoch`], the construction-captured Arc per §Frozen-4),\n//! plus the `record_last_outcome` convergence writes. No wake edge, no\n//! `pre_round`. Construction-captured deps: the registry Arc, the worker's\n//! path slices, and the `full_auto_update` consent knob.\n\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse spt_store::notif::NotifStore;\nuse spt_store::visibility::VisibilityStore;\n\nuse crate::consent::{decide, most_recently_active};\nuse crate::notif::{produce_consent_notif, NotifSurfacePolicy};\nuse crate::propagate::{request_update, UpdatePullOutcome};\nuse crate::registryhost::RegistryHost;\nuse crate::relcache::ReleaseCache;\nuse crate::release::VerifyPolicy;\nuse crate::update::BrokerAbi;\n\nuse super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n\n/// The update leg as a [`PumpWorker`]: the peer-propagation pull (REQ-UPD-1),\n/// consent-gated by default (REQ-UPD-4), with typed convergence outcomes\n/// (REQ-UPD-6).\npub(super) struct UpdateWorker {\n    /// The shared registry host — the consent notif mints from its CANONICAL\n    /// epoch (the epoch-lease invariant, §Frozen-4 construction capture).\n    registry: Arc<RegistryHost>,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    releases: PathBuf,\n    /// `release-keys.json` ([`VerifyPolicy::load_from`]).\n    release_keys: PathBuf,\n    /// Transient pull scratch.\n    scratch: PathBuf,\n    /// The perch tree (most-recently-active resolution for the consent target).\n    owlery: PathBuf,\n    /// `visibility.json` (the consent-notif surface gate).\n    vis: PathBuf,\n    /// The notif spool DB ([`NotifStore::open_at`]).\n    notif_db: PathBuf,\n    /// `true` = the explicit opt-in to prompt-free updates; default `false`\n    /// surfaces every staged release as a consent notif (REQ-UPD-4).\n    full_auto_update: bool,\n    /// This worker's scheduling period (`cadence.update`).\n    cadence: Duration,\n}\n\nimpl UpdateWorker {\n    /// Construction capture (§Frozen-4): the registry Arc, the worker's path\n    /// slices, the consent knob, and the cadence.\n    pub(super) fn new(\n        registry: Arc<RegistryHost>,\n        paths: &PumpPaths,\n        cadence: &PumpCadence,\n        full_auto_update: bool,\n    ) -> UpdateWorker {\n        UpdateWorker {\n            registry,\n            releases: paths.releases.clone(),\n            release_keys: paths.release_keys.clone(),\n            scratch: paths.scratch.clone(),\n            owlery: paths.owlery.clone(),\n            vis: paths.vis.clone(),\n            notif_db: paths.notif_db.clone(),\n            full_auto_update,\n            cadence: cadence.update,\n        }\n    }\n}\n\nimpl PumpWorker for UpdateWorker {\n    fn cadence(&self) -> Duration {\n        self.cadence\n    }\n\n    fn peer_step(\n        &mut self,\n        io: &mut PeerIo,\n        subnet: &str,\n        peer_hex: &str,\n        ctx: &RoundCtx,\n    ) -> io::Result<()> {\n        // The peer-propagation pull (REQ-UPD-1) under the PRODUCTION gate: keys\n        // + channel pin from release-keys.json (fail-closed when unprovisioned),\n        // version floor = the staged release (rollback gate), wall clock for\n        // expiry only.\n        let cache = ReleaseCache::open(&self.releases);\n        let base_policy = VerifyPolicy::load_from(&self.release_keys, 0, now_ms());\n        let current = if cache.staged_channel().as_deref()\n            == Some(base_policy.pinned_channel.as_str())\n        {\n            cache.staged_version().unwrap_or(0)\n        } else {\n            0\n        };\n        let policy = VerifyPolicy {\n            current_version: current,\n            ..base_policy\n        };\n        if policy.trusted_keys.is_empty() {\n            return Ok(());\n        }\n        let open_op = io.open_op()?;\n        match request_update(\n            &mut *io.brain,\n            io.conn_id,\n            open_op,\n            &BrokerAbi::current(),\n            &policy,\n            &cache,\n            &self.scratch,\n        )? {\n            UpdatePullOutcome::Updated(plan) => {\n                let version = cache\n                    .staged_version()\n                    .map(|v| v.to_string())\n                    .unwrap_or_else(|| \"?\".to_string());\n                eprintln!(\"UPDATE_STAGED:{version}:{:?} (from {})\", plan.class, peer_hex);\n                // The operator-facing version label: the signed metadata's\n                // semver (`product_version`) when present, else the monotonic\n                // counter — never a bare counter in the consent notif.\n                let version_label = match cache.staged_update().and_then(|s| s.product_version()) {\n                    Some(semver) => format!(\"v{semver}\"),\n                    None => format!(\"(release {version})\"),\n                };\n                // Convergence fact (M8 decision 19): the status query reports\n                // this as the last typed outcome.\n                // [impl->REQ-UPD-6]\n                let _ = cache.record_last_outcome(\"staged\");\n                // REQ-UPD-4: gated on user confirmation by default — the staged\n                // release surfaces as a consent notif (D8's producer); apply\n                // rides the existing self-update machinery once confirmed.\n                // Full-auto writes nothing.\n                let decision = decide(self.full_auto_update, most_recently_active(&self.owlery));\n                let surface = NotifSurfacePolicy {\n                    subnets: ctx\n                        .subnets\n                        .subnets\n                        .iter()\n                        .map(|s| (s.name.clone(), s.hide_new_endpoints))\n                        .collect(),\n                    vis: VisibilityStore::load_from(&self.vis),\n                };\n                let store = NotifStore::open_at(&self.notif_db)\n                    .map_err(|e| io::Error::other(e.to_string()))?;\n                // The consent notif's id MUST mint from the node's CANONICAL\n                // epoch (shared with the registry leg + the CLI notify front\n                // door), NOT `ops` (the stream-open op counter, a separate file)\n                // — else two counters issue the same `node:epoch` notif_id and\n                // collide (the rig presence-rung UNIQUE failure).\n                // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]\n                let produced = self.registry.with_epoch(|ep| {\n                    produce_consent_notif(\n                        &decision,\n                        &version_label,\n                        &store,\n                        self.registry.node_hex(),\n                        ep,\n                        subnet,\n                        &surface,\n                        &self.owlery,\n                        now_ms(),\n                    )\n                });\n                if let Err(e) = produced {\n                    eprintln!(\"UPDATE_NOTIF_FAIL:{e}\");\n                }\n            }\n            UpdatePullOutcome::UpToDate => {}\n            UpdatePullOutcome::Rejected(reason) => {\n                // Loud + typed, never silent (ADR-0004 §D).\n                eprintln!(\"UPDATE_REJECTED:{peer_hex}:{reason}\");\n                // Convergence fact (M8 decision 19) — `Rejected{reason}` in the\n                // table.\n                // [impl->REQ-UPD-6]\n                let _ = cache.record_last_outcome(&format!(\"rejected:{reason}\"));\n            }\n        }\n        Ok(())\n    }\n}\n"}],"details":{"displayContent":{"text":"//! The update pump worker (pump-seam P4) — peer-propagated self-update.\r\n//!\r\n//! `peer_step` runs the REQ-UPD-1/4/6 leg under the PRODUCTION gate: the\r\n//! VerifyPolicy floor arithmetic (keys + channel pin from `release-keys.json`,\r\n//! version floor = the staged release as the rollback gate, wall clock for\r\n//! expiry only), the [`request_update`] pull, and — on a fresh stage — the\r\n//! consent-notif production minted from the registry's CANONICAL epoch\r\n//! ([`RegistryHost::with_epoch`], the construction-captured Arc per §Frozen-4),\r\n//! plus the `record_last_outcome` convergence writes. No wake edge, no\r\n//! `pre_round`. Construction-captured deps: the registry Arc, the worker's\r\n//! path slices, and the `full_auto_update` consent knob.\r\n\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::sync::Arc;\r\nuse std::time::Duration;\r\n\r\nuse spt_store::notif::NotifStore;\r\nuse spt_store::visibility::VisibilityStore;\r\n\r\nuse crate::consent::{decide, most_recently_active};\r\nuse crate::notif::{produce_consent_notif, NotifSurfacePolicy};\r\nuse crate::propagate::{request_update, UpdatePullOutcome};\r\nuse crate::registryhost::RegistryHost;\r\nuse crate::relcache::ReleaseCache;\r\nuse crate::release::VerifyPolicy;\r\nuse crate::update::BrokerAbi;\r\n\r\nuse super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\r\n\r\n/// The update leg as a [`PumpWorker`]: the peer-propagation pull (REQ-UPD-1),\r\n/// consent-gated by default (REQ-UPD-4), with typed convergence outcomes\r\n/// (REQ-UPD-6).\r\npub(super) struct UpdateWorker {\r\n    /// The shared registry host — the consent notif mints from its CANONICAL\r\n    /// epoch (the epoch-lease invariant, §Frozen-4 construction capture).\r\n    registry: Arc<RegistryHost>,\r\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\r\n    releases: PathBuf,\r\n    /// `release-keys.json` ([`VerifyPolicy::load_from`]).\r\n    release_keys: PathBuf,\r\n    /// Transient pull scratch.\r\n    scratch: PathBuf,\r\n    /// The perch tree (most-recently-active resolution for the consent target).\r\n    owlery: PathBuf,\r\n    /// `visibility.json` (the consent-notif surface gate).\r\n    vis: PathBuf,\r\n    /// The notif spool DB ([`NotifStore::open_at`]).\r\n    notif_db: PathBuf,\r\n    /// `true` = the explicit opt-in to prompt-free updates; default `false`\r\n    /// surfaces every staged release as a consent notif (REQ-UPD-4).\r\n    full_auto_update: bool,\r\n    /// This worker's scheduling period (`cadence.update`).\r\n    cadence: Duration,\r\n}\r\n\r\nimpl UpdateWorker {\r\n    /// Construction capture (§Frozen-4): the registry Arc, the worker's path\r\n    /// slices, the consent knob, and the cadence.\r\n    pub(super) fn new(\r\n        registry: Arc<RegistryHost>,\r\n        paths: &PumpPaths,\r\n        cadence: &PumpCadence,\r\n        full_auto_update: bool,\r\n    ) -> UpdateWorker {\r\n        UpdateWorker {\r\n            registry,\r\n            releases: paths.releases.clone(),\r\n            release_keys: paths.release_keys.clone(),\r\n            scratch: paths.scratch.clone(),\r\n            owlery: paths.owlery.clone(),\r\n            vis: paths.vis.clone(),\r\n            notif_db: paths.notif_db.clone(),\r\n            full_auto_update,\r\n            cadence: cadence.update,\r\n        }\r\n    }\r\n}\r\n\r\nimpl PumpWorker for UpdateWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        peer_hex: &str,\r\n        ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        // The peer-propagation pull (REQ-UPD-1) under the PRODUCTION gate: keys\r\n        // + channel pin from release-keys.json (fail-closed when unprovisioned),\r\n        // version floor = the staged release (rollback gate), wall clock for\r\n        // expiry only.\r\n        let cache = ReleaseCache::open(&self.releases);\r\n        let base_policy = VerifyPolicy::load_from(&self.release_keys, 0, now_ms());\r\n        let current = if cache.staged_channel().as_deref()\r\n            == Some(base_policy.pinned_channel.as_str())\r\n        {\r\n            cache.staged_version().unwrap_or(0)\r\n        } else {\r\n            0\r\n        };\r\n        let policy = VerifyPolicy {\r\n            current_version: current,\r\n            ..base_policy\r\n        };\r\n        if policy.trusted_keys.is_empty() {\r\n            return Ok(());\r\n        }\r\n        let open_op = io.open_op()?;\r\n        match request_update(\r\n            &mut *io.brain,\r\n            io.conn_id,\r\n            open_op,\r\n            &BrokerAbi::current(),\r\n            &policy,\r\n            &cache,\r\n            &self.scratch,\r\n        )? {\r\n            UpdatePullOutcome::Updated(plan) => {\r\n                let version = cache\r\n                    .staged_version()\r\n                    .map(|v| v.to_string())\r\n                    .unwrap_or_else(|| \"?\".to_string());\r\n                eprintln!(\"UPDATE_STAGED:{version}:{:?} (from {})\", plan.class, peer_hex);\r\n                // The operator-facing version label: the signed metadata's\r\n                // semver (`product_version`) when present, else the monotonic\r\n                // counter — never a bare counter in the consent notif.\r\n                let version_label = match cache.staged_update().and_then(|s| s.product_version()) {\r\n                    Some(semver) => format!(\"v{semver}\"),\r\n                    None => format!(\"(release {version})\"),\r\n                };\r\n                // Convergence fact (M8 decision 19): the status query reports\r\n                // this as the last typed outcome.\r\n                // [impl->REQ-UPD-6]\r\n                let _ = cache.record_last_outcome(\"staged\");\r\n                // REQ-UPD-4: gated on user confirmation by default — the staged\r\n                // release surfaces as a consent notif (D8's producer); apply\r\n                // rides the existing self-update machinery once confirmed.\r\n                // Full-auto writes nothing.\r\n                let decision = decide(self.full_auto_update, most_recently_active(&self.owlery));\r\n                let surface = NotifSurfacePolicy {\r\n                    subnets: ctx\r\n                        .subnets\r\n                        .subnets\r\n                        .iter()\r\n                        .map(|s| (s.name.clone(), s.hide_new_endpoints))\r\n                        .collect(),\r\n                    vis: VisibilityStore::load_from(&self.vis),\r\n                };\r\n                let store = NotifStore::open_at(&self.notif_db)\r\n                    .map_err(|e| io::Error::other(e.to_string()))?;\r\n                // The consent notif's id MUST mint from the node's CANONICAL\r\n                // epoch (shared with the registry leg + the CLI notify front\r\n                // door), NOT `ops` (the stream-open op counter, a separate file)\r\n                // — else two counters issue the same `node:epoch` notif_id and\r\n                // collide (the rig presence-rung UNIQUE failure).\r\n                // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]\r\n                let produced = self.registry.with_epoch(|ep| {\r\n                    produce_consent_notif(\r\n                        &decision,\r\n                        &version_label,\r\n                        &store,\r\n                        self.registry.node_hex(),\r\n                        ep,\r\n                        subnet,\r\n                        &surface,\r\n                        &self.owlery,\r\n                        now_ms(),\r\n                    )\r\n                });\r\n                if let Err(e) = produced {\r\n                    eprintln!(\"UPDATE_NOTIF_FAIL:{e}\");\r\n                }\r\n            }\r\n            UpdatePullOutcome::UpToDate => {}\r\n            UpdatePullOutcome::Rejected(reason) => {\r\n                // Loud + typed, never silent (ADR-0004 §D).\r\n                eprintln!(\"UPDATE_REJECTED:{peer_hex}:{reason}\");\r\n                // Convergence fact (M8 decision 19) — `Rejected{reason}` in the\r\n                // table.\r\n                // [impl->REQ-UPD-6]\r\n                let _ = cache.record_last_outcome(&format!(\"rejected:{reason}\"));\r\n            }\r\n        }\r\n        Ok(())\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\update.rs"}}},"isError":false,"timestamp":1784271517740}}
{"type":"message","id":"da57f24a","parentId":"8ac44a02","timestamp":"2026-07-17T06:58:37.744Z","message":{"role":"toolResult","toolCallId":"call_8XzFl3nM6QRRCTH0VZ9EXzJU|fc_038f28d3e93bbfd4016a59d29da85081969a6950fce6d4217b","toolName":"read","content":[{"type":"text","text":"//! The registry pump worker (pump-seam P2) — the first leg cut behind the\n//! [`super::PumpWorker`] seam.\n//!\n//! `poll_wake` consumes the event-driven advertisement marker (force a round\n//! NOW on an endpoint online/offline transition); `pre_round` runs the\n//! node-local sweep (repair-evictions → silent-peer eviction → due seed\n//! rotations → re-advertise) ONCE per due round and holds the produced adverts\n//! as worker state; `peer_step` pushes this node's own rows for the given\n//! subnet over the peer's feed stream. Construction-captured deps: the shared\n//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the\n//! two registry cadence knobs.\n\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse spt_net::net::replicate::RegistryFeedRecord;\nuse spt_store::subnet::SubnetStore;\n\nuse crate::registryhost::RegistryHost;\n\nuse super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n\n/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,\n/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the\n/// prior generation one-deep for the re-seed grace) and clear its pending entry.\n/// The new seed reaches benign offliners over the member control stream's\n/// re-seed leg on their next connect — never over gossip; the revokee's\n/// already-propagated tombstone denies it the grace. Pure-store work (no conn\n/// handle needed): the daemon re-reads `subnet.json` per connect, so the bump\n/// takes effect without a restart.\n// [impl->REQ-MESH-4]\nfn fire_due_rotations(subnets_path: &std::path::Path, rotations_path: &std::path::Path, now_ms: u64) {\n    let mut pending = spt_store::rotation::RotationPending::load_from(rotations_path);\n    let due = pending.due_subnets(now_ms);\n    if due.is_empty() {\n        return;\n    }\n    let mut subnets = SubnetStore::load_from(subnets_path);\n    let mut rotated = false;\n    for sub in &due {\n        match subnets.rotate_seed(sub) {\n            Ok(rec) => {\n                eprintln!(\"SEED_ROTATED:{sub}:epoch={} (revoke window closed)\", rec.epoch);\n                rotated = true;\n            }\n            // The subnet may have been left/renamed since the revoke — drop the\n            // stale entry rather than retrying it forever.\n            Err(e) => eprintln!(\"SEED_ROTATE_SKIP:{sub}: {e}\"),\n        }\n        pending.clear(sub);\n    }\n    if rotated {\n        if let Err(e) = subnets.save_to(subnets_path) {\n            eprintln!(\"SEED_ROTATE_FAIL: {e}\");\n            return; // keep the pending entry — retry next tick rather than lose it\n        }\n    }\n    let _ = pending.save_to(rotations_path);\n}\n\n/// The registry leg as a [`PumpWorker`]: own-row re-advertisement\n/// ([`RegistryHost::advertise_local`] → one feed stream per peer). The node\n/// re-advertises its own rows; peers reconverge within one cadence\n/// (replay-is-recovery). Own rows only — a relayed third-party row would carry\n/// an origin the receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10).\npub(super) struct RegistryWorker {\n    /// The shared registry host (sweep + advertise + the canonical epoch).\n    registry: Arc<RegistryHost>,\n    /// The perch tree scanned for advertisable rows.\n    owlery: PathBuf,\n    /// `subnet.json` — read by the due-rotation leg.\n    subnets: PathBuf,\n    /// `rotation-pending.json` — the timeboxed seed-rotation schedule.\n    rotations: PathBuf,\n    /// The event-driven advertisement marker (`poll_wake`).\n    advertise_marker: PathBuf,\n    /// This worker's scheduling period (`cadence.registry`).\n    cadence: Duration,\n    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10).\n    evict_after: Duration,\n    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\n    adverts: Vec<RegistryFeedRecord>,\n}\n\nimpl RegistryWorker {\n    /// Construction capture (§Frozen-4): the shared registry plus this worker's\n    /// `PathBuf` slices and cadence knobs out of the pump's wiring.\n    pub(super) fn new(\n        registry: Arc<RegistryHost>,\n        paths: &PumpPaths,\n        cadence: &PumpCadence,\n    ) -> RegistryWorker {\n        RegistryWorker {\n            registry,\n            owlery: paths.owlery.clone(),\n            subnets: paths.subnets.clone(),\n            rotations: paths.rotations.clone(),\n            advertise_marker: paths.advertise_marker.clone(),\n            cadence: cadence.registry,\n            evict_after: cadence.registry_evict_after,\n            adverts: Vec::new(),\n        }\n    }\n}\n\nimpl PumpWorker for RegistryWorker {\n    fn cadence(&self) -> Duration {\n        self.cadence\n    }\n\n    /// Event-driven advertisement (M8 decision 15, REQ-CONV-2): an endpoint\n    /// online/offline transition dropped the marker — consume it and run the\n    /// registry leg NOW. A wake of the pump loop, not a second advertisement\n    /// path: the forced round is the same advertise_local + push code, so the\n    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\n    /// so one wake claims exactly one forced round.\n    // [impl->REQ-CONV-2]\n    fn poll_wake(&mut self) -> bool {\n        crate::registryhost::take_advertise_now(&self.advertise_marker)\n    }\n\n    /// The node-local sweep, run ONCE per due round before any `peer_step`\n    /// (§Frozen-7, order preserved verbatim):\n    fn pre_round(&mut self, _ctx: &RoundCtx) {\n        // Consume the re-pair eviction marker FIRST (M8 decision 13,\n        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\n        // tick's snapshot mirror can resurrect the pairing host's disk-side\n        // eviction.\n        // [impl->REQ-SUBNET-7]\n        self.registry.consume_repair_evictions();\n        // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-HAZARDS\n        // 4.10): a dead node identity's rows decay instead of poisoning bare-id\n        // resolution forever; a live peer re-stamps its heard entry with every\n        // admitted feed and is never touched.\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n        self.registry.evict_silent_peers(self.evict_after);\n        // Trigger (b) — per-row Offline-TTL (#2-secondary): the SAME sweep also\n        // evicts rows that have been non-routable (Offline) beyond the grace even\n        // while their author node is alive (a purge/erase Offline ghost that\n        // ghost-heal re-advertised once and whole-node eviction never reaps). Same\n        // registry_evict_after grace; own rows never decay; the receiver-observed\n        // offline_since is self-pruning so a whole-node eviction above drops its\n        // stamps for free.\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n        self.registry.evict_aged_offline_rows(self.evict_after);\n        // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window has\n        // closed — after the eviction sweep, before re-advertising. The\n        // re-minted seed rides the member control stream on the next connect;\n        // the revokee's tombstone (already propagated) keeps it locked out.\n        fire_due_rotations(&self.subnets, &self.rotations, now_ms());\n        // Re-advertise this node's own rows; held as worker state for the\n        // per-subnet `peer_step` filter below.\n        self.adverts = self.registry.advertise_local(&self.owlery);\n    }\n\n    /// Push this node's own rows for `subnet` over the peer's feed stream\n    /// (per-subnet filter of the `pre_round` adverts). Fire-and-forget, one\n    /// stream per feed.\n    fn peer_step(\n        &mut self,\n        io: &mut PeerIo,\n        subnet: &str,\n        _peer_hex: &str,\n        _ctx: &RoundCtx,\n    ) -> io::Result<()> {\n        let lines: Vec<u8> = self\n            .adverts\n            .iter()\n            .filter(|u| u.subnet() == subnet)\n            .flat_map(|u| u.encode_line())\n            .collect();\n        if !lines.is_empty() {\n            io.push_feed(&lines)?;\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_home::with_home;\n    use spt_store::epoch::EpochSource;\n\n    // [unit->REQ-MESH-4] the pump fires a seed rotation only once its window has\n    // closed: a due subnet rotates (epoch bumps, prior seed retained one-deep)\n    // and its pending entry clears; a not-yet-due subnet is untouched and stays\n    // scheduled. This is the daemon-side half of the coalescing window.\n    #[test]\n    fn fire_due_rotations_rotates_due_and_leaves_pending() {\n        let dir = tempfile::tempdir().unwrap();\n        let subnets_path = dir.path().join(\"subnet.json\");\n        let rotations_path = dir.path().join(\"rotation-pending.json\");\n\n        let mut subnets = SubnetStore::default();\n        let before_home = subnets.create_subnet(\"home\").unwrap().clone();\n        subnets.create_subnet(\"work\").unwrap();\n        subnets.save_to(&subnets_path).unwrap();\n\n        let mut pending = spt_store::rotation::RotationPending::default();\n        pending.coalesce(\"home\", &[\"aa\".to_string()], 1_000); // due at now=1_000\n        pending.coalesce(\"work\", &[\"bb\".to_string()], 5_000); // not yet due\n        pending.save_to(&rotations_path).unwrap();\n\n        fire_due_rotations(&subnets_path, &rotations_path, 1_000);\n\n        let after = SubnetStore::load_from(&subnets_path);\n        let home = after.find(\"home\").unwrap();\n        assert_eq!(home.epoch, before_home.epoch + 1, \"due subnet rotated\");\n        assert_eq!(\n            home.prev_seed_hex.as_deref(),\n            Some(before_home.seed_hex.as_str()),\n            \"prior seed retained for the grace\"\n        );\n        assert_eq!(after.find(\"work\").unwrap().epoch, 1, \"not-due subnet untouched\");\n\n        let pend = spt_store::rotation::RotationPending::load_from(&rotations_path);\n        assert!(!pend.subnets.contains_key(\"home\"), \"fired entry cleared\");\n        assert!(pend.subnets.contains_key(\"work\"), \"pending entry kept\");\n    }\n\n    // [unit->REQ-CONV-2] the worker's `poll_wake` consumes the advertise-now\n    // marker exactly once — one online/offline transition forces exactly one\n    // registry round, and an absent marker forces nothing.\n    #[test]\n    fn poll_wake_takes_the_advertise_marker_exactly_once() {\n        let dir = tempfile::tempdir().unwrap();\n        let marker = dir.path().join(\"advertise-now\");\n        let mut w = RegistryWorker {\n            registry: Arc::new(RegistryHost::new_at(\n                \"aa11\",\n                EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n                dir.path().join(\"registry\"),\n            )),\n            owlery: dir.path().join(\"owlery\"),\n            subnets: dir.path().join(\"subnet.json\"),\n            rotations: dir.path().join(\"rotation-pending.json\"),\n            advertise_marker: marker.clone(),\n            cadence: Duration::from_secs(30),\n            evict_after: Duration::from_secs(600),\n            adverts: Vec::new(),\n        };\n        assert!(!w.poll_wake(), \"nothing to take yet\");\n        std::fs::write(&marker, b\"\").unwrap();\n        assert!(w.poll_wake(), \"first take claims the transition\");\n        assert!(!w.poll_wake(), \"second take gets nothing\");\n    }\n\n    // [unit->REQ-MESH-4] `pre_round` runs the full node-local sweep once per due\n    // round: a scheduled seed rotation fires (epoch bumps) and adverts are\n    // recomputed each call (a fresh epoch per advertisement — the once-per-round\n    // re-stamp). The per-step ORDER (evict → rotate → advertise) is preserved\n    // verbatim from the pre-seam inline loop; each step keeps its own\n    // registryhost-side unit coverage.\n    #[test]\n    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\n        with_home(|home| {\n            // A served subnet plus one advertisable Self perch.\n            let owlery = home.join(\"owlery\");\n            let id = \"adv-ag\";\n            let p = spt_store::perch::resolve_perch_path_in(\n…\n            );\n…\n        });\n    }\n}\n\n[Showing lines 1-263 of 334. Use :264 to continue]"}],"details":{"truncation":{"content":"//! The registry pump worker (pump-seam P2) — the first leg cut behind the\r\n//! [`super::PumpWorker`] seam.\r\n//!\r\n//! `poll_wake` consumes the event-driven advertisement marker (force a round\r\n//! NOW on an endpoint online/offline transition); `pre_round` runs the\r\n//! node-local sweep (repair-evictions → silent-peer eviction → due seed\r\n//! rotations → re-advertise) ONCE per due round and holds the produced adverts\r\n//! as worker state; `peer_step` pushes this node's own rows for the given\r\n//! subnet over the peer's feed stream. Construction-captured deps: the shared\r\n//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the\r\n//! two registry cadence knobs.\r\n\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::sync::Arc;\r\nuse std::time::Duration;\r\n\r\nuse spt_net::net::replicate::RegistryFeedRecord;\r\nuse spt_store::subnet::SubnetStore;\r\n\r\nuse crate::registryhost::RegistryHost;\r\n\r\nuse super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\r\n\r\n/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,\r\n/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the\r\n/// prior generation one-deep for the re-seed grace) and clear its pending entry.\r\n/// The new seed reaches benign offliners over the member control stream's\r\n/// re-seed leg on their next connect — never over gossip; the revokee's\r\n/// already-propagated tombstone denies it the grace. Pure-store work (no conn\r\n/// handle needed): the daemon re-reads `subnet.json` per connect, so the bump\r\n/// takes effect without a restart.\r\n// [impl->REQ-MESH-4]\r\nfn fire_due_rotations(subnets_path: &std::path::Path, rotations_path: &std::path::Path, now_ms: u64) {\r\n    let mut pending = spt_store::rotation::RotationPending::load_from(rotations_path);\r\n    let due = pending.due_subnets(now_ms);\r\n    if due.is_empty() {\r\n        return;\r\n    }\r\n    let mut subnets = SubnetStore::load_from(subnets_path);\r\n    let mut rotated = false;\r\n    for sub in &due {\r\n        match subnets.rotate_seed(sub) {\r\n            Ok(rec) => {\r\n                eprintln!(\"SEED_ROTATED:{sub}:epoch={} (revoke window closed)\", rec.epoch);\r\n                rotated = true;\r\n            }\r\n            // The subnet may have been left/renamed since the revoke — drop the\r\n            // stale entry rather than retrying it forever.\r\n            Err(e) => eprintln!(\"SEED_ROTATE_SKIP:{sub}: {e}\"),\r\n        }\r\n        pending.clear(sub);\r\n    }\r\n    if rotated {\r\n        if let Err(e) = subnets.save_to(subnets_path) {\r\n            eprintln!(\"SEED_ROTATE_FAIL: {e}\");\r\n            return; // keep the pending entry — retry next tick rather than lose it\r\n        }\r\n    }\r\n    let _ = pending.save_to(rotations_path);\r\n}\r\n\r\n/// The registry leg as a [`PumpWorker`]: own-row re-advertisement\r\n/// ([`RegistryHost::advertise_local`] → one feed stream per peer). The node\r\n/// re-advertises its own rows; peers reconverge within one cadence\r\n/// (replay-is-recovery). Own rows only — a relayed third-party row would carry\r\n/// an origin the receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10).\r\npub(super) struct RegistryWorker {\r\n    /// The shared registry host (sweep + advertise + the canonical epoch).\r\n    registry: Arc<RegistryHost>,\r\n    /// The perch tree scanned for advertisable rows.\r\n    owlery: PathBuf,\r\n    /// `subnet.json` — read by the due-rotation leg.\r\n    subnets: PathBuf,\r\n    /// `rotation-pending.json` — the timeboxed seed-rotation schedule.\r\n    rotations: PathBuf,\r\n    /// The event-driven advertisement marker (`poll_wake`).\r\n    advertise_marker: PathBuf,\r\n    /// This worker's scheduling period (`cadence.registry`).\r\n    cadence: Duration,\r\n    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10).\r\n    evict_after: Duration,\r\n    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\r\n    adverts: Vec<RegistryFeedRecord>,\r\n}\r\n\r\nimpl RegistryWorker {\r\n    /// Construction capture (§Frozen-4): the shared registry plus this worker's\r\n    /// `PathBuf` slices and cadence knobs out of the pump's wiring.\r\n    pub(super) fn new(\r\n        registry: Arc<RegistryHost>,\r\n        paths: &PumpPaths,\r\n        cadence: &PumpCadence,\r\n    ) -> RegistryWorker {\r\n        RegistryWorker {\r\n            registry,\r\n            owlery: paths.owlery.clone(),\r\n            subnets: paths.subnets.clone(),\r\n            rotations: paths.rotations.clone(),\r\n            advertise_marker: paths.advertise_marker.clone(),\r\n            cadence: cadence.registry,\r\n            evict_after: cadence.registry_evict_after,\r\n            adverts: Vec::new(),\r\n        }\r\n    }\r\n}\r\n\r\nimpl PumpWorker for RegistryWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    /// Event-driven advertisement (M8 decision 15, REQ-CONV-2): an endpoint\r\n    /// online/offline transition dropped the marker — consume it and run the\r\n    /// registry leg NOW. A wake of the pump loop, not a second advertisement\r\n    /// path: the forced round is the same advertise_local + push code, so the\r\n    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\r\n    /// so one wake claims exactly one forced round.\r\n    // [impl->REQ-CONV-2]\r\n    fn poll_wake(&mut self) -> bool {\r\n        crate::registryhost::take_advertise_now(&self.advertise_marker)\r\n    }\r\n\r\n    /// The node-local sweep, run ONCE per due round before any `peer_step`\r\n    /// (§Frozen-7, order preserved verbatim):\r\n    fn pre_round(&mut self, _ctx: &RoundCtx) {\r\n        // Consume the re-pair eviction marker FIRST (M8 decision 13,\r\n        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\r\n        // tick's snapshot mirror can resurrect the pairing host's disk-side\r\n        // eviction.\r\n        // [impl->REQ-SUBNET-7]\r\n        self.registry.consume_repair_evictions();\r\n        // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-HAZARDS\r\n        // 4.10): a dead node identity's rows decay instead of poisoning bare-id\r\n        // resolution forever; a live peer re-stamps its heard entry with every\r\n        // admitted feed and is never touched.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_silent_peers(self.evict_after);\r\n        // Trigger (b) — per-row Offline-TTL (#2-secondary): the SAME sweep also\r\n        // evicts rows that have been non-routable (Offline) beyond the grace even\r\n        // while their author node is alive (a purge/erase Offline ghost that\r\n        // ghost-heal re-advertised once and whole-node eviction never reaps). Same\r\n        // registry_evict_after grace; own rows never decay; the receiver-observed\r\n        // offline_since is self-pruning so a whole-node eviction above drops its\r\n        // stamps for free.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_aged_offline_rows(self.evict_after);\r\n        // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window has\r\n        // closed — after the eviction sweep, before re-advertising. The\r\n        // re-minted seed rides the member control stream on the next connect;\r\n        // the revokee's tombstone (already propagated) keeps it locked out.\r\n        fire_due_rotations(&self.subnets, &self.rotations, now_ms());\r\n        // Re-advertise this node's own rows; held as worker state for the\r\n        // per-subnet `peer_step` filter below.\r\n        self.adverts = self.registry.advertise_local(&self.owlery);\r\n    }\r\n\r\n    /// Push this node's own rows for `subnet` over the peer's feed stream\r\n    /// (per-subnet filter of the `pre_round` adverts). Fire-and-forget, one\r\n    /// stream per feed.\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        _peer_hex: &str,\r\n        _ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        let lines: Vec<u8> = self\r\n            .adverts\r\n            .iter()\r\n            .filter(|u| u.subnet() == subnet)\r\n            .flat_map(|u| u.encode_line())\r\n            .collect();\r\n        if !lines.is_empty() {\r\n            io.push_feed(&lines)?;\r\n        }\r\n        Ok(())\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::epoch::EpochSource;\r\n\r\n    // [unit->REQ-MESH-4] the pump fires a seed rotation only once its window has\r\n    // closed: a due subnet rotates (epoch bumps, prior seed retained one-deep)\r\n    // and its pending entry clears; a not-yet-due subnet is untouched and stays\r\n    // scheduled. This is the daemon-side half of the coalescing window.\r\n    #[test]\r\n    fn fire_due_rotations_rotates_due_and_leaves_pending() {\r\n        let dir = tempfile::tempdir().unwrap();\r\n        let subnets_path = dir.path().join(\"subnet.json\");\r\n        let rotations_path = dir.path().join(\"rotation-pending.json\");\r\n\r\n        let mut subnets = SubnetStore::default();\r\n        let before_home = subnets.create_subnet(\"home\").unwrap().clone();\r\n        subnets.create_subnet(\"work\").unwrap();\r\n        subnets.save_to(&subnets_path).unwrap();\r\n\r\n        let mut pending = spt_store::rotation::RotationPending::default();\r\n        pending.coalesce(\"home\", &[\"aa\".to_string()], 1_000); // due at now=1_000\r\n        pending.coalesce(\"work\", &[\"bb\".to_string()], 5_000); // not yet due\r\n        pending.save_to(&rotations_path).unwrap();\r\n\r\n        fire_due_rotations(&subnets_path, &rotations_path, 1_000);\r\n\r\n        let after = SubnetStore::load_from(&subnets_path);\r\n        let home = after.find(\"home\").unwrap();\r\n        assert_eq!(home.epoch, before_home.epoch + 1, \"due subnet rotated\");\r\n        assert_eq!(\r\n            home.prev_seed_hex.as_deref(),\r\n            Some(before_home.seed_hex.as_str()),\r\n            \"prior seed retained for the grace\"\r\n        );\r\n        assert_eq!(after.find(\"work\").unwrap().epoch, 1, \"not-due subnet untouched\");\r\n\r\n        let pend = spt_store::rotation::RotationPending::load_from(&rotations_path);\r\n        assert!(!pend.subnets.contains_key(\"home\"), \"fired entry cleared\");\r\n        assert!(pend.subnets.contains_key(\"work\"), \"pending entry kept\");\r\n    }\r\n\r\n    // [unit->REQ-CONV-2] the worker's `poll_wake` consumes the advertise-now\r\n    // marker exactly once — one online/offline transition forces exactly one\r\n    // registry round, and an absent marker forces nothing.\r\n    #[test]\r\n    fn poll_wake_takes_the_advertise_marker_exactly_once() {\r\n        let dir = tempfile::tempdir().unwrap();\r\n        let marker = dir.path().join(\"advertise-now\");\r\n        let mut w = RegistryWorker {\r\n            registry: Arc::new(RegistryHost::new_at(\r\n                \"aa11\",\r\n                EpochSource::load_from(&dir.path().join(\"epoch.json\")),\r\n                dir.path().join(\"registry\"),\r\n            )),\r\n            owlery: dir.path().join(\"owlery\"),\r\n            subnets: dir.path().join(\"subnet.json\"),\r\n            rotations: dir.path().join(\"rotation-pending.json\"),\r\n            advertise_marker: marker.clone(),\r\n            cadence: Duration::from_secs(30),\r\n            evict_after: Duration::from_secs(600),\r\n            adverts: Vec::new(),\r\n        };\r\n        assert!(!w.poll_wake(), \"nothing to take yet\");\r\n        std::fs::write(&marker, b\"\").unwrap();\r\n        assert!(w.poll_wake(), \"first take claims the transition\");\r\n        assert!(!w.poll_wake(), \"second take gets nothing\");\r\n    }\r\n\r\n    // [unit->REQ-MESH-4] `pre_round` runs the full node-local sweep once per due\r\n    // round: a scheduled seed rotation fires (epoch bumps) and adverts are\r\n    // recomputed each call (a fresh epoch per advertisement — the once-per-round\r\n    // re-stamp). The per-step ORDER (evict → rotate → advertise) is preserved\r\n    // verbatim from the pre-seam inline loop; each step keeps its own\r\n    // registryhost-side unit coverage.\r\n    #[test]\r\n    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\r\n        with_home(|home| {\r\n            // A served subnet plus one advertisable Self perch.\r\n            let owlery = home.join(\"owlery\");\r\n            let id = \"adv-ag\";\r\n            let p = spt_store::perch::resolve_perch_path_in(\r","truncated":true,"truncatedBy":"lines","totalLines":334,"totalBytes":12158,"outputLines":263,"outputBytes":12158,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! The registry pump worker (pump-seam P2) — the first leg cut behind the\r\n//! [`super::PumpWorker`] seam.\r\n//!\r\n//! `poll_wake` consumes the event-driven advertisement marker (force a round\r\n//! NOW on an endpoint online/offline transition); `pre_round` runs the\r\n//! node-local sweep (repair-evictions → silent-peer eviction → due seed\r\n//! rotations → re-advertise) ONCE per due round and holds the produced adverts\r\n//! as worker state; `peer_step` pushes this node's own rows for the given\r\n//! subnet over the peer's feed stream. Construction-captured deps: the shared\r\n//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the\r\n//! two registry cadence knobs.\r\n\r\nuse std::io;\r\nuse std::path::PathBuf;\r\nuse std::sync::Arc;\r\nuse std::time::Duration;\r\n\r\nuse spt_net::net::replicate::RegistryFeedRecord;\r\nuse spt_store::subnet::SubnetStore;\r\n\r\nuse crate::registryhost::RegistryHost;\r\n\r\nuse super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\r\n\r\n/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,\r\n/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the\r\n/// prior generation one-deep for the re-seed grace) and clear its pending entry.\r\n/// The new seed reaches benign offliners over the member control stream's\r\n/// re-seed leg on their next connect — never over gossip; the revokee's\r\n/// already-propagated tombstone denies it the grace. Pure-store work (no conn\r\n/// handle needed): the daemon re-reads `subnet.json` per connect, so the bump\r\n/// takes effect without a restart.\r\n// [impl->REQ-MESH-4]\r\nfn fire_due_rotations(subnets_path: &std::path::Path, rotations_path: &std::path::Path, now_ms: u64) {\r\n    let mut pending = spt_store::rotation::RotationPending::load_from(rotations_path);\r\n    let due = pending.due_subnets(now_ms);\r\n    if due.is_empty() {\r\n        return;\r\n    }\r\n    let mut subnets = SubnetStore::load_from(subnets_path);\r\n    let mut rotated = false;\r\n    for sub in &due {\r\n        match subnets.rotate_seed(sub) {\r\n            Ok(rec) => {\r\n                eprintln!(\"SEED_ROTATED:{sub}:epoch={} (revoke window closed)\", rec.epoch);\r\n                rotated = true;\r\n            }\r\n            // The subnet may have been left/renamed since the revoke — drop the\r\n            // stale entry rather than retrying it forever.\r\n            Err(e) => eprintln!(\"SEED_ROTATE_SKIP:{sub}: {e}\"),\r\n        }\r\n        pending.clear(sub);\r\n    }\r\n    if rotated {\r\n        if let Err(e) = subnets.save_to(subnets_path) {\r\n            eprintln!(\"SEED_ROTATE_FAIL: {e}\");\r\n            return; // keep the pending entry — retry next tick rather than lose it\r\n        }\r\n    }\r\n    let _ = pending.save_to(rotations_path);\r\n}\r\n\r\n/// The registry leg as a [`PumpWorker`]: own-row re-advertisement\r\n/// ([`RegistryHost::advertise_local`] → one feed stream per peer). The node\r\n/// re-advertises its own rows; peers reconverge within one cadence\r\n/// (replay-is-recovery). Own rows only — a relayed third-party row would carry\r\n/// an origin the receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10).\r\npub(super) struct RegistryWorker {\r\n    /// The shared registry host (sweep + advertise + the canonical epoch).\r\n    registry: Arc<RegistryHost>,\r\n    /// The perch tree scanned for advertisable rows.\r\n    owlery: PathBuf,\r\n    /// `subnet.json` — read by the due-rotation leg.\r\n    subnets: PathBuf,\r\n    /// `rotation-pending.json` — the timeboxed seed-rotation schedule.\r\n    rotations: PathBuf,\r\n    /// The event-driven advertisement marker (`poll_wake`).\r\n    advertise_marker: PathBuf,\r\n    /// This worker's scheduling period (`cadence.registry`).\r\n    cadence: Duration,\r\n    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10).\r\n    evict_after: Duration,\r\n    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\r\n    adverts: Vec<RegistryFeedRecord>,\r\n}\r\n\r\nimpl RegistryWorker {\r\n    /// Construction capture (§Frozen-4): the shared registry plus this worker's\r\n    /// `PathBuf` slices and cadence knobs out of the pump's wiring.\r\n    pub(super) fn new(\r\n        registry: Arc<RegistryHost>,\r\n        paths: &PumpPaths,\r\n        cadence: &PumpCadence,\r\n    ) -> RegistryWorker {\r\n        RegistryWorker {\r\n            registry,\r\n            owlery: paths.owlery.clone(),\r\n            subnets: paths.subnets.clone(),\r\n            rotations: paths.rotations.clone(),\r\n            advertise_marker: paths.advertise_marker.clone(),\r\n            cadence: cadence.registry,\r\n            evict_after: cadence.registry_evict_after,\r\n            adverts: Vec::new(),\r\n        }\r\n    }\r\n}\r\n\r\nimpl PumpWorker for RegistryWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    /// Event-driven advertisement (M8 decision 15, REQ-CONV-2): an endpoint\r\n    /// online/offline transition dropped the marker — consume it and run the\r\n    /// registry leg NOW. A wake of the pump loop, not a second advertisement\r\n    /// path: the forced round is the same advertise_local + push code, so the\r\n    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\r\n    /// so one wake claims exactly one forced round.\r\n    // [impl->REQ-CONV-2]\r\n    fn poll_wake(&mut self) -> bool {\r\n        crate::registryhost::take_advertise_now(&self.advertise_marker)\r\n    }\r\n\r\n    /// The node-local sweep, run ONCE per due round before any `peer_step`\r\n    /// (§Frozen-7, order preserved verbatim):\r\n    fn pre_round(&mut self, _ctx: &RoundCtx) {\r\n        // Consume the re-pair eviction marker FIRST (M8 decision 13,\r\n        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\r\n        // tick's snapshot mirror can resurrect the pairing host's disk-side\r\n        // eviction.\r\n        // [impl->REQ-SUBNET-7]\r\n        self.registry.consume_repair_evictions();\r\n        // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-HAZARDS\r\n        // 4.10): a dead node identity's rows decay instead of poisoning bare-id\r\n        // resolution forever; a live peer re-stamps its heard entry with every\r\n        // admitted feed and is never touched.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_silent_peers(self.evict_after);\r\n        // Trigger (b) — per-row Offline-TTL (#2-secondary): the SAME sweep also\r\n        // evicts rows that have been non-routable (Offline) beyond the grace even\r\n        // while their author node is alive (a purge/erase Offline ghost that\r\n        // ghost-heal re-advertised once and whole-node eviction never reaps). Same\r\n        // registry_evict_after grace; own rows never decay; the receiver-observed\r\n        // offline_since is self-pruning so a whole-node eviction above drops its\r\n        // stamps for free.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_aged_offline_rows(self.evict_after);\r\n        // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window has\r\n        // closed — after the eviction sweep, before re-advertising. The\r\n        // re-minted seed rides the member control stream on the next connect;\r\n        // the revokee's tombstone (already propagated) keeps it locked out.\r\n        fire_due_rotations(&self.subnets, &self.rotations, now_ms());\r\n        // Re-advertise this node's own rows; held as worker state for the\r\n        // per-subnet `peer_step` filter below.\r\n        self.adverts = self.registry.advertise_local(&self.owlery);\r\n    }\r\n\r\n    /// Push this node's own rows for `subnet` over the peer's feed stream\r\n    /// (per-subnet filter of the `pre_round` adverts). Fire-and-forget, one\r\n    /// stream per feed.\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        _peer_hex: &str,\r\n        _ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        let lines: Vec<u8> = self\r\n            .adverts\r\n            .iter()\r\n            .filter(|u| u.subnet() == subnet)\r\n            .flat_map(|u| u.encode_line())\r\n            .collect();\r\n        if !lines.is_empty() {\r\n            io.push_feed(&lines)?;\r\n        }\r\n        Ok(())\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::epoch::EpochSource;\r\n\r\n    // [unit->REQ-MESH-4] the pump fires a seed rotation only once its window has\r\n    // closed: a due subnet rotates (epoch bumps, prior seed retained one-deep)\r\n    // and its pending entry clears; a not-yet-due subnet is untouched and stays\r\n    // scheduled. This is the daemon-side half of the coalescing window.\r\n    #[test]\r\n    fn fire_due_rotations_rotates_due_and_leaves_pending() {\r\n        let dir = tempfile::tempdir().unwrap();\r\n        let subnets_path = dir.path().join(\"subnet.json\");\r\n        let rotations_path = dir.path().join(\"rotation-pending.json\");\r\n\r\n        let mut subnets = SubnetStore::default();\r\n        let before_home = subnets.create_subnet(\"home\").unwrap().clone();\r\n        subnets.create_subnet(\"work\").unwrap();\r\n        subnets.save_to(&subnets_path).unwrap();\r\n\r\n        let mut pending = spt_store::rotation::RotationPending::default();\r\n        pending.coalesce(\"home\", &[\"aa\".to_string()], 1_000); // due at now=1_000\r\n        pending.coalesce(\"work\", &[\"bb\".to_string()], 5_000); // not yet due\r\n        pending.save_to(&rotations_path).unwrap();\r\n\r\n        fire_due_rotations(&subnets_path, &rotations_path, 1_000);\r\n\r\n        let after = SubnetStore::load_from(&subnets_path);\r\n        let home = after.find(\"home\").unwrap();\r\n        assert_eq!(home.epoch, before_home.epoch + 1, \"due subnet rotated\");\r\n        assert_eq!(\r\n            home.prev_seed_hex.as_deref(),\r\n            Some(before_home.seed_hex.as_str()),\r\n            \"prior seed retained for the grace\"\r\n        );\r\n        assert_eq!(after.find(\"work\").unwrap().epoch, 1, \"not-due subnet untouched\");\r\n\r\n        let pend = spt_store::rotation::RotationPending::load_from(&rotations_path);\r\n        assert!(!pend.subnets.contains_key(\"home\"), \"fired entry cleared\");\r\n        assert!(pend.subnets.contains_key(\"work\"), \"pending entry kept\");\r\n    }\r\n\r\n    // [unit->REQ-CONV-2] the worker's `poll_wake` consumes the advertise-now\r\n    // marker exactly once — one online/offline transition forces exactly one\r\n    // registry round, and an absent marker forces nothing.\r\n    #[test]\r\n    fn poll_wake_takes_the_advertise_marker_exactly_once() {\r\n        let dir = tempfile::tempdir().unwrap();\r\n        let marker = dir.path().join(\"advertise-now\");\r\n        let mut w = RegistryWorker {\r\n            registry: Arc::new(RegistryHost::new_at(\r\n                \"aa11\",\r\n                EpochSource::load_from(&dir.path().join(\"epoch.json\")),\r\n                dir.path().join(\"registry\"),\r\n            )),\r\n            owlery: dir.path().join(\"owlery\"),\r\n            subnets: dir.path().join(\"subnet.json\"),\r\n            rotations: dir.path().join(\"rotation-pending.json\"),\r\n            advertise_marker: marker.clone(),\r\n            cadence: Duration::from_secs(30),\r\n            evict_after: Duration::from_secs(600),\r\n            adverts: Vec::new(),\r\n        };\r\n        assert!(!w.poll_wake(), \"nothing to take yet\");\r\n        std::fs::write(&marker, b\"\").unwrap();\r\n        assert!(w.poll_wake(), \"first take claims the transition\");\r\n        assert!(!w.poll_wake(), \"second take gets nothing\");\r\n    }\r\n\r\n    // [unit->REQ-MESH-4] `pre_round` runs the full node-local sweep once per due\r\n    // round: a scheduled seed rotation fires (epoch bumps) and adverts are\r\n    // recomputed each call (a fresh epoch per advertisement — the once-per-round\r\n    // re-stamp). The per-step ORDER (evict → rotate → advertise) is preserved\r\n    // verbatim from the pre-seam inline loop; each step keeps its own\r\n    // registryhost-side unit coverage.\r\n    #[test]\r\n    fn pre_round_sweeps_and_refreshes_adverts_each_round() {\r\n        with_home(|home| {\r\n            // A served subnet plus one advertisable Self perch.\r\n            let owlery = home.join(\"owlery\");\r\n            let id = \"adv-ag\";\r\n            let p = spt_store::perch::resolve_perch_path_in(\r\n…\n            );\n…\n        });\n    }\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,null,267,null,331,332,333]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\registry.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":334,"totalBytes":12158,"outputLines":263,"outputBytes":12158,"shownRange":{"start":1,"end":263},"nextOffset":264}}},"isError":false,"timestamp":1784271517744}}
{"type":"message","id":"f64f7e56","parentId":"da57f24a","timestamp":"2026-07-17T06:58:46.609Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying needed RoundCtx and pump paths**"},{"type":"toolCall","id":"call_pC7j6crGdzLjPq7OJElyRL8P|fc_038f28d3e93bbfd4016a59d2a6814481969ffa72d75fc183d7","name":"read","arguments":{"i":"Reading pump shared state scans","path":"crates/spt-daemon/src/pump/mod.rs","selector":"210-440"}},{"type":"toolCall","id":"call_t4x2HJ4j6FSQH0i2VOCVy1Je|fc_038f28d3e93bbfd4016a59d2a6817c819684934d649023f9db","name":"read","arguments":{"i":"Reading registry advertisement","path":"crates/spt-daemon/src/registryhost.rs","selector":"350-480,760-850"}},{"type":"toolCall","id":"call_BgNJhusrlETY6XmwV72vwIxD|fc_038f28d3e93bbfd4016a59d2a681888196adc38186e35d17f9","name":"grep","arguments":{"i":"Tracing sync ref selection","pattern":"pub fn select_refs|fn select_refs|create_bundle|bundle|branches\\(|refs|read_dir|std::fs::read","path":"crates/spt-daemon/src/sync.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_NYakXDydijyDrS7bdty7jlJt|fc_038f28d3e93bbfd4016a59d2a6819081969df6d29e1b02defb","name":"grep","arguments":{"i":"Tracing notif feed storage","pattern":"pub fn emit_notif_feed|fn emit_notif_feed|fn list|pub fn list|read_to_string|std::fs::read","path":"crates/spt-daemon/src/notifsync.rs; crates/spt-store/src/notif.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_vVTselAEQrgjjzELuOu81egg|fc_038f28d3e93bbfd4016a59d2a68198819681576c4f08438d51","name":"read","arguments":{"i":"Reading history implementation","path":"crates/spt-live/src/history.rs","selector":"1-155"}},{"type":"toolCall","id":"call_kOmXQVaoNyQHELLixJ2hYKQa|fc_038f28d3e93bbfd4016a59d2a681a08196a4d35ebc698fa3b0","name":"read","arguments":{"i":"Reading drop ingest implementation","path":"crates/spt-live/src/ingest.rs","selector":"180-280"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":9880,"output":344,"cacheRead":139776,"cacheWrite":0,"totalTokens":150000,"reasoningTokens":13,"cost":{"input":0.049400000000000006,"output":0.010320000000000001,"cacheRead":0.06988799999999999,"cacheWrite":0,"total":0.129608}},"stopReason":"toolUse","timestamp":1784271517757,"responseId":"resp_038f28d3e93bbfd4016a59d29e0f1c8196ad0e23b0d9abcfa3","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2a03cf48196be9a48f96ce96299","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdKm09D5mSVi_mQKIsQfLRU50N-GqkUiLu4ELvS4ouT4WiGN164-9W1gvLqP1juNE92WzzgnoafzhrNjQq-Vw1sLlmQ1wmmyy41SOUBziFvBHp1-deQMcHdopGG1LCN1Tvu537C__k0VcKQsZWZMZ47snCNH2KqO5kpjaMmPTMgsg-TxCzb3sDRFDd7mwKr3QiMCkZdprNTKEbI0GfGVKfRxn-XxtkPHjF4oIeCK7ujEUjoVKOoHpgsTdWGk0vhra0zkkZ3C4t_pChHOZSWrC-14bcjyWIhiDNjmdnbw_3E9MvYQtVuZdIBPhZkojj_CdtVx0RZignKaR7iJu2EobZm_bvAMFtK0NorhFfp3b70ri4PAPKroicXxDkGEA2eY1p_hmkM_ppwjG6Z6mmhViGfftGH7MmAu7PxtTp5M-_1DB0TMuHMzYwS_iarkFEKYo5zZfgA2jrq7d5T3Tg-nNNFyKlPN399sMTY_W-3kXGINeEVH-CL3ZICB2I_o-CdKlK2YxOLKmtnXhj1ttDGrISNdQ9KNcPH_QxRosVcsvEMsfYIXilmpwjsUrqDdytNFhbUZb0GV7XgzhRddqWGkUMEpL-8J-5k5QOS4GCmi4bLjj3cdaF9l-wc1WnbuMM_CLZWaZ8ks30RDyMSigWlQs-CYGsvCae6UiOyvJzCONwJDyC72RM4ITYuHfuqxsKoWo47xmUGUmEkophWxvQlWKYsgM2a5zEqk4srQCWxcpqOXa5KAxfFDm-3HfQ3g8_lQjfAK42ChBNc1G9_MrIB06kT1mo8P6vfsc_4fcH75n0hGrVYnwz7VI59pJ8bkenE1Is0e3EDNaPDX72hnTAjegktBzUovwax3g5If9VtDf74JGVNZSkI81nmjFc-YeokV8c8FOciy8J6p0wP2kXPMD4nI3h0AZT5W67RBP6OBHlNPYzv-_Hv6nG0aPll9NF-Pl8FMCeluui83tNmOjcw_H_lIqA==","summary":[{"type":"summary_text","text":"**Identifying needed RoundCtx and pump paths**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2a6814481969ffa72d75fc183d7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading pump shared state scans\",\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"selector\":\"210-440\"}","call_id":"call_pC7j6crGdzLjPq7OJElyRL8P","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2a6817c819684934d649023f9db","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry advertisement\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"350-480,760-850\"}","call_id":"call_t4x2HJ4j6FSQH0i2VOCVy1Je","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2a681888196adc38186e35d17f9","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing sync ref selection\",\"pattern\":\"pub fn select_refs|fn select_refs|create_bundle|bundle|branches\\\\(|refs|read_dir|std::fs::read\",\"path\":\"crates/spt-daemon/src/sync.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_BgNJhusrlETY6XmwV72vwIxD","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2a6819081969df6d29e1b02defb","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing notif feed storage\",\"pattern\":\"pub fn emit_notif_feed|fn emit_notif_feed|fn list|pub fn list|read_to_string|std::fs::read\",\"path\":\"crates/spt-daemon/src/notifsync.rs; crates/spt-store/src/notif.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_NYakXDydijyDrS7bdty7jlJt","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2a68198819681576c4f08438d51","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading history implementation\",\"path\":\"crates/spt-live/src/history.rs\",\"selector\":\"1-155\"}","call_id":"call_vVTselAEQrgjjzELuOu81egg","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2a681a08196a4d35ebc698fa3b0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading drop ingest implementation\",\"path\":\"crates/spt-live/src/ingest.rs\",\"selector\":\"180-280\"}","call_id":"call_kOmXQVaoNyQHELLixJ2hYKQa","name":"read"}]},"duration":8851.641400009394,"ttft":2500.8293000012636,"contextSnapshot":{"promptTokens":149656,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pC7j6crGdzLjPq7OJElyRL8P|fc_038f28d3e93bbfd4016a59d2a6814481969ffa72d75fc183d7","toolName":"read","startedAt":"2026-07-17T06:58:46.610Z","args":{"path":"crates/spt-daemon/src/pump/mod.rs"},"intent":"Reading pump shared state scans"},"id":"07d87cfd","parentId":"f64f7e56","timestamp":"2026-07-17T06:58:46.610Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_t4x2HJ4j6FSQH0i2VOCVy1Je|fc_038f28d3e93bbfd4016a59d2a6817c819684934d649023f9db","toolName":"read","startedAt":"2026-07-17T06:58:46.610Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading registry advertisement"},"id":"73ac2011","parentId":"07d87cfd","timestamp":"2026-07-17T06:58:46.610Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BgNJhusrlETY6XmwV72vwIxD|fc_038f28d3e93bbfd4016a59d2a681888196adc38186e35d17f9","toolName":"grep","startedAt":"2026-07-17T06:58:46.610Z","args":{"path":"crates/spt-daemon/src/sync.rs"},"intent":"Tracing sync ref selection"},"id":"a0bd53c1","parentId":"73ac2011","timestamp":"2026-07-17T06:58:46.610Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NYakXDydijyDrS7bdty7jlJt|fc_038f28d3e93bbfd4016a59d2a6819081969df6d29e1b02defb","toolName":"grep","startedAt":"2026-07-17T06:58:46.610Z","args":{"path":"crates/spt-daemon/src/notifsync.rs; crates/spt-store/src/notif.rs"},"intent":"Tracing notif feed storage"},"id":"679e30dc","parentId":"a0bd53c1","timestamp":"2026-07-17T06:58:46.610Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vVTselAEQrgjjzELuOu81egg|fc_038f28d3e93bbfd4016a59d2a68198819681576c4f08438d51","toolName":"read","startedAt":"2026-07-17T06:58:46.610Z","args":{"path":"crates/spt-live/src/history.rs"},"intent":"Reading history implementation"},"id":"3382e413","parentId":"679e30dc","timestamp":"2026-07-17T06:58:46.610Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kOmXQVaoNyQHELLixJ2hYKQa|fc_038f28d3e93bbfd4016a59d2a681a08196a4d35ebc698fa3b0","toolName":"read","startedAt":"2026-07-17T06:58:46.610Z","args":{"path":"crates/spt-live/src/ingest.rs"},"intent":"Reading drop ingest implementation"},"id":"17154ea3","parentId":"3382e413","timestamp":"2026-07-17T06:58:46.610Z"}
{"type":"message","id":"32fc0f21","parentId":"17154ea3","timestamp":"2026-07-17T06:58:46.615Z","message":{"role":"toolResult","toolCallId":"call_BgNJhusrlETY6XmwV72vwIxD|fc_038f28d3e93bbfd4016a59d2a681888196adc38186e35d17f9","toolName":"grep","content":[{"type":"text","text":"*1|//! Context bundle sync over broker streams (M4-D6c, ADR-0013, REQ-NET-3) —\n 2|//! the serve/request drivers for the [`SyncRecord`] protocol, the server-side\n 3|//! ref gate, and the post-pull reconcile wiring.\n 4|//!\n...\n 7|//! current tips; the responder's [`serve_sync`] gates every ref, builds an\n*8|//! incremental bundle ([`BranchStore::create_bundle`]), and streams it back as\n*9|//! positional chunks. The requester fetches the bundle into quarantine and\n 10|//! rules per file with the vector driver ([`spt_store::syncmerge`]); surfaced\n 11|//! conflicts feed the Psyche reconcile when this node is the elected\n 12|//! reconciler ([`reconcile_after_sync`]).\n...\n 15|//!\n*16|//! The requester names refs, but naming is not entitlement: [`SyncPolicy`]\n 17|//! re-checks every requested ref on the responder under the\n 18|//! **handshake-proven** origin node (never payload bytes, KNOWN-HAZARDS 7.5):\n 19|//!\n...\n 28|//!   per-file p- filtering is post-v1 (CONTEXT-MEMORY §Deferred).\n*29|//! - Anything else (machinery refs, `refs/spt-sync/…`) is never served.\n 30|//!\n 31|//! ## Wire discipline + recovery\n 32|//!\n...\n 35|//! There is deliberately **no resume protocol**: a torn pull (stream lost\n*36|//! mid-bundle) is recovered by simply requesting again — the apply is\n 37|//! idempotent (an already-joined tip short-circuits on ancestry), the\n*38|//! FAULT-MATRIX \"stream loss mid-bundle\" row.\n 39|// [impl->REQ-NET-3]\n 40|// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n 41|\n...\n 112|        } else {\n*113|            false // machinery refs are never sync-served\n 114|        }\n 115|    }\n 116|}\n...\n 120|pub enum SyncServeOutcome {\n*121|    /// A bundle was served (`bytes` on the wire, carrying `refs`); `denied`\n*122|    /// lists requested refs the gate refused.\n 123|    Sent {\n 124|        bytes: u64,\n*125|        refs: Vec<String>,\n 126|        denied: Vec<String>,\n 127|    },\n 128|    /// Every allowed ref was already up to date — nothing sent.\n...\n 181|/// a hidden/unsynced mind by naming it). `scratch_dir` holds the transient\n*182|/// bundle file.\n 183|// [impl->REQ-NET-3]\n 184|// [impl->REQ-INST-5]\n 185|// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n...\n 209|                    sync_id,\n*210|                    refs,\n 211|                    have_tips,\n 212|                } = rec\n 213|                else {\n...\n 216|\n*217|                // The gate: partition requested refs under the proven origin.\n*218|                let (allowed, denied): (Vec<String>, Vec<String>) = refs\n 219|                    .into_iter()\n 220|                    .partition(|r| policy.allows(r, origin_node));\n 221|                if !denied.is_empty() {\n...\n 223|                        sync_id: sync_id.clone(),\n*224|                        refs: denied.clone(),\n 225|                        message: \"refused: not synced/visible to this origin\".to_string(),\n 226|                    }\n 227|                    .encode_line();\n...\n 230|\n*231|                // Incremental bundle for the allowed refs (absent branches and\n*232|                // up-to-date tips are skipped inside create_bundle).\n 233|                let wants: Vec<(String, Option<String>)> = allowed\n 234|                    .iter()\n 235|                    .map(|r| (r.clone(), have_tips.get(r).cloned()))\n...\n 237|                std::fs::create_dir_all(scratch_dir)?;\n*238|                let bundle = scratch_dir.join(format!(\"serve-{}.bundle\", safe_stem(&sync_id)));\n*239|                let included = match cs.branch_store().create_bundle(&bundle, &wants) {\n 240|                    Ok(i) => i,\n 241|                    Err(e) => {\n 242|                        let line = SyncRecord::Err {\n 243|                            sync_id: sync_id.clone(),\n*244|                            message: format!(\"bundle failed: {e}\"),\n 245|                        }\n 246|                        .encode_line();\n 247|                        brain.net_stream_send(stream_id, &line, None, true)?;\n...\n 254|                        total: 0,\n*255|                        refs: Vec::new(),\n 256|                    }\n 257|                    .encode_line();\n 258|                    brain.net_stream_send(stream_id, &line, None, true)?;\n...\n 261|\n*262|                // Pump the bundle file as positional chunks (fire-and-forget;\n 263|                // the offset is the dedup, not a send journal — D5b lesson).\n 264|                let bytes_total = {\n*265|                    let mut file = std::fs::File::open(&bundle)?;\n 266|                    let total = file.metadata()?.len();\n 267|                    let mut offset = 0u64;\n 268|                    let mut buf = vec![0u8; CHUNK_BYTES];\n...\n 285|                };\n*286|                let _ = std::fs::remove_file(&bundle);\n 287|                let done = SyncRecord::Done {\n 288|                    sync_id,\n 289|                    total: bytes_total,\n*290|                    refs: included.clone(),\n 291|                }\n 292|                .encode_line();\n 293|                brain.net_stream_send(stream_id, &done, None, true)?;\n...\n 295|                    bytes: bytes_total,\n*296|                    refs: included,\n 297|                    denied,\n 298|                });\n 299|            }\n...\n 309|\n*310|/// The refs this node should request from `peer_node`: every local `a-<id>`\n 311|/// branch the policy allows toward that peer (symmetric scoping — don't ask\n 312|/// for a mind the peer's own gate would refuse to receive elsewhere) plus\n 313|/// every hosted `p-<project>` branch. The pull model's requester half:\n...\n 315|// [impl->REQ-INST-5]\n*316|pub fn select_refs(\n 317|    policy: &SyncPolicy,\n 318|    cs: &ContextStore,\n 319|    peer_node: &str,\n...\n 322|        .branch_store()\n*323|        .branches()?\n 324|        .into_iter()\n 325|        .filter(|b| policy.allows(b, peer_node))\n 326|        .collect())\n...\n 328|\n*329|/// Requester side: pull `refs` from the peer over `conn_id`, apply each\n 330|/// fetched tip with the vector driver, and report per-branch verdicts.\n 331|/// `open_op` is the durable id for the exactly-once stream open; everything\n 332|/// after rides fire-and-forget. A torn pull errors — recovery is a fresh\n...\n 337|    conn_id: u64,\n*338|    refs: &[String],\n 339|    open_op: MintedOp,\n 340|    cs: &ContextStore,\n 341|    scratch_dir: &Path,\n...\n 348|    let mut have_tips = BTreeMap::new();\n*349|    for r in refs {\n 350|        if let Some(tip) = cs.branch_store().tip(r)? {\n 351|            have_tips.insert(r.clone(), tip);\n 352|        }\n...\n 355|        sync_id: sync_id.clone(),\n*356|        refs: refs.to_vec(),\n 357|        have_tips,\n 358|    }\n 359|    .encode_line();\n...\n 362|    std::fs::create_dir_all(scratch_dir)?;\n*363|    let bundle: PathBuf = scratch_dir.join(format!(\"recv-{}.bundle\", safe_stem(&sync_id)));\n 364|    let mut file = Some(\n 365|        std::fs::OpenOptions::new()\n 366|            .create(true)\n...\n 368|            .write(true)\n*369|            .open(&bundle)?,\n 370|    );\n 371|    let mut report = SyncPullReport::default();\n 372|    let mut decoder = SyncDecoder::new();\n...\n 396|                        SyncRecord::Deny {\n*397|                            sync_id: xid, refs, ..\n 398|                        } if xid == sync_id => {\n*399|                            report.denied.extend(refs);\n 400|                        }\n 401|                        SyncRecord::Chunk {\n 402|                            sync_id: xid,\n...\n 416|                            total,\n*417|                            refs,\n 418|                        } if xid == sync_id => {\n*419|                            // Close our handle before git reads the bundle.\n 420|                            if let Some(mut f) = file.take() {\n 421|                                f.flush()?;\n 422|                            }\n 423|                            if total == 0 {\n*424|                                let _ = std::fs::remove_file(&bundle);\n 425|                                return Ok(report); // everything already up to date\n 426|                            }\n*427|                            let got = std::fs::metadata(&bundle)?.len();\n 428|                            if got != total {\n*429|                                let _ = std::fs::remove_file(&bundle);\n 430|                                return Err(io::Error::new(\n 431|                                    io::ErrorKind::UnexpectedEof,\n*432|                                    format!(\"bundle incomplete: {got} of {total} bytes\"),\n 433|                                ));\n 434|                            }\n*435|                            let tips = cs.branch_store().fetch_bundle(&bundle)?;\n*436|                            let _ = std::fs::remove_file(&bundle);\n*437|                            for branch in &refs {\n 438|                                if let Some(tip) = tips.get(branch) {\n 439|                                    report.applied.push(apply_fetched(cs, branch, tip)?);\n 440|                                }\n...\n 463|                        } if xid == sync_id => {\n*464|                            let _ = std::fs::remove_file(&bundle);\n 465|                            return Err(io::Error::other(format!(\"sync refused: {message}\")));\n 466|                        }\n 467|                        _ => {}\n...\n 471|            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n*472|                let _ = std::fs::remove_file(&bundle);\n 473|                return Err(io::Error::new(\n 474|                    io::ErrorKind::UnexpectedEof,\n 475|                    \"stream ended before Done\",\n...\n 576|    // holds — unconfigured membership lists (fail-closed), hidden endpoints,\n*577|    // and untrusted origins all refuse; p- refs need a trusted origin;\n*578|    // machinery refs never serve.\n 579|    #[test]\n 580|    fn gate_decision_table() {\n 581|        let mut p = policy(\"home\", \"peer1\");\n...\n 611|\n*612|        // Machinery refs never sync-serve.\n 613|        assert!(!p.allows(\"main\", \"peer1\"));\n 614|        assert!(!p.allows(\"spt/journal\", \"peer1\"));\n 615|    }\n 616|\n*617|    // [unit->REQ-INST-5] requester-side scoping mirrors the gate: select_refs\n 618|    // names only the a- minds the peer may hold plus hosted p- projects.\n 619|    #[test]\n*620|    fn select_refs_scopes_two_tiers() {\n 621|        let tmp = tempfile::tempdir().unwrap();\n 622|        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();\n 623|        cs.live_context_path(\"doyle\").unwrap();\n...\n 630|\n*631|        let refs = select_refs(&p, &cs, \"peer1\").unwrap();\n*632|        assert!(refs.contains(&\"a-doyle\".to_string()));\n*633|        assert!(refs.contains(&\"p-spt-core\".to_string()));\n 634|        assert!(\n*635|            !refs.iter().any(|r| r == \"a-ghost\"),\n*636|            \"unsynced mind never requested: {refs:?}\"\n 637|        );\n 638|\n 639|        assert!(\n*640|            select_refs(&p, &cs, \"stranger\").unwrap().is_empty(),\n 641|            \"untrusted peer gets nothing\"\n 642|        );\n 643|    }"}],"details":{"scopePath":"crates/spt-daemon/src/sync.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\sync.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":60,"fileCount":1,"files":["crates/spt-daemon/src/sync.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/sync.rs","count":60}],"truncated":false,"displayContent":"  *1│//! Context bundle sync over broker streams (M4-D6c, ADR-0013, REQ-NET-3) —\n   2│//! the serve/request drivers for the [`SyncRecord`] protocol, the server-side\n   3│//! ref gate, and the post-pull reconcile wiring.\n   4│//!\n    │...\n   7│//! current tips; the responder's [`serve_sync`] gates every ref, builds an\n  *8│//! incremental bundle ([`BranchStore::create_bundle`]), and streams it back as\n  *9│//! positional chunks. The requester fetches the bundle into quarantine and\n  10│//! rules per file with the vector driver ([`spt_store::syncmerge`]); surfaced\n  11│//! conflicts feed the Psyche reconcile when this node is the elected\n  12│//! reconciler ([`reconcile_after_sync`]).\n    │...\n  15│//!\n *16│//! The requester names refs, but naming is not entitlement: [`SyncPolicy`]\n  17│//! re-checks every requested ref on the responder under the\n  18│//! **handshake-proven** origin node (never payload bytes, KNOWN-HAZARDS 7.5):\n  19│//!\n    │...\n  28│//!   per-file p- filtering is post-v1 (CONTEXT-MEMORY §Deferred).\n *29│//! - Anything else (machinery refs, `refs/spt-sync/…`) is never served.\n  30│//!\n  31│//! ## Wire discipline + recovery\n  32│//!\n    │...\n  35│//! There is deliberately **no resume protocol**: a torn pull (stream lost\n *36│//! mid-bundle) is recovered by simply requesting again — the apply is\n  37│//! idempotent (an already-joined tip short-circuits on ancestry), the\n *38│//! FAULT-MATRIX \"stream loss mid-bundle\" row.\n  39│// [impl->REQ-NET-3]\n  40│// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n  41│\n    │...\n 112│        } else {\n*113│            false // machinery refs are never sync-served\n 114│        }\n 115│    }\n 116│}\n    │...\n 120│pub enum SyncServeOutcome {\n*121│    /// A bundle was served (`bytes` on the wire, carrying `refs`); `denied`\n*122│    /// lists requested refs the gate refused.\n 123│    Sent {\n 124│        bytes: u64,\n*125│        refs: Vec<String>,\n 126│        denied: Vec<String>,\n 127│    },\n 128│    /// Every allowed ref was already up to date — nothing sent.\n    │...\n 181│/// a hidden/unsynced mind by naming it). `scratch_dir` holds the transient\n*182│/// bundle file.\n 183│// [impl->REQ-NET-3]\n 184│// [impl->REQ-INST-5]\n 185│// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n    │...\n 209│                    sync_id,\n*210│                    refs,\n 211│                    have_tips,\n 212│                } = rec\n 213│                else {\n    │...\n 216│\n*217│                // The gate: partition requested refs under the proven origin.\n*218│                let (allowed, denied): (Vec<String>, Vec<String>) = refs\n 219│                    .into_iter()\n 220│                    .partition(|r| policy.allows(r, origin_node));\n 221│                if !denied.is_empty() {\n    │...\n 223│                        sync_id: sync_id.clone(),\n*224│                        refs: denied.clone(),\n 225│                        message: \"refused: not synced/visible to this origin\".to_string(),\n 226│                    }\n 227│                    .encode_line();\n    │...\n 230│\n*231│                // Incremental bundle for the allowed refs (absent branches and\n*232│                // up-to-date tips are skipped inside create_bundle).\n 233│                let wants: Vec<(String, Option<String>)> = allowed\n 234│                    .iter()\n 235│                    .map(|r| (r.clone(), have_tips.get(r).cloned()))\n    │...\n 237│                std::fs::create_dir_all(scratch_dir)?;\n*238│                let bundle = scratch_dir.join(format!(\"serve-{}.bundle\", safe_stem(&sync_id)));\n*239│                let included = match cs.branch_store().create_bundle(&bundle, &wants) {\n 240│                    Ok(i) => i,\n 241│                    Err(e) => {\n 242│                        let line = SyncRecord::Err {\n 243│                            sync_id: sync_id.clone(),\n*244│                            message: format!(\"bundle failed: {e}\"),\n 245│                        }\n 246│                        .encode_line();\n 247│                        brain.net_stream_send(stream_id, &line, None, true)?;\n    │...\n 254│                        total: 0,\n*255│                        refs: Vec::new(),\n 256│                    }\n 257│                    .encode_line();\n 258│                    brain.net_stream_send(stream_id, &line, None, true)?;\n    │...\n 261│\n*262│                // Pump the bundle file as positional chunks (fire-and-forget;\n 263│                // the offset is the dedup, not a send journal — D5b lesson).\n 264│                let bytes_total = {\n*265│                    let mut file = std::fs::File::open(&bundle)?;\n 266│                    let total = file.metadata()?.len();\n 267│                    let mut offset = 0u64;\n 268│                    let mut buf = vec![0u8; CHUNK_BYTES];\n    │...\n 285│                };\n*286│                let _ = std::fs::remove_file(&bundle);\n 287│                let done = SyncRecord::Done {\n 288│                    sync_id,\n 289│                    total: bytes_total,\n*290│                    refs: included.clone(),\n 291│                }\n 292│                .encode_line();\n 293│                brain.net_stream_send(stream_id, &done, None, true)?;\n    │...\n 295│                    bytes: bytes_total,\n*296│                    refs: included,\n 297│                    denied,\n 298│                });\n 299│            }\n    │...\n 309│\n*310│/// The refs this node should request from `peer_node`: every local `a-<id>`\n 311│/// branch the policy allows toward that peer (symmetric scoping — don't ask\n 312│/// for a mind the peer's own gate would refuse to receive elsewhere) plus\n 313│/// every hosted `p-<project>` branch. The pull model's requester half:\n    │...\n 315│// [impl->REQ-INST-5]\n*316│pub fn select_refs(\n 317│    policy: &SyncPolicy,\n 318│    cs: &ContextStore,\n 319│    peer_node: &str,\n    │...\n 322│        .branch_store()\n*323│        .branches()?\n 324│        .into_iter()\n 325│        .filter(|b| policy.allows(b, peer_node))\n 326│        .collect())\n    │...\n 328│\n*329│/// Requester side: pull `refs` from the peer over `conn_id`, apply each\n 330│/// fetched tip with the vector driver, and report per-branch verdicts.\n 331│/// `open_op` is the durable id for the exactly-once stream open; everything\n 332│/// after rides fire-and-forget. A torn pull errors — recovery is a fresh\n    │...\n 337│    conn_id: u64,\n*338│    refs: &[String],\n 339│    open_op: MintedOp,\n 340│    cs: &ContextStore,\n 341│    scratch_dir: &Path,\n    │...\n 348│    let mut have_tips = BTreeMap::new();\n*349│    for r in refs {\n 350│        if let Some(tip) = cs.branch_store().tip(r)? {\n 351│            have_tips.insert(r.clone(), tip);\n 352│        }\n    │...\n 355│        sync_id: sync_id.clone(),\n*356│        refs: refs.to_vec(),\n 357│        have_tips,\n 358│    }\n 359│    .encode_line();\n    │...\n 362│    std::fs::create_dir_all(scratch_dir)?;\n*363│    let bundle: PathBuf = scratch_dir.join(format!(\"recv-{}.bundle\", safe_stem(&sync_id)));\n 364│    let mut file = Some(\n 365│        std::fs::OpenOptions::new()\n 366│            .create(true)\n    │...\n 368│            .write(true)\n*369│            .open(&bundle)?,\n 370│    );\n 371│    let mut report = SyncPullReport::default();\n 372│    let mut decoder = SyncDecoder::new();\n    │...\n 396│                        SyncRecord::Deny {\n*397│                            sync_id: xid, refs, ..\n 398│                        } if xid == sync_id => {\n*399│                            report.denied.extend(refs);\n 400│                        }\n 401│                        SyncRecord::Chunk {\n 402│                            sync_id: xid,\n    │...\n 416│                            total,\n*417│                            refs,\n 418│                        } if xid == sync_id => {\n*419│                            // Close our handle before git reads the bundle.\n 420│                            if let Some(mut f) = file.take() {\n 421│                                f.flush()?;\n 422│                            }\n 423│                            if total == 0 {\n*424│                                let _ = std::fs::remove_file(&bundle);\n 425│                                return Ok(report); // everything already up to date\n 426│                            }\n*427│                            let got = std::fs::metadata(&bundle)?.len();\n 428│                            if got != total {\n*429│                                let _ = std::fs::remove_file(&bundle);\n 430│                                return Err(io::Error::new(\n 431│                                    io::ErrorKind::UnexpectedEof,\n*432│                                    format!(\"bundle incomplete: {got} of {total} bytes\"),\n 433│                                ));\n 434│                            }\n*435│                            let tips = cs.branch_store().fetch_bundle(&bundle)?;\n*436│                            let _ = std::fs::remove_file(&bundle);\n*437│                            for branch in &refs {\n 438│                                if let Some(tip) = tips.get(branch) {\n 439│                                    report.applied.push(apply_fetched(cs, branch, tip)?);\n 440│                                }\n    │...\n 463│                        } if xid == sync_id => {\n*464│                            let _ = std::fs::remove_file(&bundle);\n 465│                            return Err(io::Error::other(format!(\"sync refused: {message}\")));\n 466│                        }\n 467│                        _ => {}\n    │...\n 471│            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n*472│                let _ = std::fs::remove_file(&bundle);\n 473│                return Err(io::Error::new(\n 474│                    io::ErrorKind::UnexpectedEof,\n 475│                    \"stream ended before Done\",\n    │...\n 576│    // holds — unconfigured membership lists (fail-closed), hidden endpoints,\n*577│    // and untrusted origins all refuse; p- refs need a trusted origin;\n*578│    // machinery refs never serve.\n 579│    #[test]\n 580│    fn gate_decision_table() {\n 581│        let mut p = policy(\"home\", \"peer1\");\n    │...\n 611│\n*612│        // Machinery refs never sync-serve.\n 613│        assert!(!p.allows(\"main\", \"peer1\"));\n 614│        assert!(!p.allows(\"spt/journal\", \"peer1\"));\n 615│    }\n 616│\n*617│    // [unit->REQ-INST-5] requester-side scoping mirrors the gate: select_refs\n 618│    // names only the a- minds the peer may hold plus hosted p- projects.\n 619│    #[test]\n*620│    fn select_refs_scopes_two_tiers() {\n 621│        let tmp = tempfile::tempdir().unwrap();\n 622│        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();\n 623│        cs.live_context_path(\"doyle\").unwrap();\n    │...\n 630│\n*631│        let refs = select_refs(&p, &cs, \"peer1\").unwrap();\n*632│        assert!(refs.contains(&\"a-doyle\".to_string()));\n*633│        assert!(refs.contains(&\"p-spt-core\".to_string()));\n 634│        assert!(\n*635│            !refs.iter().any(|r| r == \"a-ghost\"),\n*636│            \"unsynced mind never requested: {refs:?}\"\n 637│        );\n 638│\n 639│        assert!(\n*640│            select_refs(&p, &cs, \"stranger\").unwrap().is_empty(),\n 641│            \"untrusted peer gets nothing\"\n 642│        );\n 643│    }"},"isError":false,"timestamp":1784271526615}}
{"type":"message","id":"e2725e60","parentId":"32fc0f21","timestamp":"2026-07-17T06:58:46.616Z","message":{"role":"toolResult","toolCallId":"call_NYakXDydijyDrS7bdty7jlJt|fc_038f28d3e93bbfd4016a59d2a6819081969df6d29e1b02defb","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### notifsync.rs\n 82|// [impl->REQ-NOTIF-1]\n*83|pub fn emit_notif_feed(store: &NotifStore, subnet: &str) -> Result<Vec<u8>, String> {\n 84|    let mut wire = Vec::new();\n 85|    for row in store.list(subnet).map_err(|e| format!(\"notif list: {e}\"))? {\n 86|        wire.extend_from_slice(&NotifRecord::Row { row }.encode_line());\n\n## spt-store/src/\n### notif.rs\n 252|    /// Every row in `subnet`, oldest produce first (created_ms, id tiebreak).\n*253|    pub fn list(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\n 254|        self.select_rows(\n 255|            \"subnet = ?1 ORDER BY created_ms ASC, notif_id ASC\",\n 256|            params![subnet],\n...\n 419|    #[test]\n*420|    fn listing_is_subnet_scoped() {\n 421|        let dir = tempdir().unwrap();\n 422|        let s = store(dir.path());\n 423|        let mut e = epochs(dir.path());"}],"details":{"scopePath":"crates/spt-daemon/src/notifsync.rs, crates/spt-store/src/notif.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":2,"files":["crates/spt-daemon/src/notifsync.rs","crates/spt-store/src/notif.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/notifsync.rs","count":1},{"path":"crates/spt-store/src/notif.rs","count":2}],"truncated":false,"displayContent":"# crates/\n\n## spt-daemon/src/\n### notifsync.rs\n 82│// [impl->REQ-NOTIF-1]\n*83│pub fn emit_notif_feed(store: &NotifStore, subnet: &str) -> Result<Vec<u8>, String> {\n 84│    let mut wire = Vec::new();\n 85│    for row in store.list(subnet).map_err(|e| format!(\"notif list: {e}\"))? {\n 86│        wire.extend_from_slice(&NotifRecord::Row { row }.encode_line());\n\n## spt-store/src/\n### notif.rs\n 252│    /// Every row in `subnet`, oldest produce first (created_ms, id tiebreak).\n*253│    pub fn list(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\n 254│        self.select_rows(\n 255│            \"subnet = ?1 ORDER BY created_ms ASC, notif_id ASC\",\n 256│            params![subnet],\n    │...\n 419│    #[test]\n*420│    fn listing_is_subnet_scoped() {\n 421│        let dir = tempdir().unwrap();\n 422│        let s = store(dir.path());\n 423│        let mut e = epochs(dir.path());"},"isError":false,"timestamp":1784271526616}}
{"type":"message","id":"7802d75f","parentId":"e2725e60","timestamp":"2026-07-17T06:58:46.631Z","message":{"role":"toolResult","toolCallId":"call_t4x2HJ4j6FSQH0i2VOCVy1Je|fc_038f28d3e93bbfd4016a59d2a6817c819684934d649023f9db","toolName":"read","content":[{"type":"text","text":"impl RegistryHost {\n…\n    fn stamp_admit(&self) {\n        self.last_admit_ms\n            .store(wall_ms(), std::sync::atomic::Ordering::Relaxed);\n    }\n\n    /// Wall-clock ms of the last admitted inbound registry/label feed, if any\n    /// this process lifetime (ADR-0039 Decision 5 status field).\n    // [impl->REQ-PUMP-STAGE-TRUTH]\n    pub fn last_admit_ms(&self) -> Option<u64> {\n        match self\n            .last_admit_ms\n            .load(std::sync::atomic::Ordering::Relaxed)\n        {\n            0 => None,\n            ms => Some(ms),\n        }\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…\n        }\n…\n    }\n…\n    fn write_snapshots(&self) {\n…\n        if !heard.is_empty() {\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\n…\n            }\n        }\n    }\n\n    /// The snapshot path for one subnet (the CLI reader's contract).\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\n        dir.join(format!(\"{subnet}.json\"))\n    }\n\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\n    /// last admitted-feed wall ms`.\n    // [impl->REQ-SUBNET-1]\n    pub fn heard_path(dir: &Path) -> PathBuf {\n        dir.join(\"heard.meta\")\n    }\n\n    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):\n    /// a JSON array of superseded node hexes the pairing host noted —\n    /// consumed by the pump's registry tick so the IN-MEMORY rows go too\n    /// (a disk-only eviction would be resurrected by the next snapshot\n    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the\n    /// heard map.\n    pub fn repair_evict_path(dir: &Path) -> PathBuf {\n        dir.join(\"repair-evict.meta\")\n    }\n\n    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]\n    /// pattern — take is the remove): evict every noted identity's rows\n    /// from the in-memory registries + the heard map, and rewrite the\n    /// snapshots. No marker = no-op. Returns evicted row count.\n    // [impl->REQ-SUBNET-7]\n    pub fn consume_repair_evictions(&self) -> usize {\n        let path = Self::repair_evict_path(&self.snapshot_dir);\n        let Ok(raw) = std::fs::read_to_string(&path) else {\n            return 0;\n        };\n        let _ = std::fs::remove_file(&path);\n        let dead: std::collections::BTreeSet<String> =\n            serde_json::from_str(&raw).unwrap_or_default();\n        if dead.is_empty() {\n            return 0;\n        }\n        {\n            let mut heard = self.heard.lock().unwrap();\n            for hex in &dead {\n                heard.remove(hex);\n            }\n        }\n        let mut regs = self.regs.lock().unwrap();\n        let mut evicted = 0usize;\n        for (subnet, reg) in regs.iter_mut() {\n            let n = reg.evict_nodes(|node| dead.contains(node));\n            if n > 0 {\n                eprintln!(\"REPAIR_EVICT:{subnet}: {n} superseded row(s)\");\n            }\n            evicted += n;\n        }\n        drop(regs);\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…\n}"}],"details":{"displayContent":{"text":"impl RegistryHost {\n…\n    fn stamp_admit(&self) {\n        self.last_admit_ms\n            .store(wall_ms(), std::sync::atomic::Ordering::Relaxed);\n    }\n\n    /// Wall-clock ms of the last admitted inbound registry/label feed, if any\n    /// this process lifetime (ADR-0039 Decision 5 status field).\n    // [impl->REQ-PUMP-STAGE-TRUTH]\n    pub fn last_admit_ms(&self) -> Option<u64> {\n        match self\n            .last_admit_ms\n            .load(std::sync::atomic::Ordering::Relaxed)\n        {\n            0 => None,\n            ms => Some(ms),\n        }\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…\n        }\n…\n    }\n…\n    fn write_snapshots(&self) {\n…\n        if !heard.is_empty() {\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\n…\n            }\n        }\n    }\n\n    /// The snapshot path for one subnet (the CLI reader's contract).\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\n        dir.join(format!(\"{subnet}.json\"))\n    }\n\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\n    /// last admitted-feed wall ms`.\n    // [impl->REQ-SUBNET-1]\n    pub fn heard_path(dir: &Path) -> PathBuf {\n        dir.join(\"heard.meta\")\n    }\n\n    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):\n    /// a JSON array of superseded node hexes the pairing host noted —\n    /// consumed by the pump's registry tick so the IN-MEMORY rows go too\n    /// (a disk-only eviction would be resurrected by the next snapshot\n    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the\n    /// heard map.\n    pub fn repair_evict_path(dir: &Path) -> PathBuf {\n        dir.join(\"repair-evict.meta\")\n    }\n\n    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]\n    /// pattern — take is the remove): evict every noted identity's rows\n    /// from the in-memory registries + the heard map, and rewrite the\n    /// snapshots. No marker = no-op. Returns evicted row count.\n    // [impl->REQ-SUBNET-7]\n    pub fn consume_repair_evictions(&self) -> usize {\n        let path = Self::repair_evict_path(&self.snapshot_dir);\n        let Ok(raw) = std::fs::read_to_string(&path) else {\n            return 0;\n        };\n        let _ = std::fs::remove_file(&path);\n        let dead: std::collections::BTreeSet<String> =\n            serde_json::from_str(&raw).unwrap_or_default();\n        if dead.is_empty() {\n            return 0;\n        }\n        {\n            let mut heard = self.heard.lock().unwrap();\n            for hex in &dead {\n                heard.remove(hex);\n            }\n        }\n        let mut regs = self.regs.lock().unwrap();\n        let mut evicted = 0usize;\n        for (subnet, reg) in regs.iter_mut() {\n            let n = reg.evict_nodes(|node| dead.contains(node));\n            if n > 0 {\n                eprintln!(\"REPAIR_EVICT:{subnet}: {n} superseded row(s)\");\n            }\n            evicted += n;\n        }\n        drop(regs);\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…\n}","startLine":127,"lineNumbers":[127,null,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,null,551,null,634,null,738,null,757,758,null,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,null,861]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"}}},"isError":false,"timestamp":1784271526631}}
{"type":"message","id":"3ddf18ee","parentId":"7802d75f","timestamp":"2026-07-17T06:58:46.638Z","message":{"role":"toolResult","toolCallId":"call_vVTselAEQrgjjzELuOu81egg|fc_038f28d3e93bbfd4016a59d2a68198819681576c4f08438d51","toolName":"read","content":[{"type":"text","text":"//! History subsystem (M2b T1) — transcript access behind the `[history]`\n//! manifest section. Feeds the echo-commune (T3) and the resume briefs (T7).\n//!\n//! Three strategies, one normalized output ([`HistoryRecord`] lines):\n//! - **`fetcher`** — spt-core *pulls*: run the adapter's `fetcher` command\n//!   bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT); its stdout is already-normalized\n//!   history.\n//! - **`locate_normalize`** — spt-core *locates* the raw transcript\n//!   (`locate_template`), reads it, then pipes it through `normalize_command`\n//!   bounded (raw transcript on stdin → normalized stdout).\n//! - **`native`** — Path-B store: the adapter *pushed* records via\n//!   `api history-log`; spt-core reads the perch's `history.log` (the T7/M2a\n//!   sink in `spt::api::reporting::append_history`).\n//!\n//! A record is one non-empty line of normalized output. spt-core treats each as\n//! **opaque** — the adapter owns the per-line schema (MANIFEST §Principle); the\n//! history subsystem only delimits, never parses.\n//\n// Clean-room over the M2a seams (the sister's transcript access is `claude -p`\n// /`.claude/`-coupled). The fetcher/normalize command lines are the adapter's\n// opaque templates run through `run_bounded_command`; spt-core selects no model.\n// [impl->REQ-SEAM-HISTORY]\n\nuse spt_runtime::manifest::{History, HistoryStrategy};\nuse spt_runtime::{run_bounded_command, RuntimeError};\nuse spt_store::perch::{resolve_perch_path, ParentHint};\nuse std::collections::BTreeMap;\nuse std::time::Duration;\n\n/// One normalized transcript record — an opaque line. The adapter defines its\n/// schema; spt-core carries it verbatim.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct HistoryRecord {\n    pub raw: String,\n}\n\n/// A history-access failure. Carries an operator message; never panics, never\n/// hangs (the bounded commands are timeout-governed).\n#[derive(Debug)]\npub enum HistoryError {\n    /// The chosen strategy is missing a field the manifest should have required\n    /// (defensive — `Manifest::validate` already gates this).\n    MissingField {\n        strategy: HistoryStrategy,\n        field: &'static str,\n    },\n    /// A `fetcher` / `normalize_command` spawn, fill, or timeout failure.\n    Runtime(RuntimeError),\n    /// The bounded command exited non-zero (its stderr, for the operator).\n    NonZero { code: Option<i32>, stderr: String },\n    /// Reading the raw transcript (`locate_normalize`) or the native log failed.\n    Read(std::io::Error),\n}\n\nimpl std::fmt::Display for HistoryError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            HistoryError::MissingField { strategy, field } => {\n                write!(f, \"[history] strategy {strategy:?} requires `{field}`\")\n            }\n            HistoryError::Runtime(e) => write!(f, \"history command failed: {e}\"),\n            HistoryError::NonZero { code, stderr } => {\n                write!(f, \"history command exited {code:?}: {}\", stderr.trim())\n            }\n            HistoryError::Read(e) => write!(f, \"reading transcript failed: {e}\"),\n        }\n    }\n}\n\nimpl std::error::Error for HistoryError {}\n\nimpl From<RuntimeError> for HistoryError {\n    fn from(e: RuntimeError) -> Self {\n        HistoryError::Runtime(e)\n    }\n}\n\n/// Fetch + normalize the perch's history via the manifest's `[history]`\n/// strategy. `keys` supplies the substitution values (`session_id`, etc.) for\n/// the `fetcher` / `locate_template` / `normalize_command` templates; `id` +\n/// `hint` resolve the native Path-B store. A bounded command that hangs is\n/// killed (never an indefinite block); a missing native log yields *no* records\n/// (a fresh perch), not an error.\npub fn fetch_history(\n    history: &History,\n    keys: &BTreeMap<String, String>,\n    id: &str,\n    hint: ParentHint<'_>,\n    timeout: Duration,\n) -> Result<Vec<HistoryRecord>, HistoryError> {\n    match history.strategy {\n        HistoryStrategy::Fetcher => {\n            let cmd = history\n                .fetcher\n                .as_deref()\n                .ok_or(HistoryError::MissingField {\n                    strategy: HistoryStrategy::Fetcher,\n                    field: \"fetcher\",\n                })?;\n            let out = run_bounded_command(cmd, keys, timeout, None)?;\n            ok_or_nonzero(out).map(|s| records(&s))\n        }\n        HistoryStrategy::LocateNormalize => {\n            let locate = history\n                .locate_template\n                .as_deref()\n                .ok_or(HistoryError::MissingField {\n                    strategy: HistoryStrategy::LocateNormalize,\n                    field: \"locate_template\",\n                })?;\n            let normalize =\n                history\n                    .normalize_command\n                    .as_deref()\n                    .ok_or(HistoryError::MissingField {\n                        strategy: HistoryStrategy::LocateNormalize,\n                        field: \"normalize_command\",\n                    })?;\n            // Fill the locate template to find the raw transcript, read it, then\n            // hand the bytes to the normalizer on stdin.\n            let path = spt_runtime::runtime::fill_template(locate, keys)?;\n            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\n            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\n            ok_or_nonzero(out).map(|s| records(&s))\n        }\n        HistoryStrategy::Native => {\n            let log = resolve_perch_path(id, hint).join(\"history.log\");\n            match std::fs::read_to_string(&log) {\n                Ok(s) => Ok(records(&s)),\n                // No log yet ⇒ a fresh perch with no recorded history.\n                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n                Err(e) => Err(HistoryError::Read(e)),\n            }\n        }\n    }\n}\n\n/// A bounded command's stdout, or `NonZero` carrying its stderr if it failed.\nfn ok_or_nonzero(out: spt_runtime::BoundedOutput) -> Result<String, HistoryError> {\n    if out.success() {\n        Ok(out.stdout)\n    } else {\n        Err(HistoryError::NonZero {\n            code: out.status_code,\n            stderr: out.stderr,\n        })\n    }\n}\n\n/// Split normalized output into records: one per non-empty line, trailing\n/// whitespace trimmed. The native store is already newline-delimited; fetchers\n/// and normalizers emit the same shape.\nfn records(s: &str) -> Vec<HistoryRecord> {\n    s.lines()\n        .map(str::trim_end)\n        .filter(|l| !l.is_empty())\n        .map(|l| HistoryRecord { raw: l.to_string() })\n        .collect()\n}\n\n[Showing lines 1-158 of 352. Use :159 to continue]"}],"details":{"truncation":{"content":"//! History subsystem (M2b T1) — transcript access behind the `[history]`\r\n//! manifest section. Feeds the echo-commune (T3) and the resume briefs (T7).\r\n//!\r\n//! Three strategies, one normalized output ([`HistoryRecord`] lines):\r\n//! - **`fetcher`** — spt-core *pulls*: run the adapter's `fetcher` command\r\n//!   bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT); its stdout is already-normalized\r\n//!   history.\r\n//! - **`locate_normalize`** — spt-core *locates* the raw transcript\r\n//!   (`locate_template`), reads it, then pipes it through `normalize_command`\r\n//!   bounded (raw transcript on stdin → normalized stdout).\r\n//! - **`native`** — Path-B store: the adapter *pushed* records via\r\n//!   `api history-log`; spt-core reads the perch's `history.log` (the T7/M2a\r\n//!   sink in `spt::api::reporting::append_history`).\r\n//!\r\n//! A record is one non-empty line of normalized output. spt-core treats each as\r\n//! **opaque** — the adapter owns the per-line schema (MANIFEST §Principle); the\r\n//! history subsystem only delimits, never parses.\r\n//\r\n// Clean-room over the M2a seams (the sister's transcript access is `claude -p`\r\n// /`.claude/`-coupled). The fetcher/normalize command lines are the adapter's\r\n// opaque templates run through `run_bounded_command`; spt-core selects no model.\r\n// [impl->REQ-SEAM-HISTORY]\r\n\r\nuse spt_runtime::manifest::{History, HistoryStrategy};\r\nuse spt_runtime::{run_bounded_command, RuntimeError};\r\nuse spt_store::perch::{resolve_perch_path, ParentHint};\r\nuse std::collections::BTreeMap;\r\nuse std::time::Duration;\r\n\r\n/// One normalized transcript record — an opaque line. The adapter defines its\r\n/// schema; spt-core carries it verbatim.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct HistoryRecord {\r\n    pub raw: String,\r\n}\r\n\r\n/// A history-access failure. Carries an operator message; never panics, never\r\n/// hangs (the bounded commands are timeout-governed).\r\n#[derive(Debug)]\r\npub enum HistoryError {\r\n    /// The chosen strategy is missing a field the manifest should have required\r\n    /// (defensive — `Manifest::validate` already gates this).\r\n    MissingField {\r\n        strategy: HistoryStrategy,\r\n        field: &'static str,\r\n    },\r\n    /// A `fetcher` / `normalize_command` spawn, fill, or timeout failure.\r\n    Runtime(RuntimeError),\r\n    /// The bounded command exited non-zero (its stderr, for the operator).\r\n    NonZero { code: Option<i32>, stderr: String },\r\n    /// Reading the raw transcript (`locate_normalize`) or the native log failed.\r\n    Read(std::io::Error),\r\n}\r\n\r\nimpl std::fmt::Display for HistoryError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        match self {\r\n            HistoryError::MissingField { strategy, field } => {\r\n                write!(f, \"[history] strategy {strategy:?} requires `{field}`\")\r\n            }\r\n            HistoryError::Runtime(e) => write!(f, \"history command failed: {e}\"),\r\n            HistoryError::NonZero { code, stderr } => {\r\n                write!(f, \"history command exited {code:?}: {}\", stderr.trim())\r\n            }\r\n            HistoryError::Read(e) => write!(f, \"reading transcript failed: {e}\"),\r\n        }\r\n    }\r\n}\r\n\r\nimpl std::error::Error for HistoryError {}\r\n\r\nimpl From<RuntimeError> for HistoryError {\r\n    fn from(e: RuntimeError) -> Self {\r\n        HistoryError::Runtime(e)\r\n    }\r\n}\r\n\r\n/// Fetch + normalize the perch's history via the manifest's `[history]`\r\n/// strategy. `keys` supplies the substitution values (`session_id`, etc.) for\r\n/// the `fetcher` / `locate_template` / `normalize_command` templates; `id` +\r\n/// `hint` resolve the native Path-B store. A bounded command that hangs is\r\n/// killed (never an indefinite block); a missing native log yields *no* records\r\n/// (a fresh perch), not an error.\r\npub fn fetch_history(\r\n    history: &History,\r\n    keys: &BTreeMap<String, String>,\r\n    id: &str,\r\n    hint: ParentHint<'_>,\r\n    timeout: Duration,\r\n) -> Result<Vec<HistoryRecord>, HistoryError> {\r\n    match history.strategy {\r\n        HistoryStrategy::Fetcher => {\r\n            let cmd = history\r\n                .fetcher\r\n                .as_deref()\r\n                .ok_or(HistoryError::MissingField {\r\n                    strategy: HistoryStrategy::Fetcher,\r\n                    field: \"fetcher\",\r\n                })?;\r\n            let out = run_bounded_command(cmd, keys, timeout, None)?;\r\n            ok_or_nonzero(out).map(|s| records(&s))\r\n        }\r\n        HistoryStrategy::LocateNormalize => {\r\n            let locate = history\r\n                .locate_template\r\n                .as_deref()\r\n                .ok_or(HistoryError::MissingField {\r\n                    strategy: HistoryStrategy::LocateNormalize,\r\n                    field: \"locate_template\",\r\n                })?;\r\n            let normalize =\r\n                history\r\n                    .normalize_command\r\n                    .as_deref()\r\n                    .ok_or(HistoryError::MissingField {\r\n                        strategy: HistoryStrategy::LocateNormalize,\r\n                        field: \"normalize_command\",\r\n                    })?;\r\n            // Fill the locate template to find the raw transcript, read it, then\r\n            // hand the bytes to the normalizer on stdin.\r\n            let path = spt_runtime::runtime::fill_template(locate, keys)?;\r\n            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\r\n            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\r\n            ok_or_nonzero(out).map(|s| records(&s))\r\n        }\r\n        HistoryStrategy::Native => {\r\n            let log = resolve_perch_path(id, hint).join(\"history.log\");\r\n            match std::fs::read_to_string(&log) {\r\n                Ok(s) => Ok(records(&s)),\r\n                // No log yet ⇒ a fresh perch with no recorded history.\r\n                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\r\n                Err(e) => Err(HistoryError::Read(e)),\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// A bounded command's stdout, or `NonZero` carrying its stderr if it failed.\r\nfn ok_or_nonzero(out: spt_runtime::BoundedOutput) -> Result<String, HistoryError> {\r\n    if out.success() {\r\n        Ok(out.stdout)\r\n    } else {\r\n        Err(HistoryError::NonZero {\r\n            code: out.status_code,\r\n            stderr: out.stderr,\r\n        })\r\n    }\r\n}\r\n\r\n/// Split normalized output into records: one per non-empty line, trailing\r\n/// whitespace trimmed. The native store is already newline-delimited; fetchers\r\n/// and normalizers emit the same shape.\r\nfn records(s: &str) -> Vec<HistoryRecord> {\r\n    s.lines()\r\n        .map(str::trim_end)\r\n        .filter(|l| !l.is_empty())\r\n        .map(|l| HistoryRecord { raw: l.to_string() })\r\n        .collect()\r","truncated":true,"truncatedBy":"lines","totalLines":352,"totalBytes":6776,"outputLines":158,"outputBytes":6776,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! History subsystem (M2b T1) — transcript access behind the `[history]`\r\n//! manifest section. Feeds the echo-commune (T3) and the resume briefs (T7).\r\n//!\r\n//! Three strategies, one normalized output ([`HistoryRecord`] lines):\r\n//! - **`fetcher`** — spt-core *pulls*: run the adapter's `fetcher` command\r\n//!   bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT); its stdout is already-normalized\r\n//!   history.\r\n//! - **`locate_normalize`** — spt-core *locates* the raw transcript\r\n//!   (`locate_template`), reads it, then pipes it through `normalize_command`\r\n//!   bounded (raw transcript on stdin → normalized stdout).\r\n//! - **`native`** — Path-B store: the adapter *pushed* records via\r\n//!   `api history-log`; spt-core reads the perch's `history.log` (the T7/M2a\r\n//!   sink in `spt::api::reporting::append_history`).\r\n//!\r\n//! A record is one non-empty line of normalized output. spt-core treats each as\r\n//! **opaque** — the adapter owns the per-line schema (MANIFEST §Principle); the\r\n//! history subsystem only delimits, never parses.\r\n//\r\n// Clean-room over the M2a seams (the sister's transcript access is `claude -p`\r\n// /`.claude/`-coupled). The fetcher/normalize command lines are the adapter's\r\n// opaque templates run through `run_bounded_command`; spt-core selects no model.\r\n// [impl->REQ-SEAM-HISTORY]\r\n\r\nuse spt_runtime::manifest::{History, HistoryStrategy};\r\nuse spt_runtime::{run_bounded_command, RuntimeError};\r\nuse spt_store::perch::{resolve_perch_path, ParentHint};\r\nuse std::collections::BTreeMap;\r\nuse std::time::Duration;\r\n\r\n/// One normalized transcript record — an opaque line. The adapter defines its\r\n/// schema; spt-core carries it verbatim.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct HistoryRecord {\r\n    pub raw: String,\r\n}\r\n\r\n/// A history-access failure. Carries an operator message; never panics, never\r\n/// hangs (the bounded commands are timeout-governed).\r\n#[derive(Debug)]\r\npub enum HistoryError {\r\n    /// The chosen strategy is missing a field the manifest should have required\r\n    /// (defensive — `Manifest::validate` already gates this).\r\n    MissingField {\r\n        strategy: HistoryStrategy,\r\n        field: &'static str,\r\n    },\r\n    /// A `fetcher` / `normalize_command` spawn, fill, or timeout failure.\r\n    Runtime(RuntimeError),\r\n    /// The bounded command exited non-zero (its stderr, for the operator).\r\n    NonZero { code: Option<i32>, stderr: String },\r\n    /// Reading the raw transcript (`locate_normalize`) or the native log failed.\r\n    Read(std::io::Error),\r\n}\r\n\r\nimpl std::fmt::Display for HistoryError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        match self {\r\n            HistoryError::MissingField { strategy, field } => {\r\n                write!(f, \"[history] strategy {strategy:?} requires `{field}`\")\r\n            }\r\n            HistoryError::Runtime(e) => write!(f, \"history command failed: {e}\"),\r\n            HistoryError::NonZero { code, stderr } => {\r\n                write!(f, \"history command exited {code:?}: {}\", stderr.trim())\r\n            }\r\n            HistoryError::Read(e) => write!(f, \"reading transcript failed: {e}\"),\r\n        }\r\n    }\r\n}\r\n\r\nimpl std::error::Error for HistoryError {}\r\n\r\nimpl From<RuntimeError> for HistoryError {\r\n    fn from(e: RuntimeError) -> Self {\r\n        HistoryError::Runtime(e)\r\n    }\r\n}\r\n\r\n/// Fetch + normalize the perch's history via the manifest's `[history]`\r\n/// strategy. `keys` supplies the substitution values (`session_id`, etc.) for\r\n/// the `fetcher` / `locate_template` / `normalize_command` templates; `id` +\r\n/// `hint` resolve the native Path-B store. A bounded command that hangs is\r\n/// killed (never an indefinite block); a missing native log yields *no* records\r\n/// (a fresh perch), not an error.\r\npub fn fetch_history(\r\n    history: &History,\r\n    keys: &BTreeMap<String, String>,\r\n    id: &str,\r\n    hint: ParentHint<'_>,\r\n    timeout: Duration,\r\n) -> Result<Vec<HistoryRecord>, HistoryError> {\r\n    match history.strategy {\r\n        HistoryStrategy::Fetcher => {\r\n            let cmd = history\r\n                .fetcher\r\n                .as_deref()\r\n                .ok_or(HistoryError::MissingField {\r\n                    strategy: HistoryStrategy::Fetcher,\r\n                    field: \"fetcher\",\r\n                })?;\r\n            let out = run_bounded_command(cmd, keys, timeout, None)?;\r\n            ok_or_nonzero(out).map(|s| records(&s))\r\n        }\r\n        HistoryStrategy::LocateNormalize => {\r\n            let locate = history\r\n                .locate_template\r\n                .as_deref()\r\n                .ok_or(HistoryError::MissingField {\r\n                    strategy: HistoryStrategy::LocateNormalize,\r\n                    field: \"locate_template\",\r\n                })?;\r\n            let normalize =\r\n                history\r\n                    .normalize_command\r\n                    .as_deref()\r\n                    .ok_or(HistoryError::MissingField {\r\n                        strategy: HistoryStrategy::LocateNormalize,\r\n                        field: \"normalize_command\",\r\n                    })?;\r\n            // Fill the locate template to find the raw transcript, read it, then\r\n            // hand the bytes to the normalizer on stdin.\r\n            let path = spt_runtime::runtime::fill_template(locate, keys)?;\r\n            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\r\n            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\r\n            ok_or_nonzero(out).map(|s| records(&s))\r\n        }\r\n        HistoryStrategy::Native => {\r\n            let log = resolve_perch_path(id, hint).join(\"history.log\");\r\n            match std::fs::read_to_string(&log) {\r\n                Ok(s) => Ok(records(&s)),\r\n                // No log yet ⇒ a fresh perch with no recorded history.\r\n                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\r\n                Err(e) => Err(HistoryError::Read(e)),\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// A bounded command's stdout, or `NonZero` carrying its stderr if it failed.\r\nfn ok_or_nonzero(out: spt_runtime::BoundedOutput) -> Result<String, HistoryError> {\r\n    if out.success() {\r\n        Ok(out.stdout)\r\n    } else {\r\n        Err(HistoryError::NonZero {\r\n            code: out.status_code,\r\n            stderr: out.stderr,\r\n        })\r\n    }\r\n}\r\n\r\n/// Split normalized output into records: one per non-empty line, trailing\r\n/// whitespace trimmed. The native store is already newline-delimited; fetchers\r\n/// and normalizers emit the same shape.\r\nfn records(s: &str) -> Vec<HistoryRecord> {\r\n    s.lines()\r\n        .map(str::trim_end)\r\n        .filter(|l| !l.is_empty())\r\n        .map(|l| HistoryRecord { raw: l.to_string() })\r\n        .collect()\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-live\\src\\history.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":352,"totalBytes":6776,"outputLines":158,"outputBytes":6776,"shownRange":{"start":1,"end":158},"nextOffset":159}}},"isError":false,"timestamp":1784271526638}}
{"type":"message","id":"43bfb5d9","parentId":"3ddf18ee","timestamp":"2026-07-17T06:58:46.644Z","message":{"role":"toolResult","toolCallId":"call_kOmXQVaoNyQHELLixJ2hYKQa|fc_038f28d3e93bbfd4016a59d2a681a08196a4d35ebc698fa3b0","toolName":"read","content":[{"type":"text","text":"/// `id`: route each into the two-tier context store (precedence-guarded, llm\n/// source) and **delete** it (spt-core single-writer; the mind never deletes).\n///\n/// A drop is deleted whether its writes `Written` or `Suppressed` — both mean\n/// \"consumed\" (a suppressed drop was a stale snapshot, correctly discarded). A\n/// read/write *error* leaves the drop in place to retry on the next pass.\n///\n/// **Commit-before-delete (F-032, KNOWN-HAZARDS 7.40):** a non-empty\n/// `<project-context>` slice with an EMPTY `project_id` (the endpoint's anchor\n/// cwd unresolved/owlery-internal at ingest time) was never committed anywhere\n/// — deleting the drop would black-hole it. That slice is instead **preserved\n/// as a project-only pending slice under the COMMUNE suffix** (whatever the\n/// source drop's kind — a signoff-suffix pending would be reaped by the\n/// stale-signoff sweep on the next listener start): the live slice (just\n/// committed) is dropped from the pending so a retry never re-commits it, and\n/// the next ingest with a resolvable `project_id` commits + deletes. The\n/// rewrite is idempotent (the pending form re-parses to itself — no churn on\n/// steady retries), and a NEWER commune from the mind overwriting the pending\n/// file is a supersede (a fresher LLM snapshot), not a loss. An endpoint whose\n/// project id NEVER resolves (a psyche-host anchor) retains the pending\n/// indefinitely — a bounded no-op per pass, intentionally preferred over any\n/// deletion heuristic.\n// [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\npub fn ingest_drops(\n    drop_dir: &Path,\n    id: &str,\n    project_id: &str,\n    now_ms: u64,\n    window_ms: u64,\n) -> std::io::Result<Vec<Ingested>> {\n    let mut out = Vec::new();\n    for (kind, suffix) in [\n        (DropKind::Commune, COMMUNE_SUFFIX),\n        (DropKind::Signoff, SIGNOFF_SUFFIX),\n    ] {\n        let drop_path = drop_dir.join(format!(\"{id}{suffix}\"));\n        if !drop_path.exists() {\n            continue;\n        }\n        let body = std::fs::read_to_string(&drop_path)?;\n        // A dropped commune/signoff is an LLM-authored snapshot.\n        let slices = parse_two_slice(&body);\n        let writes = route_slices(\n            id,\n            project_id,\n            slices.live.as_deref(),\n            slices.project.as_deref(),\n            now_ms,\n            window_ms,\n        )?;\n        // Un-committable-now ≠ consumed: the project tier is gated on a real\n        // project_id (route_slices), so a non-empty project slice under an\n        // empty id was parsed but never written — preserve it (marker-only\n        // slices route nowhere and are NOT preserved; they carry no content).\n        // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\n        let deferred_project = project_id.is_empty()\n            && slices\n                .project\n                .as_deref()\n                .map(strip_checkpoint_markers)\n                .is_some_and(|p| !p.trim().is_empty());\n        if deferred_project {\n            let pending = format!(\n                \"<project-context>\\n{}\\n</project-context>\\n\",\n                slices.project.as_deref().unwrap_or_default()\n            );\n            // The pending ALWAYS lives under the COMMUNE suffix, whatever the\n            // source drop's kind (doyle #81 gate ruling): a `<id>-signoff.md`\n            // pending would be reaped by `signoff::sweep_stale_signoff` on the\n            // next listener start (\"a leftover must never read as a live\n            // signoff\") — recreating the black-hole on the signoff path. The\n            // pending is a pure project-slice carrier, both suffixes ingest\n            // identically, and the stale-signoff sweep never sees the commune\n            // name. A fresh commune clobbering it = the documented supersede.\n            // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\n            let pending_path = drop_dir.join(format!(\"{id}{COMMUNE_SUFFIX}\"));\n            // Idempotent: a preserved drop re-ingested before the project id\n            // resolves re-parses to this same pending form — rewrite (and log)\n            // only on a state change, never per retry tick.\n            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\n            if current != pending {\n                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\n                eprintln!(\n                    \"COMMUNE_PROJECT_DEFERRED:{id}: project context un-committable now \\\n                     (unresolved project id) — preserved as a project-only pending \\\n                     commune slice for a later resolvable ingest\"\n                );\n            }\n            // A SIGNOFF source is consumed as a drop once its deferred slice is\n            // safely carried by the commune-suffix pending: the signoff file\n            // itself must never linger (write-once-per-generation sentinel —\n            // the sweep rationale), and the live slice was committed above.\n            if kind == DropKind::Signoff {\n                std::fs::remove_file(&drop_path)?;\n            }\n            out.push(Ingested {\n                kind,\n                writes,\n                drop_path,\n                preserved: true,\n            });\n            continue;\n        }\n        // [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop\n        // after consuming it; the mind never does.\n…\n    }\n…\n}\n\n[Showing lines 179-283 of 889. Use :284 to continue]"}],"details":{"truncation":{"content":"/// `id`: route each into the two-tier context store (precedence-guarded, llm\r\n/// source) and **delete** it (spt-core single-writer; the mind never deletes).\r\n///\r\n/// A drop is deleted whether its writes `Written` or `Suppressed` — both mean\r\n/// \"consumed\" (a suppressed drop was a stale snapshot, correctly discarded). A\r\n/// read/write *error* leaves the drop in place to retry on the next pass.\r\n///\r\n/// **Commit-before-delete (F-032, KNOWN-HAZARDS 7.40):** a non-empty\r\n/// `<project-context>` slice with an EMPTY `project_id` (the endpoint's anchor\r\n/// cwd unresolved/owlery-internal at ingest time) was never committed anywhere\r\n/// — deleting the drop would black-hole it. That slice is instead **preserved\r\n/// as a project-only pending slice under the COMMUNE suffix** (whatever the\r\n/// source drop's kind — a signoff-suffix pending would be reaped by the\r\n/// stale-signoff sweep on the next listener start): the live slice (just\r\n/// committed) is dropped from the pending so a retry never re-commits it, and\r\n/// the next ingest with a resolvable `project_id` commits + deletes. The\r\n/// rewrite is idempotent (the pending form re-parses to itself — no churn on\r\n/// steady retries), and a NEWER commune from the mind overwriting the pending\r\n/// file is a supersede (a fresher LLM snapshot), not a loss. An endpoint whose\r\n/// project id NEVER resolves (a psyche-host anchor) retains the pending\r\n/// indefinitely — a bounded no-op per pass, intentionally preferred over any\r\n/// deletion heuristic.\r\n// [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\r\npub fn ingest_drops(\r\n    drop_dir: &Path,\r\n    id: &str,\r\n    project_id: &str,\r\n    now_ms: u64,\r\n    window_ms: u64,\r\n) -> std::io::Result<Vec<Ingested>> {\r\n    let mut out = Vec::new();\r\n    for (kind, suffix) in [\r\n        (DropKind::Commune, COMMUNE_SUFFIX),\r\n        (DropKind::Signoff, SIGNOFF_SUFFIX),\r\n    ] {\r\n        let drop_path = drop_dir.join(format!(\"{id}{suffix}\"));\r\n        if !drop_path.exists() {\r\n            continue;\r\n        }\r\n        let body = std::fs::read_to_string(&drop_path)?;\r\n        // A dropped commune/signoff is an LLM-authored snapshot.\r\n        let slices = parse_two_slice(&body);\r\n        let writes = route_slices(\r\n            id,\r\n            project_id,\r\n            slices.live.as_deref(),\r\n            slices.project.as_deref(),\r\n            now_ms,\r\n            window_ms,\r\n        )?;\r\n        // Un-committable-now ≠ consumed: the project tier is gated on a real\r\n        // project_id (route_slices), so a non-empty project slice under an\r\n        // empty id was parsed but never written — preserve it (marker-only\r\n        // slices route nowhere and are NOT preserved; they carry no content).\r\n        // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\r\n        let deferred_project = project_id.is_empty()\r\n            && slices\r\n                .project\r\n                .as_deref()\r\n                .map(strip_checkpoint_markers)\r\n                .is_some_and(|p| !p.trim().is_empty());\r\n        if deferred_project {\r\n            let pending = format!(\r\n                \"<project-context>\\n{}\\n</project-context>\\n\",\r\n                slices.project.as_deref().unwrap_or_default()\r\n            );\r\n            // The pending ALWAYS lives under the COMMUNE suffix, whatever the\r\n            // source drop's kind (doyle #81 gate ruling): a `<id>-signoff.md`\r\n            // pending would be reaped by `signoff::sweep_stale_signoff` on the\r\n            // next listener start (\"a leftover must never read as a live\r\n            // signoff\") — recreating the black-hole on the signoff path. The\r\n            // pending is a pure project-slice carrier, both suffixes ingest\r\n            // identically, and the stale-signoff sweep never sees the commune\r\n            // name. A fresh commune clobbering it = the documented supersede.\r\n            // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\r\n            let pending_path = drop_dir.join(format!(\"{id}{COMMUNE_SUFFIX}\"));\r\n            // Idempotent: a preserved drop re-ingested before the project id\r\n            // resolves re-parses to this same pending form — rewrite (and log)\r\n            // only on a state change, never per retry tick.\r\n            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\r\n            if current != pending {\r\n                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\r\n                eprintln!(\r\n                    \"COMMUNE_PROJECT_DEFERRED:{id}: project context un-committable now \\\r\n                     (unresolved project id) — preserved as a project-only pending \\\r\n                     commune slice for a later resolvable ingest\"\r\n                );\r\n            }\r\n            // A SIGNOFF source is consumed as a drop once its deferred slice is\r\n            // safely carried by the commune-suffix pending: the signoff file\r\n            // itself must never linger (write-once-per-generation sentinel —\r\n            // the sweep rationale), and the live slice was committed above.\r\n            if kind == DropKind::Signoff {\r\n                std::fs::remove_file(&drop_path)?;\r\n            }\r\n            out.push(Ingested {\r\n                kind,\r\n                writes,\r\n                drop_path,\r\n                preserved: true,\r\n            });\r\n            continue;\r\n        }\r\n        // [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop\r\n        // after consuming it; the mind never does.\r","truncated":true,"truncatedBy":"lines","totalLines":711,"totalBytes":5508,"outputLines":105,"outputBytes":5508,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// `id`: route each into the two-tier context store (precedence-guarded, llm\r\n/// source) and **delete** it (spt-core single-writer; the mind never deletes).\r\n///\r\n/// A drop is deleted whether its writes `Written` or `Suppressed` — both mean\r\n/// \"consumed\" (a suppressed drop was a stale snapshot, correctly discarded). A\r\n/// read/write *error* leaves the drop in place to retry on the next pass.\r\n///\r\n/// **Commit-before-delete (F-032, KNOWN-HAZARDS 7.40):** a non-empty\r\n/// `<project-context>` slice with an EMPTY `project_id` (the endpoint's anchor\r\n/// cwd unresolved/owlery-internal at ingest time) was never committed anywhere\r\n/// — deleting the drop would black-hole it. That slice is instead **preserved\r\n/// as a project-only pending slice under the COMMUNE suffix** (whatever the\r\n/// source drop's kind — a signoff-suffix pending would be reaped by the\r\n/// stale-signoff sweep on the next listener start): the live slice (just\r\n/// committed) is dropped from the pending so a retry never re-commits it, and\r\n/// the next ingest with a resolvable `project_id` commits + deletes. The\r\n/// rewrite is idempotent (the pending form re-parses to itself — no churn on\r\n/// steady retries), and a NEWER commune from the mind overwriting the pending\r\n/// file is a supersede (a fresher LLM snapshot), not a loss. An endpoint whose\r\n/// project id NEVER resolves (a psyche-host anchor) retains the pending\r\n/// indefinitely — a bounded no-op per pass, intentionally preferred over any\r\n/// deletion heuristic.\r\n// [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\r\npub fn ingest_drops(\r\n    drop_dir: &Path,\r\n    id: &str,\r\n    project_id: &str,\r\n    now_ms: u64,\r\n    window_ms: u64,\r\n) -> std::io::Result<Vec<Ingested>> {\r\n    let mut out = Vec::new();\r\n    for (kind, suffix) in [\r\n        (DropKind::Commune, COMMUNE_SUFFIX),\r\n        (DropKind::Signoff, SIGNOFF_SUFFIX),\r\n    ] {\r\n        let drop_path = drop_dir.join(format!(\"{id}{suffix}\"));\r\n        if !drop_path.exists() {\r\n            continue;\r\n        }\r\n        let body = std::fs::read_to_string(&drop_path)?;\r\n        // A dropped commune/signoff is an LLM-authored snapshot.\r\n        let slices = parse_two_slice(&body);\r\n        let writes = route_slices(\r\n            id,\r\n            project_id,\r\n            slices.live.as_deref(),\r\n            slices.project.as_deref(),\r\n            now_ms,\r\n            window_ms,\r\n        )?;\r\n        // Un-committable-now ≠ consumed: the project tier is gated on a real\r\n        // project_id (route_slices), so a non-empty project slice under an\r\n        // empty id was parsed but never written — preserve it (marker-only\r\n        // slices route nowhere and are NOT preserved; they carry no content).\r\n        // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\r\n        let deferred_project = project_id.is_empty()\r\n            && slices\r\n                .project\r\n                .as_deref()\r\n                .map(strip_checkpoint_markers)\r\n                .is_some_and(|p| !p.trim().is_empty());\r\n        if deferred_project {\r\n            let pending = format!(\r\n                \"<project-context>\\n{}\\n</project-context>\\n\",\r\n                slices.project.as_deref().unwrap_or_default()\r\n            );\r\n            // The pending ALWAYS lives under the COMMUNE suffix, whatever the\r\n            // source drop's kind (doyle #81 gate ruling): a `<id>-signoff.md`\r\n            // pending would be reaped by `signoff::sweep_stale_signoff` on the\r\n            // next listener start (\"a leftover must never read as a live\r\n            // signoff\") — recreating the black-hole on the signoff path. The\r\n            // pending is a pure project-slice carrier, both suffixes ingest\r\n            // identically, and the stale-signoff sweep never sees the commune\r\n            // name. A fresh commune clobbering it = the documented supersede.\r\n            // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\r\n            let pending_path = drop_dir.join(format!(\"{id}{COMMUNE_SUFFIX}\"));\r\n            // Idempotent: a preserved drop re-ingested before the project id\r\n            // resolves re-parses to this same pending form — rewrite (and log)\r\n            // only on a state change, never per retry tick.\r\n            let current = std::fs::read_to_string(&pending_path).unwrap_or_default();\r\n            if current != pending {\r\n                spt_store::atomic::atomic_write_string(&pending_path, &pending)?;\r\n                eprintln!(\r\n                    \"COMMUNE_PROJECT_DEFERRED:{id}: project context un-committable now \\\r\n                     (unresolved project id) — preserved as a project-only pending \\\r\n                     commune slice for a later resolvable ingest\"\r\n                );\r\n            }\r\n            // A SIGNOFF source is consumed as a drop once its deferred slice is\r\n            // safely carried by the commune-suffix pending: the signoff file\r\n            // itself must never linger (write-once-per-generation sentinel —\r\n            // the sweep rationale), and the live slice was committed above.\r\n            if kind == DropKind::Signoff {\r\n                std::fs::remove_file(&drop_path)?;\r\n            }\r\n            out.push(Ingested {\r\n                kind,\r\n                writes,\r\n                drop_path,\r\n                preserved: true,\r\n            });\r\n            continue;\r\n        }\r\n        // [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop\r\n        // after consuming it; the mind never does.\r\n…\n    }\n…\n}","startLine":179,"lineNumbers":[179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,null,291,null,293]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-live\\src\\ingest.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":889,"totalBytes":5508,"outputLines":105,"outputBytes":5508,"shownRange":{"start":179,"end":283},"nextOffset":284}}},"isError":false,"timestamp":1784271526644}}
{"type":"message","id":"21ef53ed","parentId":"43bfb5d9","timestamp":"2026-07-17T06:58:46.657Z","message":{"role":"toolResult","toolCallId":"call_pC7j6crGdzLjPq7OJElyRL8P|fc_038f28d3e93bbfd4016a59d2a6814481969ffa72d75fc183d7","toolName":"read","content":[{"type":"text","text":"/// discovery do the work ([`production_resolver`]); hermetic tests inject a\n/// concrete address map (loopback endpoints run no discovery).\npub type PeerResolver = Box<dyn Fn(&str) -> Option<serde_json::Value> + Send>;\n\n/// The production resolver: an id-only iroh address — mDNS on the LAN, n0\n/// DNS on the WAN (`addr_for_node_hex`).\npub fn production_resolver() -> PeerResolver {\n    Box::new(|hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()))\n}\n\n/// Where this node's pump-side stores live. Production = the canonical\n/// SPT_HOME layout; the loopback E2E gives its in-process \"node\" distinct\n/// roots so two nodes can share one test process.\npub struct PumpPaths {\n    /// The perch tree (registry advertisement scan).\n    pub owlery: PathBuf,\n    /// The context store root ([`ContextStore::open_or_init_in`]).\n    pub ctx_root: PathBuf,\n    /// The notif spool DB ([`NotifStore::open_at`]).\n    pub notif_db: PathBuf,\n    /// Transient pull scratch.\n    pub scratch: PathBuf,\n    /// `subnet.json` ([`SubnetStore::load_from`]).\n    pub subnets: PathBuf,\n    /// `visibility.json` ([`VisibilityStore::load_from`]).\n    pub vis: PathBuf,\n    /// The pump's durable open-op counter (its own file — colliding with the\n    /// registry's advertisement epochs would break neither, but a shared\n    /// in-memory cursor pair over one file double-mints; separate files keep\n    /// each `EpochSource` the sole writer of its counter).\n    pub ops: PathBuf,\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\n    pub releases: PathBuf,\n    /// `release-keys.json` ([`VerifyPolicy::load_from`]).\n    pub release_keys: PathBuf,\n    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a\n    /// dormant→active transition drops it; the pump consumes it and forces a\n    /// sync round NOW instead of waiting out `cadence.sync`\n    /// ([`crate::resting::PULL_MARKER_FILE`]).\n    pub pull_marker: PathBuf,\n    /// The event-driven advertisement marker (M8 decision 15, REQ-CONV-2):\n    /// an endpoint online/offline transition drops it; the pump consumes it\n    /// and forces a registry round NOW instead of waiting out\n    /// `cadence.registry` ([`crate::registryhost::ADVERTISE_MARKER_FILE`]).\n    pub advertise_marker: PathBuf,\n    /// `peer-addrs.json` — last-known peer addresses (M8 decisions 14/20,\n    /// REQ-CONV-1): consulted before id-only discovery, written back after\n    /// every successful dial ([`spt_store::peeraddrs::PeerAddrStore`]).\n    pub peer_addrs: PathBuf,\n    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):\n    /// epoch-ms written while the loop is alive, rendered by\n    /// `spt daemon status` / the `subnet status` stall warning\n    /// ([`crate::endpoint::pump_heartbeat_path`]).\n    pub heartbeat: PathBuf,\n    /// `rotation-pending.json` (Mesh-D7, REQ-MESH-4): the timeboxed seed-rotation\n    /// schedule a `subnet revoke` writes; the pump fires due entries each\n    /// registry tick ([`spt_store::rotation::RotationPending`]).\n    pub rotations: PathBuf,\n}\n\nimpl PumpPaths {\n    /// The canonical SPT_HOME layout.\n    pub fn production() -> PumpPaths {\n        let home = perch::spt_home();\n        let identity = perch::identity_dir();\n        PumpPaths {\n            owlery: perch::owlery_dir(),\n            ctx_root: home.join(\"context\"),\n            notif_db: identity.join(\"notifs.db\"),\n            scratch: home.join(\"scratch\").join(\"pump\"),\n            subnets: identity.join(\"subnet.json\"),\n            vis: identity.join(\"visibility.json\"),\n            ops: identity.join(\"pump-ops.json\"),\n            releases: home.join(\"releases\"),\n            release_keys: identity.join(\"release-keys.json\"),\n            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),\n            advertise_marker: identity.join(crate::registryhost::ADVERTISE_MARKER_FILE),\n            peer_addrs: identity.join(\"peer-addrs.json\"),\n            heartbeat: crate::endpoint::pump_heartbeat_path(),\n            rotations: identity.join(\"rotation-pending.json\"),\n        }\n    }\n}\n\n/// Pump cadences (config-driven in production: [`DaemonConfig`]).\n#[derive(Debug, Clone, Copy)]\npub struct PumpCadence {\n    pub registry: Duration,\n    pub notif: Duration,\n    pub sync: Duration,\n    pub update: Duration,\n    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): swept each\n    /// registry tick, before re-advertisement.\n    pub registry_evict_after: Duration,\n}\n\nimpl PumpCadence {\n    /// From the daemon config knobs.\n    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {\n        PumpCadence {\n            registry: cfg.registry_pump_period,\n            notif: cfg.notif_pump_period,\n            sync: cfg.sync_pull_period,\n            update: cfg.update_check_period,\n            registry_evict_after: cfg.registry_evict_after,\n        }\n    }\n}\n\n/// The pump's resolved knobs: cadences + the REQ-UPD-4 consent stance.\n#[derive(Debug, Clone, Copy)]\npub struct PumpConfig {\n    pub cadence: PumpCadence,\n    /// `true` = the explicit opt-in to prompt-free updates; default `false`\n    /// surfaces every staged release as a consent notif.\n    pub full_auto_update: bool,\n}\n\nimpl PumpConfig {\n    /// From the daemon config knobs.\n    pub fn from_config(cfg: &DaemonConfig) -> PumpConfig {\n        PumpConfig {\n            cadence: PumpCadence::from_config(cfg),\n            full_auto_update: cfg.full_auto_update,\n        }\n    }\n}\n\n/// A pull observer: `(peer_hex, report)` after every completed sync pull.\npub type OnPull = Box<dyn FnMut(&str, &SyncPullReport) + Send>;\n\n/// Observation hooks. `on_pull` fires after every completed sync pull —\n/// the conflict surface (ADR-0013) and the E2E's probe.\npub struct PumpHooks {\n    pub on_pull: OnPull,\n}\n\nimpl Default for PumpHooks {\n    /// The production hook: conflicts are loud, never silent — the artifacts\n    /// already persist (hazard 6.6); the reconcile turn runs at activation.\n    fn default() -> PumpHooks {\n        PumpHooks {\n            on_pull: Box::new(|peer, report| {\n                for (branch, file) in report.conflicts() {\n                    eprintln!(\"SYNC_CONFLICT:{peer}:{branch}:{file}\");\n                }\n            }),\n        }\n    }\n}\n\n/// The pump-worker seam (arch-review rev 2 card 1): each cadenced leg —\n/// registry / notif / sync / update — is a [`PumpWorker`] the pump schedules\n/// (cadence + wake markers → the pure [`due`] kernel) and fans out per peer\n/// (per-subnet × per-roster-member). The pump owns *when* and *toward whom*;\n/// the worker owns *what* — its node-local `pre_round` and its `peer_step`.\n///\n/// **KH 7.4 — never an LLM-bearing worker.** A pump worker is cadenced,\n/// fire-and-forget peer IO. LLM-bearing work (the bounded Psyche reconcile\n/// turn) runs at activation paths with a manifest runtime to ride (ADR-0013),\n/// NEVER on the pump thread — the seam must not become the invitation to put\n/// it here.\ntrait PumpWorker {\n    /// This worker's scheduling period (config-driven in production).\n    fn cadence(&self) -> Duration;\n\n    /// Take this worker's exactly-once wake marker — a wake forces one round\n    /// NOW regardless of cadence. Called every tick so a marker is consumed\n    /// whether or not the leg is otherwise due; the take IS the remove, so one\n    /// wake claims exactly one forced round. Default: no wake edge.\n    fn poll_wake(&mut self) -> bool {\n        false\n    }\n\n    /// Node-local work run ONCE per due round, before any `peer_step`.\n    /// Default: nothing.\n    fn pre_round(&mut self, _ctx: &RoundCtx) {}\n\n    /// The per-peer step for one `(subnet, peer)` under this round's context. A\n    /// failure aborts the remaining due workers for THIS peer and drops its\n    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\n    fn peer_step(\n        &mut self,\n        io: &mut PeerIo,\n        subnet: &str,\n        peer_hex: &str,\n        ctx: &RoundCtx,\n    ) -> io::Result<()>;\n}\n\n/// Per-round shared loads (§Frozen-3): the config-ish stores the shell reads\n/// ONCE per due round and hands every worker by `&RoundCtx`. The single-read\n/// invariant — push targets and the sync gate see the SAME roster — is made a\n/// structural fact by loading the roster-backed [`SyncPolicy`] here, once.\n/// Carries ONLY per-round shared loads; never long-lived deps (those are\n/// construction-captured by each worker).\nstruct RoundCtx {\n    /// `subnet.json` — the member subnets iterated for fan-out.\n    subnets: SubnetStore,\n    /// `attachment.json` — a detached subnet is skipped wholesale (REQ-SUBNET-5).\n    attachment: spt_store::attachment::AttachmentStore,\n    /// The roster-backed sync gate — one store feeding both the push targets\n    /// and the sync authorization (symmetric scoping).\n    policy: SyncPolicy,\n}\n\nimpl RoundCtx {\n    /// Load the per-round shared stores from the pump's paths (the access-gate\n    /// freshness stance — every config-ish store re-read per round so an edit\n    /// takes effect within one tick, no restart).\n    fn load(paths: &PumpPaths) -> RoundCtx {\n        let subnets = SubnetStore::load_from(&paths.subnets);\n        // Serve-state (M8 decision 6, REQ-SUBNET-5): a detached subnet is\n        // neither advertised into nor connected to — skipped wholesale in the\n        // per-subnet loop below.\n        // [impl->REQ-SUBNET-5]\n        let attachment = spt_store::attachment::AttachmentStore::load_from(\n            &paths\n                .subnets\n                .parent()\n                .map(|p| p.join(\"attachment.json\"))\n                .unwrap_or_else(|| PathBuf::from(\"attachment.json\")),\n        );\n        // THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster\n        // (all members), not the directly-paired set. The one roster loaded\n        // here feeds both the push (who we dial) and the sync gate (who we\n        // authorize), so they read one store.\n        let roster_path = paths\n            .subnets\n            .parent()\n            .map(|p| p.join(\"roster.json\"))\n            .unwrap_or_else(|| PathBuf::from(\"roster.json\"));\n        let policy = SyncPolicy {\n            subnets: subnets\n                .subnets\n                .iter()\n                .map(|s| (s.name.clone(), s.hide_new_endpoints))\n                .collect(),\n…\n        };\n…\n    }\n}\n\n[Showing lines 209-443 of 1982. Use :444 to continue]"}],"details":{"truncation":{"content":"/// discovery do the work ([`production_resolver`]); hermetic tests inject a\r\n/// concrete address map (loopback endpoints run no discovery).\r\npub type PeerResolver = Box<dyn Fn(&str) -> Option<serde_json::Value> + Send>;\r\n\r\n/// The production resolver: an id-only iroh address — mDNS on the LAN, n0\r\n/// DNS on the WAN (`addr_for_node_hex`).\r\npub fn production_resolver() -> PeerResolver {\r\n    Box::new(|hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()))\r\n}\r\n\r\n/// Where this node's pump-side stores live. Production = the canonical\r\n/// SPT_HOME layout; the loopback E2E gives its in-process \"node\" distinct\r\n/// roots so two nodes can share one test process.\r\npub struct PumpPaths {\r\n    /// The perch tree (registry advertisement scan).\r\n    pub owlery: PathBuf,\r\n    /// The context store root ([`ContextStore::open_or_init_in`]).\r\n    pub ctx_root: PathBuf,\r\n    /// The notif spool DB ([`NotifStore::open_at`]).\r\n    pub notif_db: PathBuf,\r\n    /// Transient pull scratch.\r\n    pub scratch: PathBuf,\r\n    /// `subnet.json` ([`SubnetStore::load_from`]).\r\n    pub subnets: PathBuf,\r\n    /// `visibility.json` ([`VisibilityStore::load_from`]).\r\n    pub vis: PathBuf,\r\n    /// The pump's durable open-op counter (its own file — colliding with the\r\n    /// registry's advertisement epochs would break neither, but a shared\r\n    /// in-memory cursor pair over one file double-mints; separate files keep\r\n    /// each `EpochSource` the sole writer of its counter).\r\n    pub ops: PathBuf,\r\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\r\n    pub releases: PathBuf,\r\n    /// `release-keys.json` ([`VerifyPolicy::load_from`]).\r\n    pub release_keys: PathBuf,\r\n    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a\r\n    /// dormant→active transition drops it; the pump consumes it and forces a\r\n    /// sync round NOW instead of waiting out `cadence.sync`\r\n    /// ([`crate::resting::PULL_MARKER_FILE`]).\r\n    pub pull_marker: PathBuf,\r\n    /// The event-driven advertisement marker (M8 decision 15, REQ-CONV-2):\r\n    /// an endpoint online/offline transition drops it; the pump consumes it\r\n    /// and forces a registry round NOW instead of waiting out\r\n    /// `cadence.registry` ([`crate::registryhost::ADVERTISE_MARKER_FILE`]).\r\n    pub advertise_marker: PathBuf,\r\n    /// `peer-addrs.json` — last-known peer addresses (M8 decisions 14/20,\r\n    /// REQ-CONV-1): consulted before id-only discovery, written back after\r\n    /// every successful dial ([`spt_store::peeraddrs::PeerAddrStore`]).\r\n    pub peer_addrs: PathBuf,\r\n    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):\r\n    /// epoch-ms written while the loop is alive, rendered by\r\n    /// `spt daemon status` / the `subnet status` stall warning\r\n    /// ([`crate::endpoint::pump_heartbeat_path`]).\r\n    pub heartbeat: PathBuf,\r\n    /// `rotation-pending.json` (Mesh-D7, REQ-MESH-4): the timeboxed seed-rotation\r\n    /// schedule a `subnet revoke` writes; the pump fires due entries each\r\n    /// registry tick ([`spt_store::rotation::RotationPending`]).\r\n    pub rotations: PathBuf,\r\n}\r\n\r\nimpl PumpPaths {\r\n    /// The canonical SPT_HOME layout.\r\n    pub fn production() -> PumpPaths {\r\n        let home = perch::spt_home();\r\n        let identity = perch::identity_dir();\r\n        PumpPaths {\r\n            owlery: perch::owlery_dir(),\r\n            ctx_root: home.join(\"context\"),\r\n            notif_db: identity.join(\"notifs.db\"),\r\n            scratch: home.join(\"scratch\").join(\"pump\"),\r\n            subnets: identity.join(\"subnet.json\"),\r\n            vis: identity.join(\"visibility.json\"),\r\n            ops: identity.join(\"pump-ops.json\"),\r\n            releases: home.join(\"releases\"),\r\n            release_keys: identity.join(\"release-keys.json\"),\r\n            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),\r\n            advertise_marker: identity.join(crate::registryhost::ADVERTISE_MARKER_FILE),\r\n            peer_addrs: identity.join(\"peer-addrs.json\"),\r\n            heartbeat: crate::endpoint::pump_heartbeat_path(),\r\n            rotations: identity.join(\"rotation-pending.json\"),\r\n        }\r\n    }\r\n}\r\n\r\n/// Pump cadences (config-driven in production: [`DaemonConfig`]).\r\n#[derive(Debug, Clone, Copy)]\r\npub struct PumpCadence {\r\n    pub registry: Duration,\r\n    pub notif: Duration,\r\n    pub sync: Duration,\r\n    pub update: Duration,\r\n    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): swept each\r\n    /// registry tick, before re-advertisement.\r\n    pub registry_evict_after: Duration,\r\n}\r\n\r\nimpl PumpCadence {\r\n    /// From the daemon config knobs.\r\n    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {\r\n        PumpCadence {\r\n            registry: cfg.registry_pump_period,\r\n            notif: cfg.notif_pump_period,\r\n            sync: cfg.sync_pull_period,\r\n            update: cfg.update_check_period,\r\n            registry_evict_after: cfg.registry_evict_after,\r\n        }\r\n    }\r\n}\r\n\r\n/// The pump's resolved knobs: cadences + the REQ-UPD-4 consent stance.\r\n#[derive(Debug, Clone, Copy)]\r\npub struct PumpConfig {\r\n    pub cadence: PumpCadence,\r\n    /// `true` = the explicit opt-in to prompt-free updates; default `false`\r\n    /// surfaces every staged release as a consent notif.\r\n    pub full_auto_update: bool,\r\n}\r\n\r\nimpl PumpConfig {\r\n    /// From the daemon config knobs.\r\n    pub fn from_config(cfg: &DaemonConfig) -> PumpConfig {\r\n        PumpConfig {\r\n            cadence: PumpCadence::from_config(cfg),\r\n            full_auto_update: cfg.full_auto_update,\r\n        }\r\n    }\r\n}\r\n\r\n/// A pull observer: `(peer_hex, report)` after every completed sync pull.\r\npub type OnPull = Box<dyn FnMut(&str, &SyncPullReport) + Send>;\r\n\r\n/// Observation hooks. `on_pull` fires after every completed sync pull —\r\n/// the conflict surface (ADR-0013) and the E2E's probe.\r\npub struct PumpHooks {\r\n    pub on_pull: OnPull,\r\n}\r\n\r\nimpl Default for PumpHooks {\r\n    /// The production hook: conflicts are loud, never silent — the artifacts\r\n    /// already persist (hazard 6.6); the reconcile turn runs at activation.\r\n    fn default() -> PumpHooks {\r\n        PumpHooks {\r\n            on_pull: Box::new(|peer, report| {\r\n                for (branch, file) in report.conflicts() {\r\n                    eprintln!(\"SYNC_CONFLICT:{peer}:{branch}:{file}\");\r\n                }\r\n            }),\r\n        }\r\n    }\r\n}\r\n\r\n/// The pump-worker seam (arch-review rev 2 card 1): each cadenced leg —\r\n/// registry / notif / sync / update — is a [`PumpWorker`] the pump schedules\r\n/// (cadence + wake markers → the pure [`due`] kernel) and fans out per peer\r\n/// (per-subnet × per-roster-member). The pump owns *when* and *toward whom*;\r\n/// the worker owns *what* — its node-local `pre_round` and its `peer_step`.\r\n///\r\n/// **KH 7.4 — never an LLM-bearing worker.** A pump worker is cadenced,\r\n/// fire-and-forget peer IO. LLM-bearing work (the bounded Psyche reconcile\r\n/// turn) runs at activation paths with a manifest runtime to ride (ADR-0013),\r\n/// NEVER on the pump thread — the seam must not become the invitation to put\r\n/// it here.\r\ntrait PumpWorker {\r\n    /// This worker's scheduling period (config-driven in production).\r\n    fn cadence(&self) -> Duration;\r\n\r\n    /// Take this worker's exactly-once wake marker — a wake forces one round\r\n    /// NOW regardless of cadence. Called every tick so a marker is consumed\r\n    /// whether or not the leg is otherwise due; the take IS the remove, so one\r\n    /// wake claims exactly one forced round. Default: no wake edge.\r\n    fn poll_wake(&mut self) -> bool {\r\n        false\r\n    }\r\n\r\n    /// Node-local work run ONCE per due round, before any `peer_step`.\r\n    /// Default: nothing.\r\n    fn pre_round(&mut self, _ctx: &RoundCtx) {}\r\n\r\n    /// The per-peer step for one `(subnet, peer)` under this round's context. A\r\n    /// failure aborts the remaining due workers for THIS peer and drops its\r\n    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        peer_hex: &str,\r\n        ctx: &RoundCtx,\r\n    ) -> io::Result<()>;\r\n}\r\n\r\n/// Per-round shared loads (§Frozen-3): the config-ish stores the shell reads\r\n/// ONCE per due round and hands every worker by `&RoundCtx`. The single-read\r\n/// invariant — push targets and the sync gate see the SAME roster — is made a\r\n/// structural fact by loading the roster-backed [`SyncPolicy`] here, once.\r\n/// Carries ONLY per-round shared loads; never long-lived deps (those are\r\n/// construction-captured by each worker).\r\nstruct RoundCtx {\r\n    /// `subnet.json` — the member subnets iterated for fan-out.\r\n    subnets: SubnetStore,\r\n    /// `attachment.json` — a detached subnet is skipped wholesale (REQ-SUBNET-5).\r\n    attachment: spt_store::attachment::AttachmentStore,\r\n    /// The roster-backed sync gate — one store feeding both the push targets\r\n    /// and the sync authorization (symmetric scoping).\r\n    policy: SyncPolicy,\r\n}\r\n\r\nimpl RoundCtx {\r\n    /// Load the per-round shared stores from the pump's paths (the access-gate\r\n    /// freshness stance — every config-ish store re-read per round so an edit\r\n    /// takes effect within one tick, no restart).\r\n    fn load(paths: &PumpPaths) -> RoundCtx {\r\n        let subnets = SubnetStore::load_from(&paths.subnets);\r\n        // Serve-state (M8 decision 6, REQ-SUBNET-5): a detached subnet is\r\n        // neither advertised into nor connected to — skipped wholesale in the\r\n        // per-subnet loop below.\r\n        // [impl->REQ-SUBNET-5]\r\n        let attachment = spt_store::attachment::AttachmentStore::load_from(\r\n            &paths\r\n                .subnets\r\n                .parent()\r\n                .map(|p| p.join(\"attachment.json\"))\r\n                .unwrap_or_else(|| PathBuf::from(\"attachment.json\")),\r\n        );\r\n        // THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster\r\n        // (all members), not the directly-paired set. The one roster loaded\r\n        // here feeds both the push (who we dial) and the sync gate (who we\r\n        // authorize), so they read one store.\r\n        let roster_path = paths\r\n            .subnets\r\n            .parent()\r\n            .map(|p| p.join(\"roster.json\"))\r\n            .unwrap_or_else(|| PathBuf::from(\"roster.json\"));\r\n        let policy = SyncPolicy {\r\n            subnets: subnets\r\n                .subnets\r","truncated":true,"truncatedBy":"lines","totalLines":1774,"totalBytes":10458,"outputLines":235,"outputBytes":10458,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// discovery do the work ([`production_resolver`]); hermetic tests inject a\r\n/// concrete address map (loopback endpoints run no discovery).\r\npub type PeerResolver = Box<dyn Fn(&str) -> Option<serde_json::Value> + Send>;\r\n\r\n/// The production resolver: an id-only iroh address — mDNS on the LAN, n0\r\n/// DNS on the WAN (`addr_for_node_hex`).\r\npub fn production_resolver() -> PeerResolver {\r\n    Box::new(|hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()))\r\n}\r\n\r\n/// Where this node's pump-side stores live. Production = the canonical\r\n/// SPT_HOME layout; the loopback E2E gives its in-process \"node\" distinct\r\n/// roots so two nodes can share one test process.\r\npub struct PumpPaths {\r\n    /// The perch tree (registry advertisement scan).\r\n    pub owlery: PathBuf,\r\n    /// The context store root ([`ContextStore::open_or_init_in`]).\r\n    pub ctx_root: PathBuf,\r\n    /// The notif spool DB ([`NotifStore::open_at`]).\r\n    pub notif_db: PathBuf,\r\n    /// Transient pull scratch.\r\n    pub scratch: PathBuf,\r\n    /// `subnet.json` ([`SubnetStore::load_from`]).\r\n    pub subnets: PathBuf,\r\n    /// `visibility.json` ([`VisibilityStore::load_from`]).\r\n    pub vis: PathBuf,\r\n    /// The pump's durable open-op counter (its own file — colliding with the\r\n    /// registry's advertisement epochs would break neither, but a shared\r\n    /// in-memory cursor pair over one file double-mints; separate files keep\r\n    /// each `EpochSource` the sole writer of its counter).\r\n    pub ops: PathBuf,\r\n    /// The staged-release cache dir ([`ReleaseCache::open`]).\r\n    pub releases: PathBuf,\r\n    /// `release-keys.json` ([`VerifyPolicy::load_from`]).\r\n    pub release_keys: PathBuf,\r\n    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a\r\n    /// dormant→active transition drops it; the pump consumes it and forces a\r\n    /// sync round NOW instead of waiting out `cadence.sync`\r\n    /// ([`crate::resting::PULL_MARKER_FILE`]).\r\n    pub pull_marker: PathBuf,\r\n    /// The event-driven advertisement marker (M8 decision 15, REQ-CONV-2):\r\n    /// an endpoint online/offline transition drops it; the pump consumes it\r\n    /// and forces a registry round NOW instead of waiting out\r\n    /// `cadence.registry` ([`crate::registryhost::ADVERTISE_MARKER_FILE`]).\r\n    pub advertise_marker: PathBuf,\r\n    /// `peer-addrs.json` — last-known peer addresses (M8 decisions 14/20,\r\n    /// REQ-CONV-1): consulted before id-only discovery, written back after\r\n    /// every successful dial ([`spt_store::peeraddrs::PeerAddrStore`]).\r\n    pub peer_addrs: PathBuf,\r\n    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):\r\n    /// epoch-ms written while the loop is alive, rendered by\r\n    /// `spt daemon status` / the `subnet status` stall warning\r\n    /// ([`crate::endpoint::pump_heartbeat_path`]).\r\n    pub heartbeat: PathBuf,\r\n    /// `rotation-pending.json` (Mesh-D7, REQ-MESH-4): the timeboxed seed-rotation\r\n    /// schedule a `subnet revoke` writes; the pump fires due entries each\r\n    /// registry tick ([`spt_store::rotation::RotationPending`]).\r\n    pub rotations: PathBuf,\r\n}\r\n\r\nimpl PumpPaths {\r\n    /// The canonical SPT_HOME layout.\r\n    pub fn production() -> PumpPaths {\r\n        let home = perch::spt_home();\r\n        let identity = perch::identity_dir();\r\n        PumpPaths {\r\n            owlery: perch::owlery_dir(),\r\n            ctx_root: home.join(\"context\"),\r\n            notif_db: identity.join(\"notifs.db\"),\r\n            scratch: home.join(\"scratch\").join(\"pump\"),\r\n            subnets: identity.join(\"subnet.json\"),\r\n            vis: identity.join(\"visibility.json\"),\r\n            ops: identity.join(\"pump-ops.json\"),\r\n            releases: home.join(\"releases\"),\r\n            release_keys: identity.join(\"release-keys.json\"),\r\n            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),\r\n            advertise_marker: identity.join(crate::registryhost::ADVERTISE_MARKER_FILE),\r\n            peer_addrs: identity.join(\"peer-addrs.json\"),\r\n            heartbeat: crate::endpoint::pump_heartbeat_path(),\r\n            rotations: identity.join(\"rotation-pending.json\"),\r\n        }\r\n    }\r\n}\r\n\r\n/// Pump cadences (config-driven in production: [`DaemonConfig`]).\r\n#[derive(Debug, Clone, Copy)]\r\npub struct PumpCadence {\r\n    pub registry: Duration,\r\n    pub notif: Duration,\r\n    pub sync: Duration,\r\n    pub update: Duration,\r\n    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): swept each\r\n    /// registry tick, before re-advertisement.\r\n    pub registry_evict_after: Duration,\r\n}\r\n\r\nimpl PumpCadence {\r\n    /// From the daemon config knobs.\r\n    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {\r\n        PumpCadence {\r\n            registry: cfg.registry_pump_period,\r\n            notif: cfg.notif_pump_period,\r\n            sync: cfg.sync_pull_period,\r\n            update: cfg.update_check_period,\r\n            registry_evict_after: cfg.registry_evict_after,\r\n        }\r\n    }\r\n}\r\n\r\n/// The pump's resolved knobs: cadences + the REQ-UPD-4 consent stance.\r\n#[derive(Debug, Clone, Copy)]\r\npub struct PumpConfig {\r\n    pub cadence: PumpCadence,\r\n    /// `true` = the explicit opt-in to prompt-free updates; default `false`\r\n    /// surfaces every staged release as a consent notif.\r\n    pub full_auto_update: bool,\r\n}\r\n\r\nimpl PumpConfig {\r\n    /// From the daemon config knobs.\r\n    pub fn from_config(cfg: &DaemonConfig) -> PumpConfig {\r\n        PumpConfig {\r\n            cadence: PumpCadence::from_config(cfg),\r\n            full_auto_update: cfg.full_auto_update,\r\n        }\r\n    }\r\n}\r\n\r\n/// A pull observer: `(peer_hex, report)` after every completed sync pull.\r\npub type OnPull = Box<dyn FnMut(&str, &SyncPullReport) + Send>;\r\n\r\n/// Observation hooks. `on_pull` fires after every completed sync pull —\r\n/// the conflict surface (ADR-0013) and the E2E's probe.\r\npub struct PumpHooks {\r\n    pub on_pull: OnPull,\r\n}\r\n\r\nimpl Default for PumpHooks {\r\n    /// The production hook: conflicts are loud, never silent — the artifacts\r\n    /// already persist (hazard 6.6); the reconcile turn runs at activation.\r\n    fn default() -> PumpHooks {\r\n        PumpHooks {\r\n            on_pull: Box::new(|peer, report| {\r\n                for (branch, file) in report.conflicts() {\r\n                    eprintln!(\"SYNC_CONFLICT:{peer}:{branch}:{file}\");\r\n                }\r\n            }),\r\n        }\r\n    }\r\n}\r\n\r\n/// The pump-worker seam (arch-review rev 2 card 1): each cadenced leg —\r\n/// registry / notif / sync / update — is a [`PumpWorker`] the pump schedules\r\n/// (cadence + wake markers → the pure [`due`] kernel) and fans out per peer\r\n/// (per-subnet × per-roster-member). The pump owns *when* and *toward whom*;\r\n/// the worker owns *what* — its node-local `pre_round` and its `peer_step`.\r\n///\r\n/// **KH 7.4 — never an LLM-bearing worker.** A pump worker is cadenced,\r\n/// fire-and-forget peer IO. LLM-bearing work (the bounded Psyche reconcile\r\n/// turn) runs at activation paths with a manifest runtime to ride (ADR-0013),\r\n/// NEVER on the pump thread — the seam must not become the invitation to put\r\n/// it here.\r\ntrait PumpWorker {\r\n    /// This worker's scheduling period (config-driven in production).\r\n    fn cadence(&self) -> Duration;\r\n\r\n    /// Take this worker's exactly-once wake marker — a wake forces one round\r\n    /// NOW regardless of cadence. Called every tick so a marker is consumed\r\n    /// whether or not the leg is otherwise due; the take IS the remove, so one\r\n    /// wake claims exactly one forced round. Default: no wake edge.\r\n    fn poll_wake(&mut self) -> bool {\r\n        false\r\n    }\r\n\r\n    /// Node-local work run ONCE per due round, before any `peer_step`.\r\n    /// Default: nothing.\r\n    fn pre_round(&mut self, _ctx: &RoundCtx) {}\r\n\r\n    /// The per-peer step for one `(subnet, peer)` under this round's context. A\r\n    /// failure aborts the remaining due workers for THIS peer and drops its\r\n    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        peer_hex: &str,\r\n        ctx: &RoundCtx,\r\n    ) -> io::Result<()>;\r\n}\r\n\r\n/// Per-round shared loads (§Frozen-3): the config-ish stores the shell reads\r\n/// ONCE per due round and hands every worker by `&RoundCtx`. The single-read\r\n/// invariant — push targets and the sync gate see the SAME roster — is made a\r\n/// structural fact by loading the roster-backed [`SyncPolicy`] here, once.\r\n/// Carries ONLY per-round shared loads; never long-lived deps (those are\r\n/// construction-captured by each worker).\r\nstruct RoundCtx {\r\n    /// `subnet.json` — the member subnets iterated for fan-out.\r\n    subnets: SubnetStore,\r\n    /// `attachment.json` — a detached subnet is skipped wholesale (REQ-SUBNET-5).\r\n    attachment: spt_store::attachment::AttachmentStore,\r\n    /// The roster-backed sync gate — one store feeding both the push targets\r\n    /// and the sync authorization (symmetric scoping).\r\n    policy: SyncPolicy,\r\n}\r\n\r\nimpl RoundCtx {\r\n    /// Load the per-round shared stores from the pump's paths (the access-gate\r\n    /// freshness stance — every config-ish store re-read per round so an edit\r\n    /// takes effect within one tick, no restart).\r\n    fn load(paths: &PumpPaths) -> RoundCtx {\r\n        let subnets = SubnetStore::load_from(&paths.subnets);\r\n        // Serve-state (M8 decision 6, REQ-SUBNET-5): a detached subnet is\r\n        // neither advertised into nor connected to — skipped wholesale in the\r\n        // per-subnet loop below.\r\n        // [impl->REQ-SUBNET-5]\r\n        let attachment = spt_store::attachment::AttachmentStore::load_from(\r\n            &paths\r\n                .subnets\r\n                .parent()\r\n                .map(|p| p.join(\"attachment.json\"))\r\n                .unwrap_or_else(|| PathBuf::from(\"attachment.json\")),\r\n        );\r\n        // THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster\r\n        // (all members), not the directly-paired set. The one roster loaded\r\n        // here feeds both the push (who we dial) and the sync gate (who we\r\n        // authorize), so they read one store.\r\n        let roster_path = paths\r\n            .subnets\r\n            .parent()\r\n            .map(|p| p.join(\"roster.json\"))\r\n            .unwrap_or_else(|| PathBuf::from(\"roster.json\"));\r\n        let policy = SyncPolicy {\r\n            subnets: subnets\r\n                .subnets\r\n                .iter()\n                .map(|s| (s.name.clone(), s.hide_new_endpoints))\n                .collect(),\n…\n        };\n…\n    }\n}","startLine":209,"lineNumbers":[209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,null,449,null,455,456]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\mod.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1982,"totalBytes":10458,"outputLines":235,"outputBytes":10458,"shownRange":{"start":209,"end":443},"nextOffset":444}}},"isError":false,"timestamp":1784271526657}}
{"type":"message","id":"bf0bac59","parentId":"21ef53ed","timestamp":"2026-07-17T06:58:57.192Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning livehost adapter reads inspection**"},{"type":"toolCall","id":"call_5iLQ24hwhIzMY3zjozFPit2K|fc_038f28d3e93bbfd4016a59d2b11880819684640fbf2d324f61","name":"read","arguments":{"i":"Reading live host scan details","path":"crates/spt-daemon/src/livehost.rs","selector":"120-250,1000-1058"}},{"type":"toolCall","id":"call_ZBOnlNGNUFzIbZ9qdI0qEln6|fc_038f28d3e93bbfd4016a59d2b1188c8196b455a0eb1e54cfe9","name":"read","arguments":{"i":"Reading brain heartbeat constants","path":"crates/spt-daemon/src/brainproc.rs","selector":"35-75"}},{"type":"toolCall","id":"call_AyPgghNIh3BBFyNmQj7WFLbJ|fc_038f28d3e93bbfd4016a59d2b118948196a483d8f408f2ddd9","name":"read","arguments":{"i":"Reading dispatch loop tail","path":"crates/spt-daemon/src/dispatch.rs","selector":"430-590"}},{"type":"toolCall","id":"call_70XscBTTorpaSL4c4COAejZH|fc_038f28d3e93bbfd4016a59d2b1189c8196a49aeb011fdc422a","name":"read","arguments":{"i":"Reading nethost stream subscribers","path":"crates/spt-daemon/src/nethost.rs","selector":"140-250,770-830,1000-1090,1270-1300"}},{"type":"toolCall","id":"call_4G5mRdLYeSXkHrmpXh4Dk3RK|fc_038f28d3e93bbfd4016a59d2b118a081968bb4cb3d07e0ae9d","name":"grep","arguments":{"i":"Finding transcript readers in daemon","pattern":"fetch_history|history\\(|project_endpoint_digest|digest|sessions::read_all|history.log|transcript","path":"crates/spt-daemon/src","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Gq9q5MBX8BC4Q2bnAbvrELhB|fc_038f28d3e93bbfd4016a59d2b118ac8196a4a3968ced6e071d","name":"grep","arguments":{"i":"Finding subsecond disk reads","pattern":"from_millis\\((25|40|50|100|200|250|500)\\)|POLL_MS|TICK","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":12513,"output":386,"cacheRead":148992,"cacheWrite":0,"totalTokens":161891,"reasoningTokens":52,"cost":{"input":0.06256500000000001,"output":0.01158,"cacheRead":0.07449599999999999,"cacheWrite":0,"total":0.14864100000000002}},"stopReason":"toolUse","timestamp":1784271526699,"responseId":"resp_038f28d3e93bbfd4016a59d2a6f38c8196b0e5915fe2d6251b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2aa14c08196992049ba31a276b3","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdKxT6HPsx_j5w0wj45qSJrJa2fb-nHpkAEKeaPLroYo5kNg2pthTaDL8-ypYVbMiXdE1PGSVJvf6l73IX1w1Bxb6TFQD5VoNqqbeH4ydFmNoE08gnpv5H7JoR4yZAQoLrekR9wJG527jU4lfiFwD2ERq9KU8PImCVyfjwA5x8909GQkjDqEAeA0u8s79AF5biYm6OJxayL9XBDJkYs02DEx3GQ0oG6d_Wu2xldd9GOJVyG681hxSc_K9jlSmjH2DckjXJAMy5dqV8UtN_9JZ7UeytxGalC0y6Kv_8NseuumpNn_xAbKjRII6-aHiAAYknRhIqn3XC0aMDw5jAXEDzsf9tj5cu9ay_7d0Ipmhl40i4g9C_qk1dRDmdlMnVjtlYygiZm4iZytYLWOkuTIXoxz1EM5WIQRtNynJBknNp1SI8i4xouKF5aRySpv23UqXsFEKagLxn7-yiirWvwQAAIWUv6XUBBdZQ2ITWnIfjGS2PTCbOea0kHesgA70KzGA-YwUvvCi1tKyrlE0-QgNgHCEREHOjxUzXaTd2H9x9E6u7IwwQsCMntVTdvuxOnuniu_ueueYEISs_ffJLe0f9PomScUff1QQUTLUMmra3QMUAhDtiF3_N6ybHpD4VgkMUzKt4MSDQO2xXdVJQSYpYXBySGCAVibKBEmPvDSy6XRy4siEvzC23fKuKl3e0oYopazRXuwnunLAHAr5y5J5g0e1BG1txMnemIfepdbxVkLvZ8BT0kWyvlrJc1WqAAGUmUGwqitMBKpd3rr8SWsK1tMQVNlpQ8owa1Ba3OHXbhi7jndQLRSfnsBnJnmBYw9uAuKGOFjIxIrHiN9-xCvZ0q7oUdS5zru7Zksq_h8sRfeKN63uq5iJmCY_aU6FYe4kOmj6UqYRiczQ6x8ymxvtVi7AUHWZOOpzv4Jb1PPN_dKhKbM6l4dNryDv22si-l3mtDL8Md3AggZXltFgxRZ288_nT87GK8Ht9_uA0HN2u3R4S2eoqtI8lGBXVM1aJOU0QizV52mqLqmBUgpg97F5vICxP1Vi_KfjfcD7W1OYsNwisb5WYHMG-ziMCY0MlvhP2nMOtr92YGOsFlEJtkIWaiVwY7XW3dIe03MZ74P7fzJLCMO_UXTQlpG9ivItucwoaTKmQZhT0kaFal5hQbFtdqHayHWhJdXmq2rsSviJA3srIY=","summary":[{"type":"summary_text","text":"**Planning livehost adapter reads inspection**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2b11880819684640fbf2d324f61","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading live host scan details\",\"path\":\"crates/spt-daemon/src/livehost.rs\",\"selector\":\"120-250,1000-1058\"}","call_id":"call_5iLQ24hwhIzMY3zjozFPit2K","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2b1188c8196b455a0eb1e54cfe9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain heartbeat constants\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"35-75\"}","call_id":"call_ZBOnlNGNUFzIbZ9qdI0qEln6","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2b118948196a483d8f408f2ddd9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatch loop tail\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"430-590\"}","call_id":"call_AyPgghNIh3BBFyNmQj7WFLbJ","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2b1189c8196a49aeb011fdc422a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading nethost stream subscribers\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"140-250,770-830,1000-1090,1270-1300\"}","call_id":"call_70XscBTTorpaSL4c4COAejZH","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2b118a081968bb4cb3d07e0ae9d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding transcript readers in daemon\",\"pattern\":\"fetch_history|history\\\\(|project_endpoint_digest|digest|sessions::read_all|history.log|transcript\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_4G5mRdLYeSXkHrmpXh4Dk3RK","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2b118ac8196a4a3968ced6e071d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding subsecond disk reads\",\"pattern\":\"from_millis\\\\((25|40|50|100|200|250|500)\\\\)|POLL_MS|TICK\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Gq9q5MBX8BC4Q2bnAbvrELhB","name":"grep"}]},"duration":10492.118900001049,"ttft":3398.5050999969244,"contextSnapshot":{"promptTokens":161505,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5iLQ24hwhIzMY3zjozFPit2K|fc_038f28d3e93bbfd4016a59d2b11880819684640fbf2d324f61","toolName":"read","startedAt":"2026-07-17T06:58:57.192Z","args":{"path":"crates/spt-daemon/src/livehost.rs"},"intent":"Reading live host scan details"},"id":"34e67f7a","parentId":"bf0bac59","timestamp":"2026-07-17T06:58:57.192Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZBOnlNGNUFzIbZ9qdI0qEln6|fc_038f28d3e93bbfd4016a59d2b1188c8196b455a0eb1e54cfe9","toolName":"read","startedAt":"2026-07-17T06:58:57.192Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain heartbeat constants"},"id":"f570cad5","parentId":"34e67f7a","timestamp":"2026-07-17T06:58:57.192Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_AyPgghNIh3BBFyNmQj7WFLbJ|fc_038f28d3e93bbfd4016a59d2b118948196a483d8f408f2ddd9","toolName":"read","startedAt":"2026-07-17T06:58:57.193Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatch loop tail"},"id":"0fb3fe9b","parentId":"f570cad5","timestamp":"2026-07-17T06:58:57.193Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_70XscBTTorpaSL4c4COAejZH|fc_038f28d3e93bbfd4016a59d2b1189c8196a49aeb011fdc422a","toolName":"read","startedAt":"2026-07-17T06:58:57.193Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading nethost stream subscribers"},"id":"11e72443","parentId":"0fb3fe9b","timestamp":"2026-07-17T06:58:57.193Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4G5mRdLYeSXkHrmpXh4Dk3RK|fc_038f28d3e93bbfd4016a59d2b118a081968bb4cb3d07e0ae9d","toolName":"grep","startedAt":"2026-07-17T06:58:57.193Z","args":{"path":"crates/spt-daemon/src"},"intent":"Finding transcript readers in daemon"},"id":"4146a642","parentId":"11e72443","timestamp":"2026-07-17T06:58:57.193Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Gq9q5MBX8BC4Q2bnAbvrELhB|fc_038f28d3e93bbfd4016a59d2b118ac8196a4a3968ced6e071d","toolName":"grep","startedAt":"2026-07-17T06:58:57.193Z","args":{"path":"crates/spt-daemon/src"},"intent":"Finding subsecond disk reads"},"id":"d5464dfa","parentId":"4146a642","timestamp":"2026-07-17T06:58:57.193Z"}
{"type":"message","id":"1fcf8d96","parentId":"d5464dfa","timestamp":"2026-07-17T06:58:57.224Z","message":{"role":"toolResult","toolCallId":"call_5iLQ24hwhIzMY3zjozFPit2K|fc_038f28d3e93bbfd4016a59d2b11880819684640fbf2d324f61","toolName":"read","content":[{"type":"text","text":"impl LiveSet {\n…\n    fn stop_host(&self, id: &str) {\n        let handle = {\n…\n            map.remove(id)\n        };\n        if let Some(h) = handle {\n            h.stop.store(true, Ordering::SeqCst);\n            let _ = h.thread.join();\n            // F-030 W3: nothing detached to reap — the per-event psyche driver spawns\n            // no resident child, so joining the driver thread + de-tabling above is the\n            // whole un-host (the resident-psyche handle-reap + orphan-reap retired with\n            // the residency machinery). Un-hosting a genuinely-offline endpoint is legit.\n        }\n    }\n}\n\n/// One reconcile sweep — the brain's live host-set invariant holder, run at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the\n/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):\n///\n/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a\n///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).\n/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.\n/// - **leave-alone** (online ∧ tabled).\n///\n/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are\n/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.\n// [impl->REQ-DAEMON-1]\npub fn reconcile_once(\n    owlery: &Path,\n    registered: &[(AdapterRecord, Manifest)],\n    adapters_dir: &Path,\n    set: &Arc<LiveSet>,\n    cfg: &DaemonConfig,\n    reason: StartReason,\n) {\n    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n    // on a TRANSIENT read failure (M11-W0.4 (ii)): a momentarily-unreadable\n    // info.json must not stop+respawn the driver (Psyche churn on a disk blip). The\n    // discriminator is the perch DIR:\n    //   - dir gone        → torn down (teardown removed the perch) → un-host;\n    //   - dir present, read_info Some(status≠online) → signed-off/offline → un-host;\n    //   - dir present, read_info None (transient/torn read) → LEAVE the driver, retry.\n    // [impl->REQ-DAEMON-1]\n    for id in set.ids() {\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let unhost = if !perch.exists() {\n            true // the perch dir is gone — torn down\n        } else {\n            match spt_store::info::read_info(&perch) {\n                Some(info) => info.status.as_deref() != Some(STATUS_ONLINE),\n                None => false, // dir present but unreadable: transient → keep hosting\n            }\n        };\n        if unhost {\n            set.stop_host(&id);\n            continue;\n        }\n        // F-030 W3: the residency-confirm + thrash-detect + orphan-reap arms retired\n        // with the resident psyche (a psyche is now a bounded per-event turn, not a\n        // process to confirm resident / rate-limit / reap). A psyche that cannot work\n        // is surfaced by the W1 per-event turn-failure budget (`note_turn_outcome`\n        // stamps `psyche_host_error` on the PARENT — psyche fields only, NEVER the\n        // parent's ready/hosted state), never by un-hosting this online endpoint. This\n        // clean insertion seam is where a future wake-resume leg slots in\n        // (REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION / REQ-HAZARD-THRASH-GUARD-BLIND).\n        // [impl->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]\n    }\n\n    // Start side: every online live endpoint not yet hosted.\n    for id in perch::list_self_perch_ids(owlery) {\n        if set.contains(&id) {\n            continue; // already hosted (leave-alone)\n        }\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let Some(info) = spt_store::info::read_info(&perch) else {\n            continue;\n        };\n        if info.state != LIVE_AGENT_STATE {\n            continue; // not a live agent (ready_agent etc.)\n        }\n        // ONLINE → host the pulse driver (existing path). NOT online but rest\n        // intent Active → the A-2 wake-resume leg: resume the harness from the last\n        // ledger session so a woken-but-unbound endpoint actually comes up.\n        if info.status.as_deref() != Some(STATUS_ONLINE) {\n            resume_woken_endpoint(&perch, &id, &info, registered, adapters_dir);\n            continue;\n        }\n        let Some(adapter) = info.adapter.as_deref() else {\n            continue; // adapterless endpoint — never live-capable\n        };\n        let manifest = match spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)\n        {\n            Ok(m) => m,\n            Err(_) => {\n                // FOLD-IN (A-2, REQ-WAKE-RESUME-LEG): a deregistered adapter on an\n                // online endpoint was silently skipped — the SAME silent host-failure\n                // class the resume leg fixes. Stamp the loud host_error REPORT (never\n                // status — the field is a report, not a liveness input).\n                // [impl->REQ-WAKE-RESUME-LEG]\n                let _ = spt_store::info::set_host_error(\n                    &perch,\n                    Some(&format!(\n                        \"adapter '{adapter}' is not a registered/active adapter on this \\\n                         node — register it (spt adapter add)\"\n                    )),\n                );\n                continue;\n            }\n        };\n        // [impl->REQ-INSTALL-11] resolve the Psyche role program against the\n        // adapter install dir — the registry record's precise `source_dir`, the\n        // same dir Feature E's api seam uses (mod.rs::resolve_ctx_manifest). The\n        // daemon-host site is the heir to the retired in-process spawn's\n        // install-dir resolution: without it the daemon-hosted Psyche resolves a\n        // copy-mode psyche binary only via PATH, not its install dir.\n        let parent = adapter.split(':').next().unwrap_or(adapter);\n        let install_dir = registered\n            .iter()\n            .find(|(rec, _)| rec.name == parent)\n            .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));\n        let Some(lifecycle) =\n            BrainLifecycle::with_config_in(&manifest, &id, cfg.clone(), install_dir.as_deref())\n        else {\n            continue; // manifest declares no [session.psyche_init]\n        };\n        // Successful host → clear any stale host_error (a recovered endpoint shows\n        // clean; doyle A-2 bind: cleared on a successful host/bind).\n        let _ = spt_store::info::set_host_error(&perch, None);\n        host_one(set, &id, lifecycle, info.session_id, reason);\n    }\n\n    // Worker reap GC (W-3, REQ-WORKER-REAP) — the PRIMARY collector of the leaked\n…\n}\n…\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n…\n        {\n…\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            let cfg = DaemonConfig::load();\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\n            // latch for any spt-hosted endpoint whose broker session is gone — and\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\n            // so the same-tick stop-side un-hosts the now-offline perch and the\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\n            // cold start after an unclean stop from spawning N phantom Psyches for N\n            // dead-but-latched perches. Skipped only when the broker is unreachable\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\n            // run_brain connects to the broker BEFORE this host spawns, so the query\n            // returns the REAL session set: empty at a cold start (every online\n            // controllable perch is a phantom → offlined), the surviving sessions at\n            // a brain restart (live agents stay, only the dead are offlined). The\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\n            if let Some(live) = query_live_session_endpoints() {\n                reconcile_hosted_liveness(&owlery, &live);\n            }\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n        }\n    })\n}\n\n#[cfg(test)]"}],"details":{"displayContent":{"text":"impl LiveSet {\n…\n    fn stop_host(&self, id: &str) {\n        let handle = {\n…\n            map.remove(id)\n        };\n        if let Some(h) = handle {\n            h.stop.store(true, Ordering::SeqCst);\n            let _ = h.thread.join();\n            // F-030 W3: nothing detached to reap — the per-event psyche driver spawns\n            // no resident child, so joining the driver thread + de-tabling above is the\n            // whole un-host (the resident-psyche handle-reap + orphan-reap retired with\n            // the residency machinery). Un-hosting a genuinely-offline endpoint is legit.\n        }\n    }\n}\n\n/// One reconcile sweep — the brain's live host-set invariant holder, run at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the\n/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):\n///\n/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a\n///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).\n/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.\n/// - **leave-alone** (online ∧ tabled).\n///\n/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are\n/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.\n// [impl->REQ-DAEMON-1]\npub fn reconcile_once(\n    owlery: &Path,\n    registered: &[(AdapterRecord, Manifest)],\n    adapters_dir: &Path,\n    set: &Arc<LiveSet>,\n    cfg: &DaemonConfig,\n    reason: StartReason,\n) {\n    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —\n    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never\n    // on a TRANSIENT read failure (M11-W0.4 (ii)): a momentarily-unreadable\n    // info.json must not stop+respawn the driver (Psyche churn on a disk blip). The\n    // discriminator is the perch DIR:\n    //   - dir gone        → torn down (teardown removed the perch) → un-host;\n    //   - dir present, read_info Some(status≠online) → signed-off/offline → un-host;\n    //   - dir present, read_info None (transient/torn read) → LEAVE the driver, retry.\n    // [impl->REQ-DAEMON-1]\n    for id in set.ids() {\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let unhost = if !perch.exists() {\n            true // the perch dir is gone — torn down\n        } else {\n            match spt_store::info::read_info(&perch) {\n                Some(info) => info.status.as_deref() != Some(STATUS_ONLINE),\n                None => false, // dir present but unreadable: transient → keep hosting\n            }\n        };\n        if unhost {\n            set.stop_host(&id);\n            continue;\n        }\n        // F-030 W3: the residency-confirm + thrash-detect + orphan-reap arms retired\n        // with the resident psyche (a psyche is now a bounded per-event turn, not a\n        // process to confirm resident / rate-limit / reap). A psyche that cannot work\n        // is surfaced by the W1 per-event turn-failure budget (`note_turn_outcome`\n        // stamps `psyche_host_error` on the PARENT — psyche fields only, NEVER the\n        // parent's ready/hosted state), never by un-hosting this online endpoint. This\n        // clean insertion seam is where a future wake-resume leg slots in\n        // (REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION / REQ-HAZARD-THRASH-GUARD-BLIND).\n        // [impl->REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION]\n    }\n\n    // Start side: every online live endpoint not yet hosted.\n    for id in perch::list_self_perch_ids(owlery) {\n        if set.contains(&id) {\n            continue; // already hosted (leave-alone)\n        }\n        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n        let Some(info) = spt_store::info::read_info(&perch) else {\n            continue;\n        };\n        if info.state != LIVE_AGENT_STATE {\n            continue; // not a live agent (ready_agent etc.)\n        }\n        // ONLINE → host the pulse driver (existing path). NOT online but rest\n        // intent Active → the A-2 wake-resume leg: resume the harness from the last\n        // ledger session so a woken-but-unbound endpoint actually comes up.\n        if info.status.as_deref() != Some(STATUS_ONLINE) {\n            resume_woken_endpoint(&perch, &id, &info, registered, adapters_dir);\n            continue;\n        }\n        let Some(adapter) = info.adapter.as_deref() else {\n            continue; // adapterless endpoint — never live-capable\n        };\n        let manifest = match spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)\n        {\n            Ok(m) => m,\n            Err(_) => {\n                // FOLD-IN (A-2, REQ-WAKE-RESUME-LEG): a deregistered adapter on an\n                // online endpoint was silently skipped — the SAME silent host-failure\n                // class the resume leg fixes. Stamp the loud host_error REPORT (never\n                // status — the field is a report, not a liveness input).\n                // [impl->REQ-WAKE-RESUME-LEG]\n                let _ = spt_store::info::set_host_error(\n                    &perch,\n                    Some(&format!(\n                        \"adapter '{adapter}' is not a registered/active adapter on this \\\n                         node — register it (spt adapter add)\"\n                    )),\n                );\n                continue;\n            }\n        };\n        // [impl->REQ-INSTALL-11] resolve the Psyche role program against the\n        // adapter install dir — the registry record's precise `source_dir`, the\n        // same dir Feature E's api seam uses (mod.rs::resolve_ctx_manifest). The\n        // daemon-host site is the heir to the retired in-process spawn's\n        // install-dir resolution: without it the daemon-hosted Psyche resolves a\n        // copy-mode psyche binary only via PATH, not its install dir.\n        let parent = adapter.split(':').next().unwrap_or(adapter);\n        let install_dir = registered\n            .iter()\n            .find(|(rec, _)| rec.name == parent)\n            .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));\n        let Some(lifecycle) =\n            BrainLifecycle::with_config_in(&manifest, &id, cfg.clone(), install_dir.as_deref())\n        else {\n            continue; // manifest declares no [session.psyche_init]\n        };\n        // Successful host → clear any stale host_error (a recovered endpoint shows\n        // clean; doyle A-2 bind: cleared on a successful host/bind).\n        let _ = spt_store::info::set_host_error(&perch, None);\n        host_one(set, &id, lifecycle, info.session_id, reason);\n    }\n\n    // Worker reap GC (W-3, REQ-WORKER-REAP) — the PRIMARY collector of the leaked\n…\n}\n…\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n…\n        {\n…\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            let cfg = DaemonConfig::load();\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\n            // latch for any spt-hosted endpoint whose broker session is gone — and\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\n            // so the same-tick stop-side un-hosts the now-offline perch and the\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\n            // cold start after an unclean stop from spawning N phantom Psyches for N\n            // dead-but-latched perches. Skipped only when the broker is unreachable\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\n            // run_brain connects to the broker BEFORE this host spawns, so the query\n            // returns the REAL session set: empty at a cold start (every online\n            // controllable perch is a phantom → offlined), the surviving sessions at\n            // a brain restart (live agents stay, only the dead are offlined). The\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\n            if let Some(live) = query_live_session_endpoints() {\n                reconcile_hosted_liveness(&owlery, &live);\n            }\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n        }\n    })\n}\n\n#[cfg(test)]","startLine":70,"lineNumbers":[70,null,117,118,null,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,null,269,null,978,979,null,981,null,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\livehost.rs"}}},"isError":false,"timestamp":1784271537224}}
{"type":"message","id":"ec075268","parentId":"1fcf8d96","timestamp":"2026-07-17T06:58:57.224Z","message":{"role":"toolResult","toolCallId":"call_70XscBTTorpaSL4c4COAejZH|fc_038f28d3e93bbfd4016a59d2b1189c8196a49aeb011fdc422a","toolName":"read","content":[{"type":"text","text":"enum SeatFail {\n…\n}\n\n/// One installed subscriber behind a bounded writer queue (ADR-0038 Amendment\n/// fixes 2+3, REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE): frames ENQUEUE under the\n/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\n/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\n/// OUTSIDE every lock. The field stall was exactly the old shape's serial\n/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\n///\n/// Writer discipline: the thread HALTS at the FIRST failed write (no\n/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\n/// sees it and the caller removes the seat. Dropping the seat drops the queue\n/// sender — the writer drains what it holds and exits.\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\nstruct SubscriberSeat {\n    sub: SharedSend,\n    tx: std::sync::mpsc::SyncSender<Envelope>,\n    /// Writer thread exited (failure or clean drain) — the seat is fully gone\n    /// and a replacement may install (the no-reinstall-until-gone rule,\n    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\n    done: Arc<std::sync::atomic::AtomicBool>,\n    /// The writer died on a WRITE FAILURE (poison), not a clean close.\n    poisoned: Arc<std::sync::atomic::AtomicBool>,\n}\n\nimpl SubscriberSeat {\n    /// Install a seat: spawn its writer thread and hand back the handle.\n    /// `replay` is the attach snapshot, moved in UNDER the log mutex (a\n    /// memory move, never conn I/O): the writer drains it COMPLETELY before\n    /// touching the live queue, so replay-then-live seq order holds by\n    /// construction even while producers enqueue concurrently. (Gate follow-\n    /// up: the first fix-3 shape enqueued the replay from the caller OUTSIDE\n    /// the mutex — a concurrent append could slot a live frame into the\n    /// middle of the replay and break the brain's strict gap check: the xfer\n    /// re-serve \"got seq 134 want 6\" CI red.)\n    /// `lease` (StreamLog seats; `None` for presence) is the stream's serve\n    /// lease AT INSTALL: the writer cancels it the moment a write fails, so\n    /// the paired serve/wire worker's next forward refuses PROMPTLY — even on\n    /// a traffic-quiet stream where no producer enqueue would ever run\n    /// `drop_seat` (a mid-replay poison with no follow-on appends previously\n    /// left the lease alive until the next append; the T4 forwarder starved).\n    /// A LATE cancel from a superseded writer — poisoned OR healthily\n    /// displaced — is harmless by construction: every install mints a fresh\n    /// lease Arc, so an old writer cancels only its own generation's Arc.\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn install(\n        sub: SharedSend,\n        replay: Vec<Envelope>,\n        lease: Option<Arc<StreamLease>>,\n    ) -> SubscriberSeat {\n        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\n        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        let writer_sub = Arc::clone(&sub);\n        let writer_done = Arc::clone(&done);\n        let writer_poisoned = Arc::clone(&poisoned);\n        std::thread::spawn(move || {\n            let poison = |halt_reason: &str| {\n                writer_poisoned.store(true, Ordering::Release);\n                if let Some(l) = &lease {\n                    l.cancel();\n                }\n                writer_sub.lifecycle_event(\"stream-sub-writer-poison\", halt_reason);\n            };\n            // Replay first — halt at the first failed write (Amendment\n            // fix 2), never iterate past a poisoned conn.\n            for frame in replay {\n                if writer_sub.write(&frame).is_err() {\n                    poison(\"replay write failed\");\n                    writer_done.store(true, Ordering::Release);\n                    return;\n                }\n            }\n            // Then live frames, same halt discipline.\n            while let Ok(frame) = rx.recv() {\n                if writer_sub.write(&frame).is_err() {\n                    poison(\"live write failed\");\n                    break;\n                }\n            }\n            writer_done.store(true, Ordering::Release);\n        });\n        SubscriberSeat {\n            sub,\n            tx,\n            done,\n            poisoned,\n        }\n    }\n\n    /// Non-blocking enqueue (the producer sites: append, finish, presence\n    /// push). `Err` means the seat must be removed.\n    fn enqueue(&self, frame: Envelope) -> Result<(), SeatFail> {\n        if self.poisoned.load(Ordering::Acquire) {\n            return Err(SeatFail::Poisoned);\n        }\n        match self.tx.try_send(frame) {\n            Ok(()) => Ok(()),\n            Err(std::sync::mpsc::TrySendError::Full(_)) => Err(SeatFail::Overflow),\n            Err(std::sync::mpsc::TrySendError::Disconnected(_)) => Err(SeatFail::Poisoned),\n        }\n    }\n\n    /// Whether this seat belongs to `sub` (detach correlation).\n    fn is(&self, sub: &SharedSend) -> bool {\n        Arc::ptr_eq(&self.sub, sub)\n    }\n\n    /// Whether the writer thread has fully exited — the reinstall gate.\n    fn gone(&self) -> bool {\n        self.done.load(Ordering::Acquire)\n    }\n}\n…\nenum RecvHalf {\n    Quic(RecvStream),\n    Loopback(ReadHalf<DuplexStream>),\n}\n\n/// One live stream the broker holds (QUIC or loopback): the send half (brain\n/// writes ride it over IPC) + the sequenced read log.\nstruct StreamEntry {\n    conn_id: u64,\n    remote_id_hex: String,\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    /// peer-initiated streams; without this fact the table is ambiguous —\n    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n    /// that served its own requester stream would deadlock on itself).\n    initiated_locally: bool,\n    /// The send half, serialized behind an async mutex (writes happen on the\n    /// host's runtime via `block_on`).\n    send: tokio::sync::Mutex<SendHalf>,\n    log: Arc<Mutex<StreamLog>>,\n    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n    /// Unused by ordinary (non-retentive) streams.\n    room: Arc<tokio::sync::Notify>,\n    /// Retired = invisible to dispatch eligibility (ADR-0038 A), NOT torn down:\n    /// retire must never sever the transport — the worker's reply send is\n    /// fire-and-forget, so a retire that dropped this entry could overtake the\n    /// still-queued reply inside the broker and reset the never-finished QUIC\n    /// send half (requester sees torn-EOF, no reply). The reply-flush ordering\n    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    retired: std::sync::atomic::AtomicBool,\n}\n\n/// Tables shared between the host's sync surface and its runtime tasks\n/// (accept loop, per-conn stream acceptors, per-stream read pumps).\nstruct NetShared {\n    next_conn_id: AtomicU64,\n    conns: Mutex<HashMap<u64, ConnEntry>>,\n    next_stream_id: AtomicU64,\n    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    ring_cap: usize,\n    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n    presence: Mutex<PresenceLog>,\n}\n\nimpl NetShared {\n    fn new() -> Self {\n        NetShared {\n            next_conn_id: AtomicU64::new(1),\n            conns: Mutex::new(HashMap::new()),\n            next_stream_id: AtomicU64::new(1),\n            streams: Mutex::new(HashMap::new()),\n            ring_cap: ring_cap_from_env(),\n            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n        }\n    }\n}\n\n/// The stream ring depth — [`DEFAULT_STREAM_RING_CHUNKS`] in production.\n/// `SPT_STREAM_RING_CHUNKS` is a TEST-RIG-ONLY valve (the D2 regression rolls\n/// an opener out of a small ring deterministically instead of pushing >4096\n…\nfn register_stream(\n    shared: &Arc<NetShared>,\n    conn_id: u64,\n    remote_id_hex: &str,\n    send: SendHalf,\n    recv: RecvHalf,\n    initiated_locally: bool,\n) -> u64 {\n    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n    let room = Arc::new(tokio::sync::Notify::new());\n    shared.streams.lock().unwrap().insert(\n        id,\n        Arc::new(StreamEntry {\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            initiated_locally,\n            send: tokio::sync::Mutex::new(send),\n            log: Arc::clone(&log),\n            room: Arc::clone(&room),\n            retired: std::sync::atomic::AtomicBool::new(false),\n        }),\n    );\n\n    // The read pump — the stream's single producer. A clean end (peer finish)\n    // and a torn end (conn lost) both finish the log; the buffered chunks stay\n    // replayable either way. The transport differs only at this read leaf\n    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the\n    // same `StreamLog`, so nothing above the pump can tell them apart.\n    //\n    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n    // so before pulling the next chunk the pump WAITS while the undrained ring is\n    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n    tokio::spawn(async move {\n        match recv {\n            RecvHalf::Quic(mut recv) => loop {\n                await_ring_room(&log, &room).await;\n                match recv.read_chunk(STREAM_READ_CHUNK).await {\n                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n                    Ok(None) | Err(_) => {\n                        log.lock().unwrap().finish();\n                        break;\n                    }\n                }\n            },\n            RecvHalf::Loopback(mut recv) => {\n                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n                loop {\n                    await_ring_room(&log, &room).await;\n                    match recv.read(&mut buf).await {\n                        Ok(0) | Err(_) => {\n                            log.lock().unwrap().finish();\n                            break;\n                        }\n                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n                    }\n                }\n            }\n        }\n    });\n\n    id\n}\n\n/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.\n// [impl->REQ-SHELL-4]\nasync fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n    loop {\n        {\n            let l = log.lock().unwrap();\n            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n                return;\n            }\n        }\n        room.notified().await;\n    }\n}\n\n/// The broker-owned net endpoint + live connection/stream tables (see module\n/// docs).\npub struct NetHost {\n    /// The dedicated tokio runtime hosting the endpoint and its accept loop.\n    /// Dropping the host drops the runtime, stopping every net task with it.\n    runtime: tokio::runtime::Runtime,\n    endpoint: Arc<NetEndpoint>,\n    shared: Arc<NetShared>,\n…\n}\n…\n        crate::pairhost::spawn_meet_rotation(\n            runtime.handle(),\n            Arc::clone(&endpoint),\n            cfg.relay,\n            cfg.discovery,\n            cfg.scope,\n        );\n\n        // The limiter Arc lives on in the accept loop; the host keeps no\n        // handle (per-ceremony state is the responder's, not the table's).\n        drop(pair_rate);\n\n        // DIAGNOSTIC (UPDATE-WEDGE round-3): the reactor-liveness canary. Stamps\n        // epoch-ms every ~25ms FROM the 2-worker runtime, so a starved runtime\n        // (workers monopolized by non-yielding dead-peer connects) shows as a\n        // stale stamp. Throwaway probe — no REQ, gated by nothing (cheap store).\n        let net_canary_tick_ms = Arc::new(AtomicU64::new(epoch_ms()));\n        let canary = Arc::clone(&net_canary_tick_ms);\n        runtime.spawn(async move {\n            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n            loop {\n                ivl.tick().await;\n                canary.store(epoch_ms(), Ordering::Relaxed);\n            }\n        });\n\n        Ok(NetHost {\n            runtime,\n            endpoint,\n            shared,\n            dial_ops: Mutex::new(HashMap::new()),\n…\n        })"}],"details":{"displayContent":{"text":"enum SeatFail {\n…\n}\n\n/// One installed subscriber behind a bounded writer queue (ADR-0038 Amendment\n/// fixes 2+3, REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE): frames ENQUEUE under the\n/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\n/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\n/// OUTSIDE every lock. The field stall was exactly the old shape's serial\n/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\n///\n/// Writer discipline: the thread HALTS at the FIRST failed write (no\n/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\n/// sees it and the caller removes the seat. Dropping the seat drops the queue\n/// sender — the writer drains what it holds and exits.\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\nstruct SubscriberSeat {\n    sub: SharedSend,\n    tx: std::sync::mpsc::SyncSender<Envelope>,\n    /// Writer thread exited (failure or clean drain) — the seat is fully gone\n    /// and a replacement may install (the no-reinstall-until-gone rule,\n    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\n    done: Arc<std::sync::atomic::AtomicBool>,\n    /// The writer died on a WRITE FAILURE (poison), not a clean close.\n    poisoned: Arc<std::sync::atomic::AtomicBool>,\n}\n\nimpl SubscriberSeat {\n    /// Install a seat: spawn its writer thread and hand back the handle.\n    /// `replay` is the attach snapshot, moved in UNDER the log mutex (a\n    /// memory move, never conn I/O): the writer drains it COMPLETELY before\n    /// touching the live queue, so replay-then-live seq order holds by\n    /// construction even while producers enqueue concurrently. (Gate follow-\n    /// up: the first fix-3 shape enqueued the replay from the caller OUTSIDE\n    /// the mutex — a concurrent append could slot a live frame into the\n    /// middle of the replay and break the brain's strict gap check: the xfer\n    /// re-serve \"got seq 134 want 6\" CI red.)\n    /// `lease` (StreamLog seats; `None` for presence) is the stream's serve\n    /// lease AT INSTALL: the writer cancels it the moment a write fails, so\n    /// the paired serve/wire worker's next forward refuses PROMPTLY — even on\n    /// a traffic-quiet stream where no producer enqueue would ever run\n    /// `drop_seat` (a mid-replay poison with no follow-on appends previously\n    /// left the lease alive until the next append; the T4 forwarder starved).\n    /// A LATE cancel from a superseded writer — poisoned OR healthily\n    /// displaced — is harmless by construction: every install mints a fresh\n    /// lease Arc, so an old writer cancels only its own generation's Arc.\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn install(\n        sub: SharedSend,\n        replay: Vec<Envelope>,\n        lease: Option<Arc<StreamLease>>,\n    ) -> SubscriberSeat {\n        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\n        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        let writer_sub = Arc::clone(&sub);\n        let writer_done = Arc::clone(&done);\n        let writer_poisoned = Arc::clone(&poisoned);\n        std::thread::spawn(move || {\n            let poison = |halt_reason: &str| {\n                writer_poisoned.store(true, Ordering::Release);\n                if let Some(l) = &lease {\n                    l.cancel();\n                }\n                writer_sub.lifecycle_event(\"stream-sub-writer-poison\", halt_reason);\n            };\n            // Replay first — halt at the first failed write (Amendment\n            // fix 2), never iterate past a poisoned conn.\n            for frame in replay {\n                if writer_sub.write(&frame).is_err() {\n                    poison(\"replay write failed\");\n                    writer_done.store(true, Ordering::Release);\n                    return;\n                }\n            }\n            // Then live frames, same halt discipline.\n            while let Ok(frame) = rx.recv() {\n                if writer_sub.write(&frame).is_err() {\n                    poison(\"live write failed\");\n                    break;\n                }\n            }\n            writer_done.store(true, Ordering::Release);\n        });\n        SubscriberSeat {\n            sub,\n            tx,\n            done,\n            poisoned,\n        }\n    }\n\n    /// Non-blocking enqueue (the producer sites: append, finish, presence\n    /// push). `Err` means the seat must be removed.\n    fn enqueue(&self, frame: Envelope) -> Result<(), SeatFail> {\n        if self.poisoned.load(Ordering::Acquire) {\n            return Err(SeatFail::Poisoned);\n        }\n        match self.tx.try_send(frame) {\n            Ok(()) => Ok(()),\n            Err(std::sync::mpsc::TrySendError::Full(_)) => Err(SeatFail::Overflow),\n            Err(std::sync::mpsc::TrySendError::Disconnected(_)) => Err(SeatFail::Poisoned),\n        }\n    }\n\n    /// Whether this seat belongs to `sub` (detach correlation).\n    fn is(&self, sub: &SharedSend) -> bool {\n        Arc::ptr_eq(&self.sub, sub)\n    }\n\n    /// Whether the writer thread has fully exited — the reinstall gate.\n    fn gone(&self) -> bool {\n        self.done.load(Ordering::Acquire)\n    }\n}\n…\nenum RecvHalf {\n    Quic(RecvStream),\n    Loopback(ReadHalf<DuplexStream>),\n}\n\n/// One live stream the broker holds (QUIC or loopback): the send half (brain\n/// writes ride it over IPC) + the sequenced read log.\nstruct StreamEntry {\n    conn_id: u64,\n    remote_id_hex: String,\n    /// Whether THIS node opened the stream (`open_stream`) vs the peer\n    /// (accept loop). The inbound dispatcher (D9-1) claims only\n    /// peer-initiated streams; without this fact the table is ambiguous —\n    /// it holds locally-opened outbound streams too (gotcha: a dispatcher\n    /// that served its own requester stream would deadlock on itself).\n    initiated_locally: bool,\n    /// The send half, serialized behind an async mutex (writes happen on the\n    /// host's runtime via `block_on`).\n    send: tokio::sync::Mutex<SendHalf>,\n    log: Arc<Mutex<StreamLog>>,\n    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below\n    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].\n    /// Unused by ordinary (non-retentive) streams.\n    room: Arc<tokio::sync::Notify>,\n    /// Retired = invisible to dispatch eligibility (ADR-0038 A), NOT torn down:\n    /// retire must never sever the transport — the worker's reply send is\n    /// fire-and-forget, so a retire that dropped this entry could overtake the\n    /// still-queued reply inside the broker and reset the never-finished QUIC\n    /// send half (requester sees torn-EOF, no reply). The reply-flush ordering\n    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n    retired: std::sync::atomic::AtomicBool,\n}\n\n/// Tables shared between the host's sync surface and its runtime tasks\n/// (accept loop, per-conn stream acceptors, per-stream read pumps).\nstruct NetShared {\n    next_conn_id: AtomicU64,\n    conns: Mutex<HashMap<u64, ConnEntry>>,\n    next_stream_id: AtomicU64,\n    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,\n    ring_cap: usize,\n    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.\n    presence: Mutex<PresenceLog>,\n}\n\nimpl NetShared {\n    fn new() -> Self {\n        NetShared {\n            next_conn_id: AtomicU64::new(1),\n            conns: Mutex::new(HashMap::new()),\n            next_stream_id: AtomicU64::new(1),\n            streams: Mutex::new(HashMap::new()),\n            ring_cap: ring_cap_from_env(),\n            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),\n        }\n    }\n}\n\n/// The stream ring depth — [`DEFAULT_STREAM_RING_CHUNKS`] in production.\n/// `SPT_STREAM_RING_CHUNKS` is a TEST-RIG-ONLY valve (the D2 regression rolls\n/// an opener out of a small ring deterministically instead of pushing >4096\n…\nfn register_stream(\n    shared: &Arc<NetShared>,\n    conn_id: u64,\n    remote_id_hex: &str,\n    send: SendHalf,\n    recv: RecvHalf,\n    initiated_locally: bool,\n) -> u64 {\n    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);\n    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));\n    let room = Arc::new(tokio::sync::Notify::new());\n    shared.streams.lock().unwrap().insert(\n        id,\n        Arc::new(StreamEntry {\n            conn_id,\n            remote_id_hex: remote_id_hex.to_string(),\n            initiated_locally,\n            send: tokio::sync::Mutex::new(send),\n            log: Arc::clone(&log),\n            room: Arc::clone(&room),\n            retired: std::sync::atomic::AtomicBool::new(false),\n        }),\n    );\n\n    // The read pump — the stream's single producer. A clean end (peer finish)\n    // and a torn end (conn lost) both finish the log; the buffered chunks stay\n    // replayable either way. The transport differs only at this read leaf\n    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the\n    // same `StreamLog`, so nothing above the pump can tell them apart.\n    //\n    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,\n    // so before pulling the next chunk the pump WAITS while the undrained ring is\n    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so\n    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the\n    // SENDER — \"lag, never loss\" (CONTEXT:262). A `drain` wakes `room`. Ordinary\n    // streams have `retentive_cap == 0`, so this gate is a no-op for them.\n    tokio::spawn(async move {\n        match recv {\n            RecvHalf::Quic(mut recv) => loop {\n                await_ring_room(&log, &room).await;\n                match recv.read_chunk(STREAM_READ_CHUNK).await {\n                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),\n                    Ok(None) | Err(_) => {\n                        log.lock().unwrap().finish();\n                        break;\n                    }\n                }\n            },\n            RecvHalf::Loopback(mut recv) => {\n                let mut buf = vec![0u8; STREAM_READ_CHUNK];\n                loop {\n                    await_ring_room(&log, &room).await;\n                    match recv.read(&mut buf).await {\n                        Ok(0) | Err(_) => {\n                            log.lock().unwrap().finish();\n                            break;\n                        }\n                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n                    }\n                }\n            }\n        }\n    });\n\n    id\n}\n\n/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained\n/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for\n/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.\n// [impl->REQ-SHELL-4]\nasync fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {\n    loop {\n        {\n            let l = log.lock().unwrap();\n            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {\n                return;\n            }\n        }\n        room.notified().await;\n    }\n}\n\n/// The broker-owned net endpoint + live connection/stream tables (see module\n/// docs).\npub struct NetHost {\n    /// The dedicated tokio runtime hosting the endpoint and its accept loop.\n    /// Dropping the host drops the runtime, stopping every net task with it.\n    runtime: tokio::runtime::Runtime,\n    endpoint: Arc<NetEndpoint>,\n    shared: Arc<NetShared>,\n…\n}\n…\n        crate::pairhost::spawn_meet_rotation(\n            runtime.handle(),\n            Arc::clone(&endpoint),\n            cfg.relay,\n            cfg.discovery,\n            cfg.scope,\n        );\n\n        // The limiter Arc lives on in the accept loop; the host keeps no\n        // handle (per-ceremony state is the responder's, not the table's).\n        drop(pair_rate);\n\n        // DIAGNOSTIC (UPDATE-WEDGE round-3): the reactor-liveness canary. Stamps\n        // epoch-ms every ~25ms FROM the 2-worker runtime, so a starved runtime\n        // (workers monopolized by non-yielding dead-peer connects) shows as a\n        // stale stamp. Throwaway probe — no REQ, gated by nothing (cheap store).\n        let net_canary_tick_ms = Arc::new(AtomicU64::new(epoch_ms()));\n        let canary = Arc::clone(&net_canary_tick_ms);\n        runtime.spawn(async move {\n            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n            loop {\n                ivl.tick().await;\n                canary.store(epoch_ms(), Ordering::Relaxed);\n            }\n        });\n\n        Ok(NetHost {\n            runtime,\n            endpoint,\n            shared,\n            dial_ops: Mutex::new(HashMap::new()),\n…\n        })","startLine":134,"lineNumbers":[134,null,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,null,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,null,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,null,1147,null,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,null,1311]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784271537224}}
{"type":"message","id":"8b1d747e","parentId":"ec075268","timestamp":"2026-07-17T06:58:57.227Z","message":{"role":"toolResult","toolCallId":"call_Gq9q5MBX8BC4Q2bnAbvrELhB|fc_038f28d3e93bbfd4016a59d2b118ac8196a4a3968ced6e071d","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## attach.rs\n 101|/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\n*102|const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n 103|\n 104|/// The CONTROLLER fell behind the live ring further than the ring holds — the\n 105|/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n## brainproc.rs\n 56|/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n*57|const TICK: Duration = Duration::from_millis(200);\n 58|\n 59|/// How long the brain child retries connecting to the broker socket before\n 60|/// giving up (the broker may still be binding — the child is freshly spawned).\n...\n 62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n*63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n 64|\n 65|/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n 66|/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n...\n 624|\n*625|/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n 626|/// promptly mid-backoff (saturating — never an underflow panic).\n 627|fn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n 628|    let mut slept = Duration::ZERO;\n 629|    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630|        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n*631|        slept += TICK;\n 632|    }\n 633|}\n 634|\n...\n 657|/// Watch a freshly-spawned trial candidate for readiness within `window`\n*658|/// (ADR-0018 Q7, D6-2). Polls in `TICK` slices (so a daemon stop stays prompt,\n 659|/// A11/note-ii) for a `brain.ready` stamp **== this spawn's `generation`** — a\n 660|/// stale stamp from another generation never satisfies the gate (the single\n 661|/// load-bearing correctness invariant; the A9 clear-before-spawn closes the\n...\n 718|        }\n*719|        thread::sleep(TICK);\n 720|    }\n 721|}\n 722|\n...\n 898|                            }\n*899|                            Ok(None) => thread::sleep(TICK),\n 900|                            Err(e) => {\n 901|                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n 902|                                break;\n## broker.rs\n 198|/// Poll slice while waiting on a racing wake's session to appear.\n*199|const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 200|\n 201|/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n 202|/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n...\n 370|/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n*371|const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372|/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373|/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n 374|/// producing output — the POSITIVE signal that the input reader is attached.\n## config.rs\n 372|        let cfg = DaemonConfig::load_from(&path);\n*373|        assert_eq!(cfg.pulse_period, Duration::from_millis(250));\n 374|        assert_ne!(\n 375|            cfg.pulse_period, DEFAULT_PULSE_PERIOD,\n 376|            \"must override the default\"\n...\n 521|        let cfg = DaemonConfig::load_from(&path);\n*522|        assert_eq!(cfg.crash_grace, Duration::from_millis(500));\n 523|        assert_ne!(\n 524|            cfg.crash_grace, DEFAULT_CRASH_GRACE,\n 525|            \"must override the default\"\n## conn.rs\n 394|                    .dog_cv\n*395|                    .wait_timeout(d, Duration::from_millis(100))\n 396|                    .unwrap_or_else(|p| p.into_inner());\n 397|                d = g;\n 398|                drop(d);\n## daemon.rs\n 36|/// Poll cadence while waiting for the spawned daemon to come up.\n*37|const START_POLL: Duration = Duration::from_millis(25);\n 38|\n 39|/// The per-machine broker/brain daemon process.\n 40|pub struct Daemon;\n...\n 1625|        for _ in 0..25 {\n*1626|            std::thread::sleep(Duration::from_millis(40));\n 1627|            if !is_process_alive(gc) {\n 1628|                still_alive = false;\n 1629|                break;\n## dispatch.rs\n 86|/// workers block on their stream's event feed).\n*87|pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n 88|\n 89|/// What protocol a peer-initiated stream speaks, by its first record's shape.\n 90|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n...\n 190|/// Backoff base for a released claim (doubles per failed attempt).\n*191|const DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n 192|\n 193|/// The backoff before retry number `attempts` (1-based) runs.\n 194|fn retry_backoff(attempts: u32) -> Duration {\n## livehost.rs\n 1019|            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n*1020|                std::thread::sleep(Duration::from_millis(100));\n 1021|                continue;\n 1022|            }\n 1023|            let owlery = perch::owlery_dir();\n## nethost.rs\n 1288|        runtime.spawn(async move {\n*1289|            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n 1290|            loop {\n 1291|                ivl.tick().await;\n 1292|                canary.store(epoch_ms(), Ordering::Relaxed);\n## notif.rs\n 1179|                );\n*1180|                std::thread::sleep(std::time::Duration::from_millis(50));\n 1181|            };\n 1182|            assert_eq!(\n 1183|                content.trim(),\n## pairhost.rs\n 231|                                // pause so a tight error can't spin the loop.\n*232|                                tokio::time::sleep(Duration::from_millis(200)).await;\n 233|                            }\n 234|                        }\n 235|                    }\n...\n 436|                    }\n*437|                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,\n 438|                }\n 439|            }\n 440|        }\n...\n 476|        spt_net::net::pairing::ntp::force_refresh,\n*477|        Duration::from_millis(500),\n 478|    )\n 479|    .await\n 480|    .map_err(|e| {\n## projwriter.rs\n 49|/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n*50|/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\n*51|const POLL_MS: u64 = 500;\n 52|/// Debounce window after the first observed event — a burst (an adapter\n 53|/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\n 54|const DEBOUNCE_MS: u64 = 250;\n...\n 629|        }\n*630|        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\n 631|        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\n 632|        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\n 633|\n...\n 1061|        crate::test_home::with_home(|home| {\n*1062|            std::env::set_var(\"SPT_PROJINDEX_POLL_MS\", \"50\");\n 1063|            std::env::set_var(\"SPT_PROJINDEX_DEBOUNCE_MS\", \"150\");\n 1064|            let owlery = home.join(\"owlery\");\n 1065|            std::fs::create_dir_all(&owlery).unwrap();\n...\n 1082|                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n*1083|                std::thread::sleep(Duration::from_millis(25));\n 1084|            }\n 1085|            let enums_after_boot = stats().unwrap().cumulative.branch_enumerations;\n 1086|\n...\n 1098|                assert!(Instant::now() < consumed, \"burst never consumed\");\n*1099|                std::thread::sleep(Duration::from_millis(25));\n 1100|            }\n 1101|            let s = stats().unwrap();\n 1102|            stop.store(true, Ordering::SeqCst);\n 1103|            handle.join().unwrap();\n*1104|            std::env::remove_var(\"SPT_PROJINDEX_POLL_MS\");\n 1105|            std::env::remove_var(\"SPT_PROJINDEX_DEBOUNCE_MS\");\n 1106|\n 1107|            let burst_cycles = s.cumulative.branch_enumerations - enums_after_boot;\n## relay.rs\n 25|/// stays responsive to a stop signal (mirrors the interim listener cadence).\n*26|pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);\n 27|\n 28|/// A stateless message relay for one perch: a poll listener bound to the id plus\n 29|/// the perch path it drains. It carries no message buffer — every byte it\n## shellhost.rs\n 418|            }\n*419|            std::thread::sleep(std::time::Duration::from_millis(50));\n 420|        }\n 421|    }\n 422|\n...\n 437|            }\n*438|            std::thread::sleep(std::time::Duration::from_millis(50));\n 439|        }\n 440|    }\n 441|    let _ = std::fs::remove_file(perch.join(SHELL_PID_FILE));\n...\n 796|        // Give the NOOP a beat to exit so the liveness probe sees it gone.\n*797|        std::thread::sleep(std::time::Duration::from_millis(200));\n 798|\n 799|        let start = std::time::Instant::now();\n 800|        let outcome = close_shell(owlery, \"doyle\", &id, Some(&shell)).unwrap();\n## translation.rs\n 36|/// a translation filter has nothing to flush on EOF.\n*37|const REAP_GRACE: Duration = Duration::from_millis(500);\n 38|/// Poll interval while waiting out [`REAP_GRACE`].\n 39|const REAP_POLL: Duration = Duration::from_millis(20);\n 40|\n\n## pump/\n### mod.rs\n 96|/// The pump's tick granularity — each cadence fires when its period elapsed.\n*97|const TICK: Duration = Duration::from_millis(200);\n 98|\n 99|/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall\n 100|/// warning reads 3-cadence staleness, so a few seconds of heartbeat\n...\n 611|        if !any_due {\n*612|            thread::sleep(TICK);\n 613|            continue;\n 614|        }\n 615|\n...\n 1199|/// `base` is the backoff floor (production: [`SUPERVISE_BACKOFF_BASE`];\n*1200|/// the unit harness shrinks it). Backoff sleeps in [`TICK`] slices so a\n 1201|/// stop request lands promptly. Generic over the body for the unit\n 1202|/// harness; production binds [`run_peer_pump`].\n 1203|// [impl->REQ-DAEMON-5]\n...\n 1233|        while slept < backoff && !stop.load(Ordering::Relaxed) {\n*1234|            thread::sleep(TICK.min(backoff - slept));\n*1235|            slept += TICK;\n 1236|        }\n 1237|    }\n 1238|}\n...\n 1286|        // And it becomes due once enough has elapsed.\n*1287|        let earlier = now - Duration::from_millis(50);\n 1288|        assert!(due(Some(earlier), false, now, Duration::from_millis(10)));\n*1289|        assert!(!due(Some(earlier), false, now, Duration::from_millis(500)));\n 1290|    }\n 1291|\n 1292|    // [unit->REQ-CONV-2] [unit->REQ-INST-3] the wake edge (a worker's\n...\n 1308|        // Wake on an already-elapsed leg is due (the same single round).\n*1309|        let earlier = now - Duration::from_millis(50);\n 1310|        assert!(due(Some(earlier), true, now, Duration::from_millis(10)));\n 1311|    }\n 1312|"}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":44,"fileCount":16,"files":["crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/dispatch.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","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/relay.rs","crates/spt-daemon/src/shellhost.rs","crates/spt-daemon/src/translation.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/attach.rs","count":1},{"path":"crates/spt-daemon/src/brainproc.rs","count":8},{"path":"crates/spt-daemon/src/broker.rs","count":2},{"path":"crates/spt-daemon/src/config.rs","count":2},{"path":"crates/spt-daemon/src/conn.rs","count":1},{"path":"crates/spt-daemon/src/daemon.rs","count":2},{"path":"crates/spt-daemon/src/dispatch.rs","count":2},{"path":"crates/spt-daemon/src/livehost.rs","count":1},{"path":"crates/spt-daemon/src/nethost.rs","count":1},{"path":"crates/spt-daemon/src/notif.rs","count":1},{"path":"crates/spt-daemon/src/pairhost.rs","count":3},{"path":"crates/spt-daemon/src/projwriter.rs","count":7},{"path":"crates/spt-daemon/src/pump/mod.rs","count":8},{"path":"crates/spt-daemon/src/relay.rs","count":1},{"path":"crates/spt-daemon/src/shellhost.rs","count":3},{"path":"crates/spt-daemon/src/translation.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## attach.rs\n 101│/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.\n*102│const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);\n 103│\n 104│/// The CONTROLLER fell behind the live ring further than the ring holds — the\n 105│/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an\n## brainproc.rs\n  56│/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n *57│const TICK: Duration = Duration::from_millis(200);\n  58│\n  59│/// How long the brain child retries connecting to the broker socket before\n  60│/// giving up (the broker may still be binding — the child is freshly spawned).\n    │...\n  62│/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\n *63│const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n  64│\n  65│/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n  66│/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n    │...\n 624│\n*625│/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n 626│/// promptly mid-backoff (saturating — never an underflow panic).\n 627│fn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n 628│    let mut slept = Duration::ZERO;\n 629│    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630│        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n*631│        slept += TICK;\n 632│    }\n 633│}\n 634│\n    │...\n 657│/// Watch a freshly-spawned trial candidate for readiness within `window`\n*658│/// (ADR-0018 Q7, D6-2). Polls in `TICK` slices (so a daemon stop stays prompt,\n 659│/// A11/note-ii) for a `brain.ready` stamp **== this spawn's `generation`** — a\n 660│/// stale stamp from another generation never satisfies the gate (the single\n 661│/// load-bearing correctness invariant; the A9 clear-before-spawn closes the\n    │...\n 718│        }\n*719│        thread::sleep(TICK);\n 720│    }\n 721│}\n 722│\n    │...\n 898│                            }\n*899│                            Ok(None) => thread::sleep(TICK),\n 900│                            Err(e) => {\n 901│                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n 902│                                break;\n## broker.rs\n 198│/// Poll slice while waiting on a racing wake's session to appear.\n*199│const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 200│\n 201│/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n 202│/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n    │...\n 370│/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n*371│const INJECT_SETTLE_POLL: Duration = Duration::from_millis(40);\n 372│/// The benign readiness probe: a DSR cursor-position query (`ESC[6n`). Invisible to\n 373│/// a TUI input line and never a submit; a live terminal/child answers or re-renders,\n 374│/// producing output — the POSITIVE signal that the input reader is attached.\n## config.rs\n 372│        let cfg = DaemonConfig::load_from(&path);\n*373│        assert_eq!(cfg.pulse_period, Duration::from_millis(250));\n 374│        assert_ne!(\n 375│            cfg.pulse_period, DEFAULT_PULSE_PERIOD,\n 376│            \"must override the default\"\n    │...\n 521│        let cfg = DaemonConfig::load_from(&path);\n*522│        assert_eq!(cfg.crash_grace, Duration::from_millis(500));\n 523│        assert_ne!(\n 524│            cfg.crash_grace, DEFAULT_CRASH_GRACE,\n 525│            \"must override the default\"\n## conn.rs\n 394│                    .dog_cv\n*395│                    .wait_timeout(d, Duration::from_millis(100))\n 396│                    .unwrap_or_else(|p| p.into_inner());\n 397│                d = g;\n 398│                drop(d);\n## daemon.rs\n   36│/// Poll cadence while waiting for the spawned daemon to come up.\n  *37│const START_POLL: Duration = Duration::from_millis(25);\n   38│\n   39│/// The per-machine broker/brain daemon process.\n   40│pub struct Daemon;\n     │...\n 1625│        for _ in 0..25 {\n*1626│            std::thread::sleep(Duration::from_millis(40));\n 1627│            if !is_process_alive(gc) {\n 1628│                still_alive = false;\n 1629│                break;\n## dispatch.rs\n  86│/// workers block on their stream's event feed).\n *87│pub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n  88│\n  89│/// What protocol a peer-initiated stream speaks, by its first record's shape.\n  90│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n    │...\n 190│/// Backoff base for a released claim (doubles per failed attempt).\n*191│const DISPATCH_RETRY_BASE: Duration = Duration::from_millis(500);\n 192│\n 193│/// The backoff before retry number `attempts` (1-based) runs.\n 194│fn retry_backoff(attempts: u32) -> Duration {\n## livehost.rs\n 1019│            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n*1020│                std::thread::sleep(Duration::from_millis(100));\n 1021│                continue;\n 1022│            }\n 1023│            let owlery = perch::owlery_dir();\n## nethost.rs\n 1288│        runtime.spawn(async move {\n*1289│            let mut ivl = tokio::time::interval(Duration::from_millis(25));\n 1290│            loop {\n 1291│                ivl.tick().await;\n 1292│                canary.store(epoch_ms(), Ordering::Relaxed);\n## notif.rs\n 1179│                );\n*1180│                std::thread::sleep(std::time::Duration::from_millis(50));\n 1181│            };\n 1182│            assert_eq!(\n 1183│                content.trim(),\n## pairhost.rs\n 231│                                // pause so a tight error can't spin the loop.\n*232│                                tokio::time::sleep(Duration::from_millis(200)).await;\n 233│                            }\n 234│                        }\n 235│                    }\n    │...\n 436│                    }\n*437│                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,\n 438│                }\n 439│            }\n 440│        }\n    │...\n 476│        spt_net::net::pairing::ntp::force_refresh,\n*477│        Duration::from_millis(500),\n 478│    )\n 479│    .await\n 480│    .map_err(|e| {\n## projwriter.rs\n   49│/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n  *50│/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\n  *51│const POLL_MS: u64 = 500;\n   52│/// Debounce window after the first observed event — a burst (an adapter\n   53│/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\n   54│const DEBOUNCE_MS: u64 = 250;\n     │...\n  629│        }\n *630│        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\n  631│        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\n  632│        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\n  633│\n     │...\n 1061│        crate::test_home::with_home(|home| {\n*1062│            std::env::set_var(\"SPT_PROJINDEX_POLL_MS\", \"50\");\n 1063│            std::env::set_var(\"SPT_PROJINDEX_DEBOUNCE_MS\", \"150\");\n 1064│            let owlery = home.join(\"owlery\");\n 1065│            std::fs::create_dir_all(&owlery).unwrap();\n     │...\n 1082│                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n*1083│                std::thread::sleep(Duration::from_millis(25));\n 1084│            }\n 1085│            let enums_after_boot = stats().unwrap().cumulative.branch_enumerations;\n 1086│\n     │...\n 1098│                assert!(Instant::now() < consumed, \"burst never consumed\");\n*1099│                std::thread::sleep(Duration::from_millis(25));\n 1100│            }\n 1101│            let s = stats().unwrap();\n 1102│            stop.store(true, Ordering::SeqCst);\n 1103│            handle.join().unwrap();\n*1104│            std::env::remove_var(\"SPT_PROJINDEX_POLL_MS\");\n 1105│            std::env::remove_var(\"SPT_PROJINDEX_DEBOUNCE_MS\");\n 1106│\n 1107│            let burst_cycles = s.cumulative.branch_enumerations - enums_after_boot;\n## relay.rs\n 25│/// stays responsive to a stop signal (mirrors the interim listener cadence).\n*26│pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);\n 27│\n 28│/// A stateless message relay for one perch: a poll listener bound to the id plus\n 29│/// the perch path it drains. It carries no message buffer — every byte it\n## shellhost.rs\n 418│            }\n*419│            std::thread::sleep(std::time::Duration::from_millis(50));\n 420│        }\n 421│    }\n 422│\n    │...\n 437│            }\n*438│            std::thread::sleep(std::time::Duration::from_millis(50));\n 439│        }\n 440│    }\n 441│    let _ = std::fs::remove_file(perch.join(SHELL_PID_FILE));\n    │...\n 796│        // Give the NOOP a beat to exit so the liveness probe sees it gone.\n*797│        std::thread::sleep(std::time::Duration::from_millis(200));\n 798│\n 799│        let start = std::time::Instant::now();\n 800│        let outcome = close_shell(owlery, \"doyle\", &id, Some(&shell)).unwrap();\n## translation.rs\n 36│/// a translation filter has nothing to flush on EOF.\n*37│const REAP_GRACE: Duration = Duration::from_millis(500);\n 38│/// Poll interval while waiting out [`REAP_GRACE`].\n 39│const REAP_POLL: Duration = Duration::from_millis(20);\n 40│\n\n## pump/\n### mod.rs\n   96│/// The pump's tick granularity — each cadence fires when its period elapsed.\n  *97│const TICK: Duration = Duration::from_millis(200);\n   98│\n   99│/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall\n  100│/// warning reads 3-cadence staleness, so a few seconds of heartbeat\n     │...\n  611│        if !any_due {\n *612│            thread::sleep(TICK);\n  613│            continue;\n  614│        }\n  615│\n     │...\n 1199│/// `base` is the backoff floor (production: [`SUPERVISE_BACKOFF_BASE`];\n*1200│/// the unit harness shrinks it). Backoff sleeps in [`TICK`] slices so a\n 1201│/// stop request lands promptly. Generic over the body for the unit\n 1202│/// harness; production binds [`run_peer_pump`].\n 1203│// [impl->REQ-DAEMON-5]\n     │...\n 1233│        while slept < backoff && !stop.load(Ordering::Relaxed) {\n*1234│            thread::sleep(TICK.min(backoff - slept));\n*1235│            slept += TICK;\n 1236│        }\n 1237│    }\n 1238│}\n     │...\n 1286│        // And it becomes due once enough has elapsed.\n*1287│        let earlier = now - Duration::from_millis(50);\n 1288│        assert!(due(Some(earlier), false, now, Duration::from_millis(10)));\n*1289│        assert!(!due(Some(earlier), false, now, Duration::from_millis(500)));\n 1290│    }\n 1291│\n 1292│    // [unit->REQ-CONV-2] [unit->REQ-INST-3] the wake edge (a worker's\n     │...\n 1308│        // Wake on an already-elapsed leg is due (the same single round).\n*1309│        let earlier = now - Duration::from_millis(50);\n 1310│        assert!(due(Some(earlier), true, now, Duration::from_millis(10)));\n 1311│    }\n 1312│"},"isError":false,"timestamp":1784271537227}}
{"type":"message","id":"f25372db","parentId":"8b1d747e","timestamp":"2026-07-17T06:58:57.232Z","message":{"role":"toolResult","toolCallId":"call_4G5mRdLYeSXkHrmpXh4Dk3RK|fc_038f28d3e93bbfd4016a59d2b118a081968bb4cb3d07e0ae9d","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## adapter_update.rs\n 43|    pub signed: SignedRelease,\n*44|    /// The pulled artifact bytes (hashed + matched against the signed digest).\n 45|    pub bytes: Vec<u8>,\n 46|}\n 47|\n...\n 72|pub enum AdapterUpdateOutcome {\n*73|    /// `file_pull`: the payload is authentic (adapter key) + intact (digest) and\n 74|    /// ready to apply. Carries the verified metadata.\n 75|    Verified(ReleaseMetadata),\n 76|    /// `delegated` + attested: run the adapter's own updater (the trusted command).\n...\n 104|            };\n*105|            // Authentic (adapter key) + intact (digest) — the two halves of\n 106|            // adapter content signing.\n 107|            match verify_signature(&payload.signed, &key) {\n 108|                Ok(meta) => match verify_artifact(&meta, &payload.bytes) {\n...\n 305|\n*306|    // [unit->REQ-UPD-5] tampered bytes (digest mismatch) are rejected even with a\n 307|    // valid signature over the metadata.\n 308|    #[test]\n 309|    fn file_pull_tampered_bytes_rejected() {\n## brain.rs\n 895|    /// Attach this (freshly-connected) brain to an **existing** broker session's\n*896|    /// output from `from_seq`, e.g. a dedicated digest-feed subscriber that did\n 897|    /// not spawn the session itself. Sets the resume cursor + session, then\n 898|    /// subscribes (the broker replays from `from_seq`, then streams live). Unlike\n 899|    /// [`Brain::handoff`] this does not touch the generation — it is a *second*\n## brainproc.rs\n 13|//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n*14|//! verb. The digest hub serve + seed-control stay broker-side continuity\n*15|//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n 16|//! loops.\n 17|//!\n 18|//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n...\n 1396|\n*1397|    /// D7-1: the `exe_hash` breadcrumb reader extracts the resident-bytes digest\n 1398|    /// and is N-1-readable — a legacy D6-era `{pid, generation}` stamp (no\n 1399|    /// `exe_hash`) reads `None`, never an error.\n 1400|    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n## daemon.rs\n 26|use crate::config::{DaemonConfig, RelayChoice};\n*27|use crate::digesthub::{self, DigestHub};\n 28|use crate::endpoint::{\n*29|    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n 30|};\n 31|use crate::nethost::{NetConfig, NetHost};\n 32|use crate::seedmap::{self, SeedRegistry};\n...\n 253|\n*254|        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n*255|        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n 256|        // delta-stream subscribes against it. Served on a background thread like\n 257|        // the broker. The per-session feed that *populates* the hub is driven by\n 258|        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n 259|        // sessions are spawned through the broker; a bare daemon serves the\n*260|        // channel and answers \"no live digest\" until a session is hosted.\n*261|        let digest_hub = Arc::new(DigestHub::new());\n 262|        {\n*263|            let hub = Arc::clone(&digest_hub);\n 264|            thread::spawn(move || {\n*265|                let _ = digesthub::serve_digest_control(&digest_socket_name(), hub);\n 266|            });\n 267|        }\n 268|\n 269|        // The drive hub (M11-W2, REQ-SHELL-3) holds the per-shell ephemeral drive\n*270|        // slot — the owner→shell control surface. Broker-held like the digest hub\n 271|        // (the daemon-lifetime CLI-reachable anchor; a brain restart loses\n 272|        // nothing — the slot is ephemeral control, a restart correctly drops it).\n 273|        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n## digest.rs\n*1|//! The session-digest **log projection** — the daemon-side, on-demand projection\n 2|//! of an endpoint's live activity buffer from its normalized session logs\n 3|//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n 4|//!\n...\n 7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n*8|//! on every output chunk. The digest is now a **projection of the same normalized\n 9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n*10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n*11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n 12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n 13|//! read the logs.\n 14|//!\n...\n 20|//!   per-session JSONL is Path A's worked example.\n*21|//! - A **log-less** adapter pushes digest-records directly via `api digest-entry`\n*22|//!   into the perch's `digest.log` → the projection reads that.\n 23|//!\n*24|//! The records carry the published digest-record contract\n*25|//! ([`spt_term::DigestRecord`]); the projection folds them into the structured\n*26|//! [`Digest`] (N-turn window, collapsed tool sprints, ~25-char arg truncation).\n 27|//!\n 28|//! ## Snapshot vs delta\n 29|//!\n 30|//! A projection is diffed against the hub's prior snapshot\n*31|//! ([`crate::digesthub::DigestHub::project_and_publish`]) to produce a\n*32|//! [`DigestUpdate`]: `from` is where the changed turns begin, and the subscriber\n 33|//! applies it by **truncating its view to `from`, then appending `turns`**. The\n*34|//! delta-stream **contract** ships; its deltas are driven by pull / digest-entry\n 35|//! publishes (autonomous file-watch freshness is deferred to the consuming\n*36|//! frontend milestone — CONTEXT.md \"session digest\").\n 37|// [impl->REQ-TERM-4]\n 38|\n 39|use std::collections::BTreeMap;\n...\n 43|\n*44|use spt_live::extract_digest;\n*45|use spt_runtime::manifest::Digest as ManifestDigest;\n 46|use spt_store::perch::{self, ParentHint};\n*47|use spt_term::{Digest, DigestConfig, TimelineItem, Turn};\n 48|\n*49|/// Bound for the projection's extractor run (the `[digest]` extractor is a\n 50|/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n*51|const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n 52|\n 53|/// How many recent sessions the spanning projection enumerates from the ledger\n 54|/// (REQ-TERM-6 — \"the last K\"). Bounds the per-session extractor runs; the visible\n*55|/// turn window ([`DigestConfig::window_turns`]) bounds what is shown across them.\n 56|const SPAN_SESSIONS: usize = 5;\n 57|\n 58|/// Consumer-supplied presentation overrides (ADR-0019: an adapter declares\n*59|/// `[digest]` presentation defaults that **any consumer may override** at\n 60|/// pull/subscribe). Every field is optional — unset falls through to the\n## digesthub.rs\n*1|//! The daemon-held digest hub + its control channel (ADR-0008 amendment /\n 2|//! REQ-TERM-4).\n 3|//!\n 4|//! The hub holds the latest projected snapshot per endpoint and fans every change\n*5|//! out to live subscribers. The digest is **projected on demand** from the\n*6|//! endpoint's normalized records ([`project_endpoint_digest`]) — there is no live\n*7|//! feed; a pull or a `digest-entry` push triggers a projection, and\n*8|//! [`DigestHub::project_and_publish`] diffs it against the prior snapshot to\n*9|//! produce the [`DigestUpdate`] delta. The control channel is the access surface\n 10|//! ADR-0008 describes — two modes over one transport:\n 11|//!\n*12|//! - **snapshot pull** — `spt endpoint digest <id>` ([`pull_snapshot`]) projects\n 13|//!   the endpoint's current buffer on demand and returns it;\n*14|//! - **delta-stream** — `spt endpoint digest <id> --follow` ([`follow`])\n 15|//!   subscribes, receives a base snapshot, then **only the changes**\n*16|//!   ([`DigestUpdate`]s) as pulls / `digest-entry` pushes re-project. The stream\n 17|//!   contract ships; autonomous file-watch freshness is deferred to the consuming\n*18|//!   frontend milestone (CONTEXT.md \"session digest\").\n 19|//!\n 20|//! The channel rides the same forward-compatible [`Envelope`] + codec + version\n 21|//! handshake as the broker / seed-control IPC (a transient `spt` CLI connects as a\n...\n 33|use serde::{Deserialize, Serialize};\n*34|use spt_term::{Digest, DigestEntry, Turn};\n 35|\n 36|use crate::codec::{read_frame, write_frame};\n*37|use crate::digest::{common_prefix_len, project_endpoint_digest, DigestOverride, DigestUpdate};\n 38|use crate::frame::{Envelope, Role};\n 39|use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};\n 40|\n*41|/// Client→daemon: pull the current structured digest for an endpoint (projects\n 42|/// it on demand from the endpoint's normalized records).\n*43|pub const KIND_DIGEST_SNAPSHOT: &str = \"digest_snapshot\";\n*44|/// Daemon→client: the snapshot reply (digest + version; absent ⇒ no digest).\n*45|pub const KIND_DIGEST_RESULT: &str = \"digest_result\";\n 46|/// Client→daemon: subscribe to an endpoint's delta-stream from a version floor.\n*47|pub const KIND_DIGEST_SUBSCRIBE: &str = \"digest_subscribe\";\n*48|/// Daemon→client: one structured digest change (the streamed delta / base).\n*49|pub const KIND_DIGEST_DELTA: &str = \"digest_delta\";\n 50|/// Client→daemon: re-project an endpoint now and publish any change to live\n*51|/// subscribers — the push-driven delta after an `api digest-entry` append (no\n 52|/// reply; the delta reaches subscribers via the fan-out, REQ-TERM-4).\n## drivehub.rs\n 11|//! **Where the slot lives (broker, not relay; broker, not brain).** The slot is\n*12|//! held by the always-up BROKER process (alongside [`crate::digesthub::DigestHub`]),\n 13|//! the daemon-lifetime CLI-reachable anchor post-ADR-0018 — NOT the restartable\n 14|//! brain child (a CLI cannot reach brain memory; all control sockets are\n 15|//! broker-served) and NOT a file (a file would survive a daemon restart and\n...\n 31|//! The channel rides the same forward-compatible [`Envelope`] + codec + version\n*32|//! handshake as the digest / broker / seed-control IPC (a transient `spt` CLI\n 33|//! connects as a [`Role::Brain`] logic client). **Local addressing only** —\n 34|//! cross-node drive over the ephemeral Iroh link is a later task (never spooled,\n 35|//! same drop-on-offline semantics).\n...\n 179|/// Bind the drive-control channel at `name` and serve it forever against `hub`.\n*180|/// Thread-per-connection (mirroring the digest / broker / seed-control channels).\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## endpoint.rs\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 45|}\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 51|// [impl->REQ-SHELL-3]\n 52|pub fn drive_socket_name() -> String {\n## harnesshost.rs\n 85|/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh\n*86|/// `claude --session-id {session_id}`, which would start a blank transcript). A\n 87|/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`\n 88|/// (full back-compat); a `false` always uses `[session.self]`.\n 89|// [impl->REQ-HOST-RUN-1]\n...\n 223|/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)\n*224|/// passes so the harness reads the RIGHT `.claude`/session history/digest source.\n 225|///\n 226|/// `is_resume`: forwarded to [`prepare_harness_spawn`] — `true` selects the\n 227|/// adapter's `[session.resume]` native-resume command (REQ-SESSION-RESUME-TEMPLATE)\n...\n 561|    // runs `claude -r {session_id}`, not the fresh `claude --session-id …` that\n*562|    // would start a blank transcript), and `is_resume=false` fills SELF. Both fill\n 563|    // the SAME {id}/{session_id} catalog.\n 564|    #[test]\n 565|    fn prepare_selects_resume_template_on_resume_else_self() {\n...\n 600|        // The two templates produce genuinely different argv (the bug class: a\n*601|        // resume that re-ran the fresh command → blank transcript).\n 602|        assert_ne!(\n 603|            resumed.tokens, fresh.tokens,\n 604|            \"resume and self must tokenize to different commands\"\n## lib.rs\n 121|pub mod deelevate;\n*122|pub mod digest;\n*123|pub mod digesthub;\n 124|pub mod docshost;\n 125|pub mod dispatch;\n 126|pub mod drivehub;\n...\n 190|};\n*191|pub use digest::{\n*192|    common_prefix_len, project_endpoint_digest, resolve_config, DigestOverride, DigestUpdate,\n 193|};\n*194|pub use digesthub::{\n*195|    digest_to_json, follow, pull_snapshot, render_digest, render_update, reproject,\n*196|    serve_digest_control, update_to_json, DigestHub,\n 197|};\n 198|pub use effect::{\n 199|    with_tracing_retry, EffectJournal, EffectKey, EffectKind, Outcome, OP_NO_LONGER_HELD_MARKER,\n...\n 208|pub use endpoint::{\n*209|    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name,\n 210|    pump_heartbeat_path, read_pump_health, read_pump_heartbeat, seed_socket_name,\n 211|    tunnel_socket_name,\n 212|};\n## lifecycle.rs\n 26|use spt_live::{\n*27|    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n 28|    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n 29|};\n 30|use spt_runtime::{Manifest, ManifestRuntime};\n...\n 753|    /// Fetch history per the manifest `[history]` strategy (empty if none).\n*754|    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n 755|        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n 756|        // history fetch below), so a live-update refresh never waits on it.\n 757|        let manifest = self.cell.manifest_snapshot();\n...\n 760|        };\n*761|        fetch_history(\n 762|            h,\n 763|            &self.base_keys(session_id),\n 764|            &self.id,\n...\n 858|        };\n*859|        let history = self.history(session_id);\n 860|        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n 861|        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n 862|        // dir so the summarizer child runs where the endpoint lives, never in the\n...\n 1002|        // needs no `.live-bin` own-copy (unlike the retired resident spawn) — the\n*1003|        // install-dir/PATH snapshot is correct here (same class as translate/digest).\n 1004|        //\n 1005|        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n 1006|        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n...\n 2885|    // machinery — a per-event psyche turn is TURN-BOUNDED (same image-lock class as\n*2886|    // translate/digest) and spawns from the install-dir/PATH snapshot, so no\n 2887|    // per-endpoint copy is needed. REQ-INSTALL-11's `unit` stage stays covered by the\n 2888|    // install-dir program-resolution units in spt-runtime (runtime.rs) + harnesshost.\n 2889|}\n## msg.rs\n 280|    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\n*281|    /// consumer (Claude Code) reads its `.claude`, session history, and digest\n 282|    /// source relative to cwd, so a project-scoped launcher MUST place the\n 283|    /// harness in the user's project root, not the daemon's. Additive +\n 284|    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\n## nethost.rs\n 1171|    /// ([`crate::pairhost::respond`]), never registered in the stream table.\n*1172|    /// One endpoint for both is deliberate: the pairing transcript binds and\n 1173|    /// pins the REAL node pubkey, and the relay can only route one endpoint\n 1174|    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).\n 1175|    /// The meet-rotation listener loop rides the same runtime.\n## projwriter.rs\n 416|        for (id, perch_path, origin_cwd) in &targets {\n*417|            let entries = sessions::read_all(perch_path);\n 418|            let store_ids: Vec<String> = self\n 419|                .branch_order\n 420|                .iter()\n...\n 1186|            let perch = owlery.join(id);\n*1187|            let entries = sessions::read_all(&perch);\n 1188|            let origin = info::read_info(&perch).and_then(|r| r.cwd);\n 1189|            projderive::project_refs_from(\n 1190|                &entries,\n## propagate.rs\n 328|    /// metadata) carry zero artifact bytes; a post-fetch reject means the\n*329|    /// pulled bytes did not match the signed digest.\n 330|    Rejected(RejectReason),\n 331|}\n 332|\n...\n 340|/// [`verify_metadata`] on the offer before any bytes move, then the whole\n*341|/// [`plan_verified`] front door (metadata + artifact digest + classify) on\n 342|/// the real bytes before staging.\n 343|// [impl->REQ-UPD-1]\n 344|// [impl->REQ-UPD-2]\n## registryhost.rs\n 893|fn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\n*894|    let entries = spt_store::sessions::read_all(perch_path);\n 895|    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\n 896|    let mut out: Vec<String> = Vec::new();\n 897|    for cwd in entries.iter().rev().filter_map(|e| e.cwd.as_deref()) {\n## relcache.rs\n 16|//! presence is the commit point, so [`ReleaseCache::staged`] never observes a\n*17|//! metadata file whose artifact is missing or torn mid-write. A digest\n 18|//! mismatch between the two (a corrupted or tampered artifact at rest) is\n 19|//! surfaced to the *peer's* gate as `ArtifactMismatch` — never silently\n 20|//! re-served as healthy (the serve side does not re-verify; the receive side\n...\n 263|    /// metadata last (commit point). The caller MUST have verified that the\n*264|    /// provided bytes match the signed digests.\n 265|    // [impl->REQ-UPD-6]\n 266|    pub fn stage_update_set(\n 267|        &self,\n## release.rs\n 34|use serde::{Deserialize, Serialize};\n*35|use sha2::{Digest, Sha256};\n 36|\n 37|use crate::update::ReleaseSpec;\n 38|\n...\n 92|/// the Rust target triple (e.g. `x86_64-pc-windows-msvc`); this entry carries\n*93|/// the digest and update-class axes for that platform's binary.\n 94|// [impl->REQ-UPD-6]\n 95|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n 96|pub struct UpdateArtifactMetadata {\n...\n 191|/// A signed update set: exact signed metadata bytes plus detached Ed25519\n*192|/// signature. The signature covers all platform artifact digests.\n 193|// [impl->REQ-UPD-6]\n 194|#[derive(Debug, Clone, Serialize, Deserialize)]\n 195|pub struct SignedUpdateSet {\n...\n 441|    Rollback { current: u64, candidate: u64 },\n*442|    /// The actual binary's digest does not match the signed `artifact_sha256`.\n 443|    ArtifactMismatch { signed: String, actual: String },\n 444|    /// A signed update set has no artifact for this node's platform.\n 445|    NoArtifactForPlatform { platform: String },\n...\n 472|                    f,\n*473|                    \"artifact digest mismatch: signed {signed}, actual {actual}\"\n 474|                )\n 475|            }\n 476|            RejectReason::NoArtifactForPlatform { platform } => {\n...\n 488|/// [`verify_artifact`] with the downloaded binary next (the signature binds it\n*489|/// by digest, but the digest must still be checked against the real bytes).\n 490|///\n 491|/// Check order is deliberate: identify + authenticate the signer *first* (an\n 492|/// unknown/revoked key or a bad signature is rejected before any field of the\n...\n 643|/// ([`REQ-UPD-2`]). Run after [`verify_metadata`] so the signature has already\n*644|/// attested to the expected digest — this closes the loop to the real bytes.\n 645|// [impl->REQ-UPD-2]\n 646|pub fn verify_artifact(meta: &ReleaseMetadata, artifact: &[u8]) -> Result<(), RejectReason> {\n 647|    let actual = sha256_hex(artifact);\n...\n 657|\n*658|/// Confirm downloaded docs-bundle bytes match the digest inside a signed\n 659|/// update set. The caller treats a mismatch as SKIP-the-docs, never as a\n 660|/// release rejection (ADR-0036 §4 failure isolation).\n 661|// [impl->REQ-DOCS-RELEASE-ASSET]\n...\n 679|\n*680|/// Confirm this platform's downloaded bytes match the digest inside a signed\n 681|/// update set.\n 682|// [impl->REQ-UPD-6]\n 683|pub fn verify_update_set_artifact(\n...\n 699|\n*700|/// Lowercase-hex SHA-256 of `bytes` — the artifact-digest format used in\n 701|/// [`ReleaseMetadata::artifact_sha256`].\n 702|pub fn sha256_hex(bytes: &[u8]) -> String {\n 703|    let mut hasher = Sha256::new();\n...\n 931|\n*932|        // Mismatched bytes reject with the digest pair.\n 933|        match verify_update_set_docs(&parsed, b\"TAMPERED\") {\n 934|            Err(RejectReason::ArtifactMismatch { signed, actual }) => {\n 935|                assert_eq!(signed, sha256_hex(bundle));\n...\n 1006|    // [unit->REQ-UPD-2] a correctly-signed, newer, in-channel, unexpired release\n*1007|    // from a trusted key verifies, and its bytes match the signed digest.\n 1008|    #[test]\n 1009|    fn valid_release_verifies_and_artifact_matches() {\n 1010|        let sk = signing_key(7);\n...\n 1015|        assert_eq!(meta.version, 6);\n*1016|        verify_artifact(&meta, artifact).expect(\"artifact digest matches the signed value\");\n 1017|        assert_eq!(meta.spec().broker_resource_abi, BROKER_RESOURCE_ABI);\n 1018|    }\n 1019|\n...\n 1126|\n*1127|    // [unit->REQ-UPD-2] a binary whose bytes do not hash to the signed digest is\n 1128|    // rejected — the signature attests to specific bytes, not just a manifest.\n 1129|    #[test]\n*1130|    fn artifact_digest_mismatch_is_rejected() {\n 1131|        let sk = signing_key(7);\n 1132|        let signed = sign_release(&sk, &meta_for(6, b\"the signed bytes\"));\n 1133|        let meta = verify_metadata(&signed, &policy(&sk)).expect(\"metadata verifies\");\n*1134|        // A different binary than the one whose digest was signed.\n 1135|        let tampered = b\"DIFFERENT bytes than were signed\";\n 1136|        assert!(matches!(\n 1137|            verify_artifact(&meta, tampered),\n...\n 1154|        verify_update_set_artifact(&meta, \"x86_64-unknown-linux-gnu\", b\"linux bytes\")\n*1155|            .expect(\"linux artifact digest matches\");\n 1156|        verify_update_set_artifact(&meta, \"x86_64-pc-windows-msvc\", b\"win bytes\")\n*1157|            .expect(\"windows artifact digest matches\");\n 1158|        assert_eq!(\n 1159|            meta.provenance.as_ref().unwrap().build_host.as_deref(),\n 1160|            Some(\"fastbox\")\n...\n 1180|\n*1181|    // [unit->REQ-UPD-6] the whole set is signed: modifying any platform digest\n 1182|    // after signing invalidates the single set signature.\n 1183|    #[test]\n 1184|    fn update_set_tampering_breaks_whole_set_signature() {\n...\n 1221|\n*1222|    // [unit->REQ-UPD-6] a bytes/digest mismatch on the selected platform is\n 1223|    // rejected even though the set metadata itself verifies.\n 1224|    #[test]\n 1225|    fn update_set_artifact_mismatch_is_rejected() {\n## seedproofx.rs\n 5|//! [`seedproof`](spt_net::net::mesh::seedproof) is the pure crypto (membership\n*6|//! key + mutual channel-bound transcript). This module runs the **mutual\n 7|//! exchange** over a dedicated control stream at every dial + accept, *before*\n 8|//! the connection is registered as usable: each side proves current-epoch\n 9|//! knowledge of the seed for **every subnet the two nodes share**, and the\n...\n 35|//! connection drops (exact-epoch; the N-1 re-seed grace is Mesh-D7). Nonces are\n*36|//! per-connection-fresh and reused across the per-subnet proofs (the transcript\n 37|//! already binds `subnet_id` + `seed_epoch`).\n 38|// [impl->REQ-MESH-1]\n 39|\n...\n 44|use spt_net::net::mesh::seedproof::{\n*45|    MembershipKey, ProofRole, SeedProofFrame, SeedProofTranscript, NONCE_LEN,\n 46|};\n 47|use spt_proto::identity::{Identity, PublicKey};\n 48|use spt_store::peeraddrs::PeerAddrStore;\n...\n 267|    let remote_hex = remote.to_hex();\n*268|    // Channel binding: the transcript pins both handshake-proven pubkeys in a\n 269|    // role-fixed order (dialer first), so a tag is valid on exactly this conn.\n 270|    let (dialer_pub, acceptor_pub) = match role {\n 271|        ProofRole::Dialer => (local_id, remote),\n...\n 312|/// One subnet's local generations, as the proof exchange needs them: the\n*313|/// channel-bound transcript + membership key for the current generation and\n 314|/// (one-deep) the prior one (Mesh-D7). The outbound proof set is one tag per\n 315|/// generation; the inbound is graded against both.\n 316|struct GenProof {\n...\n 318|    mk: MembershipKey,\n*319|    transcript: SeedProofTranscript,\n 320|}\n 321|\n 322|struct LocalGens {\n...\n 331|    fn outbound(&self, mine: ProofRole) -> Vec<(u64, [u8; 32])> {\n*332|        let mut out = vec![(self.current.epoch, self.current.transcript.tag(&self.current.mk, mine))];\n 333|        if let Some(p) = &self.prev {\n*334|            out.push((p.epoch, p.transcript.tag(&p.mk, mine)));\n 335|        }\n 336|        out\n 337|    }\n...\n 340|    /// in the peer's role `prover`. A tag is matched only at the generation whose\n*341|    /// epoch it carries — the transcript binds the epoch, so a tag for one\n 342|    /// generation can never satisfy another.\n 343|    fn matches(&self, peer_proofs: &[(u64, [u8; 32])], prover: ProofRole) -> (bool, bool) {\n 344|        let mc = peer_proofs.iter().any(|(e, t)| {\n*345|            *e == self.current.epoch && self.current.transcript.verify(&self.current.mk, prover, t)\n 346|        });\n 347|        let mp = self.prev.as_ref().is_some_and(|p| {\n 348|            peer_proofs\n 349|                .iter()\n*350|                .any(|(e, t)| *e == p.epoch && p.transcript.verify(&p.mk, prover, t))\n 351|        });\n 352|        (mc, mp)\n 353|    }\n...\n 369|        mk: MembershipKey::derive(seed, name, epoch),\n*370|        transcript: SeedProofTranscript {\n 371|            subnet_id: name.to_string(),\n 372|            seed_epoch: epoch,\n 373|            dialer_pub,\n## shellhost.rs\n 28|use hmac::{Hmac, Mac};\n*29|use sha2::{Digest, Sha256};\n 30|\n 31|use spt_runtime::manifest::{AdapterKind, CommandReceipt, Manifest, Shell};\n 32|use spt_runtime::registry::AdapterRecord;\n\nShowing files 1-20 of 22+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":124,"fileCount":20,"files":["crates/spt-daemon/src/adapter_update.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/drivehub.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/release.rs","crates/spt-daemon/src/seedproofx.rs","crates/spt-daemon/src/shellhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/adapter_update.rs","count":4},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-daemon/src/brainproc.rs","count":3},{"path":"crates/spt-daemon/src/daemon.rs","count":9},{"path":"crates/spt-daemon/src/digest.rs","count":20},{"path":"crates/spt-daemon/src/digesthub.rs","count":20},{"path":"crates/spt-daemon/src/drivehub.rs","count":3},{"path":"crates/spt-daemon/src/endpoint.rs","count":4},{"path":"crates/spt-daemon/src/harnesshost.rs","count":4},{"path":"crates/spt-daemon/src/lib.rs","count":8},{"path":"crates/spt-daemon/src/lifecycle.rs","count":6},{"path":"crates/spt-daemon/src/msg.rs","count":1},{"path":"crates/spt-daemon/src/nethost.rs","count":1},{"path":"crates/spt-daemon/src/projwriter.rs","count":2},{"path":"crates/spt-daemon/src/propagate.rs","count":2},{"path":"crates/spt-daemon/src/registryhost.rs","count":1},{"path":"crates/spt-daemon/src/relcache.rs","count":2},{"path":"crates/spt-daemon/src/release.rs","count":20},{"path":"crates/spt-daemon/src/seedproofx.rs","count":12},{"path":"crates/spt-daemon/src/shellhost.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## adapter_update.rs\n  43│    pub signed: SignedRelease,\n *44│    /// The pulled artifact bytes (hashed + matched against the signed digest).\n  45│    pub bytes: Vec<u8>,\n  46│}\n  47│\n    │...\n  72│pub enum AdapterUpdateOutcome {\n *73│    /// `file_pull`: the payload is authentic (adapter key) + intact (digest) and\n  74│    /// ready to apply. Carries the verified metadata.\n  75│    Verified(ReleaseMetadata),\n  76│    /// `delegated` + attested: run the adapter's own updater (the trusted command).\n    │...\n 104│            };\n*105│            // Authentic (adapter key) + intact (digest) — the two halves of\n 106│            // adapter content signing.\n 107│            match verify_signature(&payload.signed, &key) {\n 108│                Ok(meta) => match verify_artifact(&meta, &payload.bytes) {\n    │...\n 305│\n*306│    // [unit->REQ-UPD-5] tampered bytes (digest mismatch) are rejected even with a\n 307│    // valid signature over the metadata.\n 308│    #[test]\n 309│    fn file_pull_tampered_bytes_rejected() {\n## brain.rs\n 895│    /// Attach this (freshly-connected) brain to an **existing** broker session's\n*896│    /// output from `from_seq`, e.g. a dedicated digest-feed subscriber that did\n 897│    /// not spawn the session itself. Sets the resume cursor + session, then\n 898│    /// subscribes (the broker replays from `from_seq`, then streams live). Unlike\n 899│    /// [`Brain::handoff`] this does not touch the generation — it is a *second*\n## brainproc.rs\n   13│//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n  *14│//! verb. The digest hub serve + seed-control stay broker-side continuity\n  *15│//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n   16│//! loops.\n   17│//!\n   18│//! What D1 proves (de-risk spike #1, the design's hardest invariant): the\n     │...\n 1396│\n*1397│    /// D7-1: the `exe_hash` breadcrumb reader extracts the resident-bytes digest\n 1398│    /// and is N-1-readable — a legacy D6-era `{pid, generation}` stamp (no\n 1399│    /// `exe_hash`) reads `None`, never an error.\n 1400│    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n## daemon.rs\n  26│use crate::config::{DaemonConfig, RelayChoice};\n *27│use crate::digesthub::{self, DigestHub};\n  28│use crate::endpoint::{\n *29│    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n  30│};\n  31│use crate::nethost::{NetConfig, NetHost};\n  32│use crate::seedmap::{self, SeedRegistry};\n    │...\n 253│\n*254│        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n*255│        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n 256│        // delta-stream subscribes against it. Served on a background thread like\n 257│        // the broker. The per-session feed that *populates* the hub is driven by\n 258│        // the brain (the parse is logic — ADR-0004 §B), wired as daemon-hosted\n 259│        // sessions are spawned through the broker; a bare daemon serves the\n*260│        // channel and answers \"no live digest\" until a session is hosted.\n*261│        let digest_hub = Arc::new(DigestHub::new());\n 262│        {\n*263│            let hub = Arc::clone(&digest_hub);\n 264│            thread::spawn(move || {\n*265│                let _ = digesthub::serve_digest_control(&digest_socket_name(), hub);\n 266│            });\n 267│        }\n 268│\n 269│        // The drive hub (M11-W2, REQ-SHELL-3) holds the per-shell ephemeral drive\n*270│        // slot — the owner→shell control surface. Broker-held like the digest hub\n 271│        // (the daemon-lifetime CLI-reachable anchor; a brain restart loses\n 272│        // nothing — the slot is ephemeral control, a restart correctly drops it).\n 273│        // `spt shell drive` writes it, `api drive-poll --link` takes-and-clears it.\n## digest.rs\n *1│//! The session-digest **log projection** — the daemon-side, on-demand projection\n  2│//! of an endpoint's live activity buffer from its normalized session logs\n  3│//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n  4│//!\n   │...\n  7│//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n *8│//! on every output chunk. The digest is now a **projection of the same normalized\n  9│//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n*10│//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n*11│//! records via [`project_endpoint_digest`], and the hub publishes the result.\n 12│//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n 13│//! read the logs.\n 14│//!\n   │...\n 20│//!   per-session JSONL is Path A's worked example.\n*21│//! - A **log-less** adapter pushes digest-records directly via `api digest-entry`\n*22│//!   into the perch's `digest.log` → the projection reads that.\n 23│//!\n*24│//! The records carry the published digest-record contract\n*25│//! ([`spt_term::DigestRecord`]); the projection folds them into the structured\n*26│//! [`Digest`] (N-turn window, collapsed tool sprints, ~25-char arg truncation).\n 27│//!\n 28│//! ## Snapshot vs delta\n 29│//!\n 30│//! A projection is diffed against the hub's prior snapshot\n*31│//! ([`crate::digesthub::DigestHub::project_and_publish`]) to produce a\n*32│//! [`DigestUpdate`]: `from` is where the changed turns begin, and the subscriber\n 33│//! applies it by **truncating its view to `from`, then appending `turns`**. The\n*34│//! delta-stream **contract** ships; its deltas are driven by pull / digest-entry\n 35│//! publishes (autonomous file-watch freshness is deferred to the consuming\n*36│//! frontend milestone — CONTEXT.md \"session digest\").\n 37│// [impl->REQ-TERM-4]\n 38│\n 39│use std::collections::BTreeMap;\n   │...\n 43│\n*44│use spt_live::extract_digest;\n*45│use spt_runtime::manifest::Digest as ManifestDigest;\n 46│use spt_store::perch::{self, ParentHint};\n*47│use spt_term::{Digest, DigestConfig, TimelineItem, Turn};\n 48│\n*49│/// Bound for the projection's extractor run (the `[digest]` extractor is a\n 50│/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n*51│const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n 52│\n 53│/// How many recent sessions the spanning projection enumerates from the ledger\n 54│/// (REQ-TERM-6 — \"the last K\"). Bounds the per-session extractor runs; the visible\n*55│/// turn window ([`DigestConfig::window_turns`]) bounds what is shown across them.\n 56│const SPAN_SESSIONS: usize = 5;\n 57│\n 58│/// Consumer-supplied presentation overrides (ADR-0019: an adapter declares\n*59│/// `[digest]` presentation defaults that **any consumer may override** at\n 60│/// pull/subscribe). Every field is optional — unset falls through to the\n## digesthub.rs\n *1│//! The daemon-held digest hub + its control channel (ADR-0008 amendment /\n  2│//! REQ-TERM-4).\n  3│//!\n  4│//! The hub holds the latest projected snapshot per endpoint and fans every change\n *5│//! out to live subscribers. The digest is **projected on demand** from the\n *6│//! endpoint's normalized records ([`project_endpoint_digest`]) — there is no live\n *7│//! feed; a pull or a `digest-entry` push triggers a projection, and\n *8│//! [`DigestHub::project_and_publish`] diffs it against the prior snapshot to\n *9│//! produce the [`DigestUpdate`] delta. The control channel is the access surface\n 10│//! ADR-0008 describes — two modes over one transport:\n 11│//!\n*12│//! - **snapshot pull** — `spt endpoint digest <id>` ([`pull_snapshot`]) projects\n 13│//!   the endpoint's current buffer on demand and returns it;\n*14│//! - **delta-stream** — `spt endpoint digest <id> --follow` ([`follow`])\n 15│//!   subscribes, receives a base snapshot, then **only the changes**\n*16│//!   ([`DigestUpdate`]s) as pulls / `digest-entry` pushes re-project. The stream\n 17│//!   contract ships; autonomous file-watch freshness is deferred to the consuming\n*18│//!   frontend milestone (CONTEXT.md \"session digest\").\n 19│//!\n 20│//! The channel rides the same forward-compatible [`Envelope`] + codec + version\n 21│//! handshake as the broker / seed-control IPC (a transient `spt` CLI connects as a\n   │...\n 33│use serde::{Deserialize, Serialize};\n*34│use spt_term::{Digest, DigestEntry, Turn};\n 35│\n 36│use crate::codec::{read_frame, write_frame};\n*37│use crate::digest::{common_prefix_len, project_endpoint_digest, DigestOverride, DigestUpdate};\n 38│use crate::frame::{Envelope, Role};\n 39│use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};\n 40│\n*41│/// Client→daemon: pull the current structured digest for an endpoint (projects\n 42│/// it on demand from the endpoint's normalized records).\n*43│pub const KIND_DIGEST_SNAPSHOT: &str = \"digest_snapshot\";\n*44│/// Daemon→client: the snapshot reply (digest + version; absent ⇒ no digest).\n*45│pub const KIND_DIGEST_RESULT: &str = \"digest_result\";\n 46│/// Client→daemon: subscribe to an endpoint's delta-stream from a version floor.\n*47│pub const KIND_DIGEST_SUBSCRIBE: &str = \"digest_subscribe\";\n*48│/// Daemon→client: one structured digest change (the streamed delta / base).\n*49│pub const KIND_DIGEST_DELTA: &str = \"digest_delta\";\n 50│/// Client→daemon: re-project an endpoint now and publish any change to live\n*51│/// subscribers — the push-driven delta after an `api digest-entry` append (no\n 52│/// reply; the delta reaches subscribers via the fan-out, REQ-TERM-4).\n## drivehub.rs\n  11│//! **Where the slot lives (broker, not relay; broker, not brain).** The slot is\n *12│//! held by the always-up BROKER process (alongside [`crate::digesthub::DigestHub`]),\n  13│//! the daemon-lifetime CLI-reachable anchor post-ADR-0018 — NOT the restartable\n  14│//! brain child (a CLI cannot reach brain memory; all control sockets are\n  15│//! broker-served) and NOT a file (a file would survive a daemon restart and\n    │...\n  31│//! The channel rides the same forward-compatible [`Envelope`] + codec + version\n *32│//! handshake as the digest / broker / seed-control IPC (a transient `spt` CLI\n  33│//! connects as a [`Role::Brain`] logic client). **Local addressing only** —\n  34│//! cross-node drive over the ephemeral Iroh link is a later task (never spooled,\n  35│//! same drop-on-offline semantics).\n    │...\n 179│/// Bind the drive-control channel at `name` and serve it forever against `hub`.\n*180│/// Thread-per-connection (mirroring the digest / broker / seed-control channels).\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## endpoint.rs\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 45│}\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 51│// [impl->REQ-SHELL-3]\n 52│pub fn drive_socket_name() -> String {\n## harnesshost.rs\n  85│/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh\n *86│/// `claude --session-id {session_id}`, which would start a blank transcript). A\n  87│/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`\n  88│/// (full back-compat); a `false` always uses `[session.self]`.\n  89│// [impl->REQ-HOST-RUN-1]\n    │...\n 223│/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)\n*224│/// passes so the harness reads the RIGHT `.claude`/session history/digest source.\n 225│///\n 226│/// `is_resume`: forwarded to [`prepare_harness_spawn`] — `true` selects the\n 227│/// adapter's `[session.resume]` native-resume command (REQ-SESSION-RESUME-TEMPLATE)\n    │...\n 561│    // runs `claude -r {session_id}`, not the fresh `claude --session-id …` that\n*562│    // would start a blank transcript), and `is_resume=false` fills SELF. Both fill\n 563│    // the SAME {id}/{session_id} catalog.\n 564│    #[test]\n 565│    fn prepare_selects_resume_template_on_resume_else_self() {\n    │...\n 600│        // The two templates produce genuinely different argv (the bug class: a\n*601│        // resume that re-ran the fresh command → blank transcript).\n 602│        assert_ne!(\n 603│            resumed.tokens, fresh.tokens,\n 604│            \"resume and self must tokenize to different commands\"\n## lib.rs\n 121│pub mod deelevate;\n*122│pub mod digest;\n*123│pub mod digesthub;\n 124│pub mod docshost;\n 125│pub mod dispatch;\n 126│pub mod drivehub;\n    │...\n 190│};\n*191│pub use digest::{\n*192│    common_prefix_len, project_endpoint_digest, resolve_config, DigestOverride, DigestUpdate,\n 193│};\n*194│pub use digesthub::{\n*195│    digest_to_json, follow, pull_snapshot, render_digest, render_update, reproject,\n*196│    serve_digest_control, update_to_json, DigestHub,\n 197│};\n 198│pub use effect::{\n 199│    with_tracing_retry, EffectJournal, EffectKey, EffectKind, Outcome, OP_NO_LONGER_HELD_MARKER,\n    │...\n 208│pub use endpoint::{\n*209│    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name,\n 210│    pump_heartbeat_path, read_pump_health, read_pump_heartbeat, seed_socket_name,\n 211│    tunnel_socket_name,\n 212│};\n## lifecycle.rs\n   26│use spt_live::{\n  *27│    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n   28│    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n   29│};\n   30│use spt_runtime::{Manifest, ManifestRuntime};\n     │...\n  753│    /// Fetch history per the manifest `[history]` strategy (empty if none).\n *754│    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n  755│        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n  756│        // history fetch below), so a live-update refresh never waits on it.\n  757│        let manifest = self.cell.manifest_snapshot();\n     │...\n  760│        };\n *761│        fetch_history(\n  762│            h,\n  763│            &self.base_keys(session_id),\n  764│            &self.id,\n     │...\n  858│        };\n *859│        let history = self.history(session_id);\n  860│        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n  861│        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n  862│        // dir so the summarizer child runs where the endpoint lives, never in the\n     │...\n 1002│        // needs no `.live-bin` own-copy (unlike the retired resident spawn) — the\n*1003│        // install-dir/PATH snapshot is correct here (same class as translate/digest).\n 1004│        //\n 1005│        // F-027 Half-B env parity (REQ-PSYCHE-SPAWN-ENV-PARITY): thread the PARENT\n 1006│        // record's captured read-env stamps (the `[env] direction=\"read\"` allowlist,\n     │...\n 2885│    // machinery — a per-event psyche turn is TURN-BOUNDED (same image-lock class as\n*2886│    // translate/digest) and spawns from the install-dir/PATH snapshot, so no\n 2887│    // per-endpoint copy is needed. REQ-INSTALL-11's `unit` stage stays covered by the\n 2888│    // install-dir program-resolution units in spt-runtime (runtime.rs) + harnesshost.\n 2889│}\n## msg.rs\n 280│    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\n*281│    /// consumer (Claude Code) reads its `.claude`, session history, and digest\n 282│    /// source relative to cwd, so a project-scoped launcher MUST place the\n 283│    /// harness in the user's project root, not the daemon's. Additive +\n 284│    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\n## nethost.rs\n 1171│    /// ([`crate::pairhost::respond`]), never registered in the stream table.\n*1172│    /// One endpoint for both is deliberate: the pairing transcript binds and\n 1173│    /// pins the REAL node pubkey, and the relay can only route one endpoint\n 1174│    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).\n 1175│    /// The meet-rotation listener loop rides the same runtime.\n## projwriter.rs\n  416│        for (id, perch_path, origin_cwd) in &targets {\n *417│            let entries = sessions::read_all(perch_path);\n  418│            let store_ids: Vec<String> = self\n  419│                .branch_order\n  420│                .iter()\n     │...\n 1186│            let perch = owlery.join(id);\n*1187│            let entries = sessions::read_all(&perch);\n 1188│            let origin = info::read_info(&perch).and_then(|r| r.cwd);\n 1189│            projderive::project_refs_from(\n 1190│                &entries,\n## propagate.rs\n 328│    /// metadata) carry zero artifact bytes; a post-fetch reject means the\n*329│    /// pulled bytes did not match the signed digest.\n 330│    Rejected(RejectReason),\n 331│}\n 332│\n    │...\n 340│/// [`verify_metadata`] on the offer before any bytes move, then the whole\n*341│/// [`plan_verified`] front door (metadata + artifact digest + classify) on\n 342│/// the real bytes before staging.\n 343│// [impl->REQ-UPD-1]\n 344│// [impl->REQ-UPD-2]\n## registryhost.rs\n 893│fn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\n*894│    let entries = spt_store::sessions::read_all(perch_path);\n 895│    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\n 896│    let mut out: Vec<String> = Vec::new();\n 897│    for cwd in entries.iter().rev().filter_map(|e| e.cwd.as_deref()) {\n## relcache.rs\n  16│//! presence is the commit point, so [`ReleaseCache::staged`] never observes a\n *17│//! metadata file whose artifact is missing or torn mid-write. A digest\n  18│//! mismatch between the two (a corrupted or tampered artifact at rest) is\n  19│//! surfaced to the *peer's* gate as `ArtifactMismatch` — never silently\n  20│//! re-served as healthy (the serve side does not re-verify; the receive side\n    │...\n 263│    /// metadata last (commit point). The caller MUST have verified that the\n*264│    /// provided bytes match the signed digests.\n 265│    // [impl->REQ-UPD-6]\n 266│    pub fn stage_update_set(\n 267│        &self,\n## release.rs\n   34│use serde::{Deserialize, Serialize};\n  *35│use sha2::{Digest, Sha256};\n   36│\n   37│use crate::update::ReleaseSpec;\n   38│\n     │...\n   92│/// the Rust target triple (e.g. `x86_64-pc-windows-msvc`); this entry carries\n  *93│/// the digest and update-class axes for that platform's binary.\n   94│// [impl->REQ-UPD-6]\n   95│#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n   96│pub struct UpdateArtifactMetadata {\n     │...\n  191│/// A signed update set: exact signed metadata bytes plus detached Ed25519\n *192│/// signature. The signature covers all platform artifact digests.\n  193│// [impl->REQ-UPD-6]\n  194│#[derive(Debug, Clone, Serialize, Deserialize)]\n  195│pub struct SignedUpdateSet {\n     │...\n  441│    Rollback { current: u64, candidate: u64 },\n *442│    /// The actual binary's digest does not match the signed `artifact_sha256`.\n  443│    ArtifactMismatch { signed: String, actual: String },\n  444│    /// A signed update set has no artifact for this node's platform.\n  445│    NoArtifactForPlatform { platform: String },\n     │...\n  472│                    f,\n *473│                    \"artifact digest mismatch: signed {signed}, actual {actual}\"\n  474│                )\n  475│            }\n  476│            RejectReason::NoArtifactForPlatform { platform } => {\n     │...\n  488│/// [`verify_artifact`] with the downloaded binary next (the signature binds it\n *489│/// by digest, but the digest must still be checked against the real bytes).\n  490│///\n  491│/// Check order is deliberate: identify + authenticate the signer *first* (an\n  492│/// unknown/revoked key or a bad signature is rejected before any field of the\n     │...\n  643│/// ([`REQ-UPD-2`]). Run after [`verify_metadata`] so the signature has already\n *644│/// attested to the expected digest — this closes the loop to the real bytes.\n  645│// [impl->REQ-UPD-2]\n  646│pub fn verify_artifact(meta: &ReleaseMetadata, artifact: &[u8]) -> Result<(), RejectReason> {\n  647│    let actual = sha256_hex(artifact);\n     │...\n  657│\n *658│/// Confirm downloaded docs-bundle bytes match the digest inside a signed\n  659│/// update set. The caller treats a mismatch as SKIP-the-docs, never as a\n  660│/// release rejection (ADR-0036 §4 failure isolation).\n  661│// [impl->REQ-DOCS-RELEASE-ASSET]\n     │...\n  679│\n *680│/// Confirm this platform's downloaded bytes match the digest inside a signed\n  681│/// update set.\n  682│// [impl->REQ-UPD-6]\n  683│pub fn verify_update_set_artifact(\n     │...\n  699│\n *700│/// Lowercase-hex SHA-256 of `bytes` — the artifact-digest format used in\n  701│/// [`ReleaseMetadata::artifact_sha256`].\n  702│pub fn sha256_hex(bytes: &[u8]) -> String {\n  703│    let mut hasher = Sha256::new();\n     │...\n  931│\n *932│        // Mismatched bytes reject with the digest pair.\n  933│        match verify_update_set_docs(&parsed, b\"TAMPERED\") {\n  934│            Err(RejectReason::ArtifactMismatch { signed, actual }) => {\n  935│                assert_eq!(signed, sha256_hex(bundle));\n     │...\n 1006│    // [unit->REQ-UPD-2] a correctly-signed, newer, in-channel, unexpired release\n*1007│    // from a trusted key verifies, and its bytes match the signed digest.\n 1008│    #[test]\n 1009│    fn valid_release_verifies_and_artifact_matches() {\n 1010│        let sk = signing_key(7);\n     │...\n 1015│        assert_eq!(meta.version, 6);\n*1016│        verify_artifact(&meta, artifact).expect(\"artifact digest matches the signed value\");\n 1017│        assert_eq!(meta.spec().broker_resource_abi, BROKER_RESOURCE_ABI);\n 1018│    }\n 1019│\n     │...\n 1126│\n*1127│    // [unit->REQ-UPD-2] a binary whose bytes do not hash to the signed digest is\n 1128│    // rejected — the signature attests to specific bytes, not just a manifest.\n 1129│    #[test]\n*1130│    fn artifact_digest_mismatch_is_rejected() {\n 1131│        let sk = signing_key(7);\n 1132│        let signed = sign_release(&sk, &meta_for(6, b\"the signed bytes\"));\n 1133│        let meta = verify_metadata(&signed, &policy(&sk)).expect(\"metadata verifies\");\n*1134│        // A different binary than the one whose digest was signed.\n 1135│        let tampered = b\"DIFFERENT bytes than were signed\";\n 1136│        assert!(matches!(\n 1137│            verify_artifact(&meta, tampered),\n     │...\n 1154│        verify_update_set_artifact(&meta, \"x86_64-unknown-linux-gnu\", b\"linux bytes\")\n*1155│            .expect(\"linux artifact digest matches\");\n 1156│        verify_update_set_artifact(&meta, \"x86_64-pc-windows-msvc\", b\"win bytes\")\n*1157│            .expect(\"windows artifact digest matches\");\n 1158│        assert_eq!(\n 1159│            meta.provenance.as_ref().unwrap().build_host.as_deref(),\n 1160│            Some(\"fastbox\")\n     │...\n 1180│\n*1181│    // [unit->REQ-UPD-6] the whole set is signed: modifying any platform digest\n 1182│    // after signing invalidates the single set signature.\n 1183│    #[test]\n 1184│    fn update_set_tampering_breaks_whole_set_signature() {\n     │...\n 1221│\n*1222│    // [unit->REQ-UPD-6] a bytes/digest mismatch on the selected platform is\n 1223│    // rejected even though the set metadata itself verifies.\n 1224│    #[test]\n 1225│    fn update_set_artifact_mismatch_is_rejected() {\n## seedproofx.rs\n   5│//! [`seedproof`](spt_net::net::mesh::seedproof) is the pure crypto (membership\n  *6│//! key + mutual channel-bound transcript). This module runs the **mutual\n   7│//! exchange** over a dedicated control stream at every dial + accept, *before*\n   8│//! the connection is registered as usable: each side proves current-epoch\n   9│//! knowledge of the seed for **every subnet the two nodes share**, and the\n    │...\n  35│//! connection drops (exact-epoch; the N-1 re-seed grace is Mesh-D7). Nonces are\n *36│//! per-connection-fresh and reused across the per-subnet proofs (the transcript\n  37│//! already binds `subnet_id` + `seed_epoch`).\n  38│// [impl->REQ-MESH-1]\n  39│\n    │...\n  44│use spt_net::net::mesh::seedproof::{\n *45│    MembershipKey, ProofRole, SeedProofFrame, SeedProofTranscript, NONCE_LEN,\n  46│};\n  47│use spt_proto::identity::{Identity, PublicKey};\n  48│use spt_store::peeraddrs::PeerAddrStore;\n    │...\n 267│    let remote_hex = remote.to_hex();\n*268│    // Channel binding: the transcript pins both handshake-proven pubkeys in a\n 269│    // role-fixed order (dialer first), so a tag is valid on exactly this conn.\n 270│    let (dialer_pub, acceptor_pub) = match role {\n 271│        ProofRole::Dialer => (local_id, remote),\n    │...\n 312│/// One subnet's local generations, as the proof exchange needs them: the\n*313│/// channel-bound transcript + membership key for the current generation and\n 314│/// (one-deep) the prior one (Mesh-D7). The outbound proof set is one tag per\n 315│/// generation; the inbound is graded against both.\n 316│struct GenProof {\n    │...\n 318│    mk: MembershipKey,\n*319│    transcript: SeedProofTranscript,\n 320│}\n 321│\n 322│struct LocalGens {\n    │...\n 331│    fn outbound(&self, mine: ProofRole) -> Vec<(u64, [u8; 32])> {\n*332│        let mut out = vec![(self.current.epoch, self.current.transcript.tag(&self.current.mk, mine))];\n 333│        if let Some(p) = &self.prev {\n*334│            out.push((p.epoch, p.transcript.tag(&p.mk, mine)));\n 335│        }\n 336│        out\n 337│    }\n    │...\n 340│    /// in the peer's role `prover`. A tag is matched only at the generation whose\n*341│    /// epoch it carries — the transcript binds the epoch, so a tag for one\n 342│    /// generation can never satisfy another.\n 343│    fn matches(&self, peer_proofs: &[(u64, [u8; 32])], prover: ProofRole) -> (bool, bool) {\n 344│        let mc = peer_proofs.iter().any(|(e, t)| {\n*345│            *e == self.current.epoch && self.current.transcript.verify(&self.current.mk, prover, t)\n 346│        });\n 347│        let mp = self.prev.as_ref().is_some_and(|p| {\n 348│            peer_proofs\n 349│                .iter()\n*350│                .any(|(e, t)| *e == p.epoch && p.transcript.verify(&p.mk, prover, t))\n 351│        });\n 352│        (mc, mp)\n 353│    }\n    │...\n 369│        mk: MembershipKey::derive(seed, name, epoch),\n*370│        transcript: SeedProofTranscript {\n 371│            subnet_id: name.to_string(),\n 372│            seed_epoch: epoch,\n 373│            dialer_pub,\n## shellhost.rs\n 28│use hmac::{Hmac, Mac};\n*29│use sha2::{Digest, Sha256};\n 30│\n 31│use spt_runtime::manifest::{AdapterKind, CommandReceipt, Manifest, Shell};\n 32│use spt_runtime::registry::AdapterRecord;"},"isError":false,"timestamp":1784271537232}}
{"type":"message","id":"6ef5fa59","parentId":"f25372db","timestamp":"2026-07-17T06:58:57.248Z","message":{"role":"toolResult","toolCallId":"call_AyPgghNIh3BBFyNmQj7WFLbJ|fc_038f28d3e93bbfd4016a59d2b118948196a483d8f408f2ddd9","toolName":"read","content":[{"type":"text","text":"pub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now(), breaker_window),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Bounded redispatch worker pool (ADR-0038 Amendment fix 7): a cold stream\n/// table claims at most this many rows per poll tick.\nconst MAX_DISPATCH_WORKERS: usize = 8;\n\n/// PURE pool gate (Amendment fix 7): may the loop claim another row right\n/// now? The cold-enumeration break rides this — a refresh with a large\n/// backlog claims at most [`MAX_DISPATCH_WORKERS`] rows per poll, the rest\n/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn pool_has_room(active: usize) -> bool {\n    active < MAX_DISPATCH_WORKERS\n}\n\n[Showing lines 429-593 of 1558. Use :594 to continue]"}],"details":{"truncation":{"content":"pub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now(), breaker_window),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Bounded redispatch worker pool (ADR-0038 Amendment fix 7): a cold stream\n/// table claims at most this many rows per poll tick.\nconst MAX_DISPATCH_WORKERS: usize = 8;\n\n/// PURE pool gate (Amendment fix 7): may the loop claim another row right\n/// now? The cold-enumeration break rides this — a refresh with a large\n/// backlog claims at most [`MAX_DISPATCH_WORKERS`] rows per poll, the rest\n/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn pool_has_room(active: usize) -> bool {\n    active < MAX_DISPATCH_WORKERS\n}","truncated":true,"truncatedBy":"lines","totalLines":1130,"totalBytes":8158,"outputLines":165,"outputBytes":8158,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn run_dispatch_loop(\n    broker_name: &str,\n    paths: &DispatchPaths,\n    registry: Arc<RegistryHost>,\n    stop: &AtomicBool,\n    poll: Duration,\n) -> io::Result<()> {\n    let mut brain = connect(broker_name)?;\n    // Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable —\n    // replaces the pre-fix one-shot `HashSet` whose burned claims permanently\n    // abandoned a stream on the first transient failure.\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now(), breaker_window),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Bounded redispatch worker pool (ADR-0038 Amendment fix 7): a cold stream\n/// table claims at most this many rows per poll tick.\nconst MAX_DISPATCH_WORKERS: usize = 8;\n\n/// PURE pool gate (Amendment fix 7): may the loop claim another row right\n/// now? The cold-enumeration break rides this — a refresh with a large\n/// backlog claims at most [`MAX_DISPATCH_WORKERS`] rows per poll, the rest\n/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn pool_has_room(active: usize) -> bool {\n    active < MAX_DISPATCH_WORKERS\n}","startLine":429,"lineNumbers":[429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1558,"totalBytes":8158,"outputLines":165,"outputBytes":8158,"shownRange":{"start":429,"end":593},"nextOffset":594}}},"isError":false,"timestamp":1784271537248}}
{"type":"message","id":"6287b113","parentId":"6ef5fa59","timestamp":"2026-07-17T06:58:57.268Z","message":{"role":"toolResult","toolCallId":"call_ZBOnlNGNUFzIbZ9qdI0qEln6|fc_038f28d3e93bbfd4016a59d2b1188c8196b455a0eb1e54cfe9","toolName":"read","content":[{"type":"text","text":"use crate::endpoint::{brain_ready_path, broker_socket_name};\nuse crate::relcache::{AppliedRecord, ReleaseCache};\n\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\n/// never a hot respawn loop.\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n/// A run at least this long counts as healthy and resets the backoff floor.\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n/// brain has this long to signal `ready` for its generation before the window\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n/// **exits before** signaling ready is respawned this many consecutive times\n/// (a transient first-boot hiccup should not abandon a good binary) before the\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n/// window-expiry triggers rollback.\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// How long the brain child retries connecting to the broker socket before\n/// giving up (the broker may still be binding — the child is freshly spawned).\nconst BRAIN_CONNECT_TRIES: u32 = 300;\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\n/// reliable than a brain→brain frame, which retires in D4).\n///\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\n/// never a parse failure that would brick the brain before it rehydrates.\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum StartReason {\n    /// The broker's first brain spawn this broker lifetime.\n    Cold,\n    /// A supervised respawn after an unexpected brain exit.\n…\n}\n\n[Showing lines 34-78 of 2116. Use :79 to continue]"}],"details":{"truncation":{"content":"use crate::endpoint::{brain_ready_path, broker_socket_name};\r\nuse crate::relcache::{AppliedRecord, ReleaseCache};\r\n\r\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\r\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\r\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\r\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\r\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\r\n/// never a hot respawn loop.\r\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\r\n/// A run at least this long counts as healthy and resets the backoff floor.\r\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\r\n/// brain has this long to signal `ready` for its generation before the window\r\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\r\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\r\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\r\n/// **exits before** signaling ready is respawned this many consecutive times\r\n/// (a transient first-boot hiccup should not abandon a good binary) before the\r\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\r\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\r\n/// window-expiry triggers rollback.\r\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\r\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\r\nconst TICK: Duration = Duration::from_millis(200);\r\n\r\n/// How long the brain child retries connecting to the broker socket before\r\n/// giving up (the broker may still be binding — the child is freshly spawned).\r\nconst BRAIN_CONNECT_TRIES: u32 = 300;\r\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\r\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\r\n\r\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\r\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\r\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\r\n/// reliable than a brain→brain frame, which retires in D4).\r\n///\r\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\r\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\r\n/// never a parse failure that would brick the brain before it rehydrates.\r\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\r\npub enum StartReason {\r\n    /// The broker's first brain spawn this broker lifetime.\r\n    Cold,\r\n    /// A supervised respawn after an unexpected brain exit.\r","truncated":true,"truncatedBy":"lines","totalLines":2083,"totalBytes":2682,"outputLines":45,"outputBytes":2682,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"use crate::endpoint::{brain_ready_path, broker_socket_name};\r\nuse crate::relcache::{AppliedRecord, ReleaseCache};\r\n\r\n/// Supervisor backoff floor after a brain death (mirrors the peer-pump\r\n/// supervisor, M8 decision 23): a healthy run resets to this; a fast death\r\n/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].\r\nconst SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);\r\n/// Backoff ceiling — a persistently-crashing brain stays visible and bounded,\r\n/// never a hot respawn loop.\r\nconst SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\r\n/// A run at least this long counts as healthy and resets the backoff floor.\r\n/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\r\n/// brain has this long to signal `ready` for its generation before the window\r\n/// elapses (the \"up but useless\" alive-never-ready rollback path).\r\nconst SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\r\n/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\r\n/// **exits before** signaling ready is respawned this many consecutive times\r\n/// (a transient first-boot hiccup should not abandon a good binary) before the\r\n/// supervisor auto-rolls-back to the last-known-good binary. Matched to the\r\n/// capped-backoff cadence; either this budget OR an alive-but-never-ready\r\n/// window-expiry triggers rollback.\r\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\r\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\r\nconst TICK: Duration = Duration::from_millis(200);\r\n\r\n/// How long the brain child retries connecting to the broker socket before\r\n/// giving up (the broker may still be binding — the child is freshly spawned).\r\nconst BRAIN_CONNECT_TRIES: u32 = 300;\r\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\r\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\r\n\r\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\r\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\r\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\r\n/// reliable than a brain→brain frame, which retires in D4).\r\n///\r\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\r\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\r\n/// never a parse failure that would brick the brain before it rehydrates.\r\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\r\npub enum StartReason {\r\n    /// The broker's first brain spawn this broker lifetime.\r\n    Cold,\r\n    /// A supervised respawn after an unexpected brain exit.\r\n…\n}","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,null,82]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":2682,"outputLines":45,"outputBytes":2682,"shownRange":{"start":34,"end":78},"nextOffset":79}}},"isError":false,"timestamp":1784271537268}}
{"type":"message","id":"8598969c","parentId":"6287b113","timestamp":"2026-07-17T06:59:10.115Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting project ID derivation from git**"},{"type":"toolCall","id":"call_psLfL2MBdt1pi2SyL8A6J2KL|fc_038f28d3e93bbfd4016a59d2be02508196a8f56b7f0ba4b81b","name":"read","arguments":{"i":"Reading project derivation IO","path":"crates/spt-store/src/project.rs","selector":"1-240"}},{"type":"toolCall","id":"call_vxlcm4KGFpBwUaK15wnvpy7L|fc_038f28d3e93bbfd4016a59d2be02708196bbb52c2e0468a14f","name":"read","arguments":{"i":"Reading project ref derivation","path":"crates/spt-store/src/projderive.rs","selector":"1-260"}},{"type":"toolCall","id":"call_rBDqnkNGujYy897xf8mrgWn6|fc_038f28d3e93bbfd4016a59d2be0278819697294527a1d9d928","name":"grep","arguments":{"i":"Finding project function definitions","pattern":"fn project_id_and_display_for_dir|fn project_id_for_dir|fn git_root|read_to_string|canonicalize","path":"crates/spt-store/src/project.rs; crates/spt-store/src/projderive.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_6ITWu3IrpHC1zOd6j8TY6BeT|fc_038f28d3e93bbfd4016a59d2be02808196ae97381acd621481","name":"grep","arguments":{"i":"Finding branch tree scan command","pattern":"pub fn ls_files_at|fn ls_files_at|ls-tree","path":"crates/spt-store/src/branchstore.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_XcmUyCIoxrFfZq9OPdXEVISj|fc_038f28d3e93bbfd4016a59d2be02848196964169e6427d665e","name":"grep","arguments":{"i":"Finding adapter registry scan","pattern":"pub fn registered|fn registered|read_dir|read_to_string|manifest.toml","path":"crates/spt-runtime/src/registry.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_vmE6gbB6NSga5sNCGwqn9JSP|fc_038f28d3e93bbfd4016a59d2be028881968bf4236f6360182d","name":"read","arguments":{"i":"Reading pulse gate semantics","path":"crates/spt-live/src/pulse.rs","selector":"1-120"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":19123,"output":386,"cacheRead":161280,"cacheWrite":0,"totalTokens":180789,"reasoningTokens":48,"cost":{"input":0.095615,"output":0.01158,"cacheRead":0.08063999999999999,"cacheWrite":0,"total":0.187835}},"stopReason":"toolUse","timestamp":1784271537467,"responseId":"resp_038f28d3e93bbfd4016a59d2b1f3c88196979395890a293e65","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2b6614c81968cc120961e1a3e28","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdK-jh_U-tvkzRmv52GiUao6NOmGP9BScmxSDxaO8r1IrO4HAWjRebwK2UYQYEmYZfppklvgk13M1TzQdK3regB-s8ehtuiQx0yP2jA0TyA9h0RDU5madvHA7vfRJgWKMXXohO6gZu-08zAXPDk4ifaJNogtKSfMLPQbCP6x6SGz71nWpUC7zK9sb_WdhlGTYbl-ky0ivPR3a0OgCurGHNNO74a81jlyltJeklKMnxhB-u2p0dSVGN4j4O8UR9Axd1iQLoYuVjnqpfVYQ5z1tkxDJOiV4_IX16kiHSFn4Il8W6qlCxJbOzeietoCfz7zGze1oL2EYkB1Aye_mFi7FF4QPEek4Dql7RSkw_mEDuAmMepi5lbO2cB2_jXSQtYY7DBzPOl8bRvI_xdzO69ybORfP5iH5X5W5JJNqcao8jxLz9EJwIAZiGVrM8xjaSgBp6haI3dp_GhZtuGehiaoHvMCheO_UCbTEzxRgrDuyx2oButFmMI3oYFj_25sARDShHAqeBW9C73SRHSdSVJPkcExGbJ9wg2MzgkgPD8cXV_Xzy88ejHGYtL-Vo_g6SA3z5i3I2s_nkcL9o4A6NdA1JioU36qIT3jACM_5jX3rP2Us-qMF_fFIe_tDaDuTysYh6QMU5us8nOf5cf1B8DRu7DZ3nm73H-718_LBwxhUG_vdNotuCC5ohY4Dtm4lhTQUnRJdGAQQvc23xZ-k11vnlLJoS3R0GXFXz97qzi80x_UFCRod1fju79xbEeHsORTBIPPCwsYVpgkVH6MFCIZyvfPXsuOrI4ITylPJP9WUk6wnBWtrOYXmgNwEiubzwhZVuiguuqJzMnEOJTuqnt_cTWcMqjvcrctNUfATAcylybVpaLWqp5P3aSaV-Z4zIlA2ZJg0mgp1sCSpw-fXumgz3w5fn6I5jRnez2fsScGS2jfMtT78oqJPmvcjpkATRqLbseZA_UaVVqTVYylwJf9LT8EaHgeNpXwgP6j_lHIXE1gJzATqBb4hEGUdUvXFdnMJnUYRki5Ym8sM3YzJKBCdKKBkCujMvrr6B-vfeIXBt6nUzLR8Xvolqc9bZ2boauIZMnRvs5RMl0vC_Guhk__H6vCLaceLsSNnm8llI3a3Bpckfd49i38NdGDlmOjJWIHuZq-d-pmIjNwuO23_yJkDeITl09XkudP70uO7DbAuUpVcP0=","summary":[{"type":"summary_text","text":"**Inspecting project ID derivation from git**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2be02508196a8f56b7f0ba4b81b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading project derivation IO\",\"path\":\"crates/spt-store/src/project.rs\",\"selector\":\"1-240\"}","call_id":"call_psLfL2MBdt1pi2SyL8A6J2KL","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2be02708196bbb52c2e0468a14f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading project ref derivation\",\"path\":\"crates/spt-store/src/projderive.rs\",\"selector\":\"1-260\"}","call_id":"call_vxlcm4KGFpBwUaK15wnvpy7L","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2be0278819697294527a1d9d928","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding project function definitions\",\"pattern\":\"fn project_id_and_display_for_dir|fn project_id_for_dir|fn git_root|read_to_string|canonicalize\",\"path\":\"crates/spt-store/src/project.rs; crates/spt-store/src/projderive.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_rBDqnkNGujYy897xf8mrgWn6","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2be02808196ae97381acd621481","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding branch tree scan command\",\"pattern\":\"pub fn ls_files_at|fn ls_files_at|ls-tree\",\"path\":\"crates/spt-store/src/branchstore.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_6ITWu3IrpHC1zOd6j8TY6BeT","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2be02848196964169e6427d665e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding adapter registry scan\",\"pattern\":\"pub fn registered|fn registered|read_dir|read_to_string|manifest.toml\",\"path\":\"crates/spt-runtime/src/registry.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_XcmUyCIoxrFfZq9OPdXEVISj","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2be028881968bf4236f6360182d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading pulse gate semantics\",\"path\":\"crates/spt-live/src/pulse.rs\",\"selector\":\"1-120\"}","call_id":"call_vmE6gbB6NSga5sNCGwqn9JSP","name":"read"}]},"duration":12646.980599999428,"ttft":4925.32310000062,"contextSnapshot":{"promptTokens":180403,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_psLfL2MBdt1pi2SyL8A6J2KL|fc_038f28d3e93bbfd4016a59d2be02508196a8f56b7f0ba4b81b","toolName":"read","startedAt":"2026-07-17T06:59:10.116Z","args":{"path":"crates/spt-store/src/project.rs"},"intent":"Reading project derivation IO"},"id":"b57066b3","parentId":"8598969c","timestamp":"2026-07-17T06:59:10.116Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vxlcm4KGFpBwUaK15wnvpy7L|fc_038f28d3e93bbfd4016a59d2be02708196bbb52c2e0468a14f","toolName":"read","startedAt":"2026-07-17T06:59:10.116Z","args":{"path":"crates/spt-store/src/projderive.rs"},"intent":"Reading project ref derivation"},"id":"095b0ae6","parentId":"b57066b3","timestamp":"2026-07-17T06:59:10.116Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_rBDqnkNGujYy897xf8mrgWn6|fc_038f28d3e93bbfd4016a59d2be0278819697294527a1d9d928","toolName":"grep","startedAt":"2026-07-17T06:59:10.116Z","args":{"path":"crates/spt-store/src/project.rs; crates/spt-store/src/projderive.rs"},"intent":"Finding project function definitions"},"id":"d55c1ab0","parentId":"095b0ae6","timestamp":"2026-07-17T06:59:10.116Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6ITWu3IrpHC1zOd6j8TY6BeT|fc_038f28d3e93bbfd4016a59d2be02808196ae97381acd621481","toolName":"grep","startedAt":"2026-07-17T06:59:10.116Z","args":{"path":"crates/spt-store/src/branchstore.rs"},"intent":"Finding branch tree scan command"},"id":"2260aab6","parentId":"d55c1ab0","timestamp":"2026-07-17T06:59:10.116Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XcmUyCIoxrFfZq9OPdXEVISj|fc_038f28d3e93bbfd4016a59d2be02848196964169e6427d665e","toolName":"grep","startedAt":"2026-07-17T06:59:10.117Z","args":{"path":"crates/spt-runtime/src/registry.rs"},"intent":"Finding adapter registry scan"},"id":"fbec127b","parentId":"2260aab6","timestamp":"2026-07-17T06:59:10.117Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vmE6gbB6NSga5sNCGwqn9JSP|fc_038f28d3e93bbfd4016a59d2be028881968bf4236f6360182d","toolName":"read","startedAt":"2026-07-17T06:59:10.117Z","args":{"path":"crates/spt-live/src/pulse.rs"},"intent":"Reading pulse gate semantics"},"id":"73714c7e","parentId":"fbec127b","timestamp":"2026-07-17T06:59:10.117Z"}
{"type":"message","id":"812cd896","parentId":"73714c7e","timestamp":"2026-07-17T06:59:10.118Z","message":{"role":"toolResult","toolCallId":"call_6ITWu3IrpHC1zOd6j8TY6BeT|fc_038f28d3e93bbfd4016a59d2be02808196ae97381acd621481","toolName":"grep","content":[{"type":"text","text":" 345|    // [impl->REQ-NET-3]\n*346|    pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {\n 347|        let out = run_git_ok(\n 348|            &[\n 349|                \"-C\",\n 350|                &self.dir_arg(),\n*351|                \"ls-tree\",\n 352|                \"-r\",\n 353|                \"--name-only\",\n 354|                commitish,"}],"details":{"scopePath":"crates/spt-store/src/branchstore.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\branchstore.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":2,"fileCount":1,"files":["crates/spt-store/src/branchstore.rs"],"fileMatches":[{"path":"crates/spt-store/src/branchstore.rs","count":2}],"truncated":false,"displayContent":" 345│    // [impl->REQ-NET-3]\n*346│    pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {\n 347│        let out = run_git_ok(\n 348│            &[\n 349│                \"-C\",\n 350│                &self.dir_arg(),\n*351│                \"ls-tree\",\n 352│                \"-r\",\n 353│                \"--name-only\",\n 354│                commitish,"},"isError":false,"timestamp":1784271550118}}
{"type":"message","id":"20386722","parentId":"812cd896","timestamp":"2026-07-17T06:59:10.120Z","message":{"role":"toolResult","toolCallId":"call_XcmUyCIoxrFfZq9OPdXEVISj|fc_038f28d3e93bbfd4016a59d2be02848196964169e6427d665e","toolName":"grep","content":[{"type":"text","text":" 25|//!\n*26|//! Layout per adapter: `adapters/<name>/record.toml` (+ `manifest.toml` in\n 27|//! copy mode). Records write atomically (REQ-HAZARD-EBUSY-RENAME stance via\n 28|//! `spt_store::atomic`).\n 29|// [impl->REQ-INSTALL-4]\n...\n 150|fn manifest_copy_file(adapters_dir: &Path, name: &str) -> PathBuf {\n*151|    adapters_dir.join(name).join(\"manifest.toml\")\n 152|}\n 153|\n*154|/// Resolve the manifest file inside a source: `<dir>/manifest.toml`, or the\n 155|/// path itself when it already names a file.\n 156|fn source_manifest_file(source: &Path) -> PathBuf {\n 157|    if source.is_dir() {\n*158|        source.join(\"manifest.toml\")\n 159|    } else {\n 160|        source.to_path_buf()\n 161|    }\n...\n 219|    };\n*220|    match std::fs::read_to_string(&full) {\n 221|        Ok(contents) => Some(Value::String(contents)),\n 222|        Err(e) => {\n 223|            eprintln!(\n...\n 278|    std::fs::create_dir_all(dst)?;\n*279|    for entry in std::fs::read_dir(src)? {\n 280|        let entry = entry?;\n 281|        let from = entry.path();\n 282|        let to = dst.join(entry.file_name());\n...\n 292|/// Register (or re-register) the adapter whose manifest lives at `source`\n*293|/// (a directory containing `manifest.toml`, or the manifest path itself).\n 294|/// Validate-first: an invalid manifest records **nothing**. Mode falls out of\n 295|/// the `[update]` avenue — `delegated` ⇒ pointer, `file_pull`/absent ⇒ copy.\n 296|/// Re-adding an existing name refreshes the record and re-activates a\n...\n 319|    let manifest_file = source_manifest_file(source);\n*320|    let toml_src = std::fs::read_to_string(&manifest_file)?;\n 321|    let manifest = Manifest::from_toml_str(&toml_src).map_err(RegistryError::Manifest)?;\n 322|\n 323|    // F-5 (REQ-ADAPTER-FLOOR-ENFORCE): refuse BEFORE writing anything if the installed\n...\n 433|    }\n*434|    let s = std::fs::read_to_string(&path)?;\n 435|    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))\n 436|}\n 437|\n...\n 455|    };\n*456|    let s = std::fs::read_to_string(&path).map_err(|e| {\n 457|        // A POINTER-mode adapter whose source manifest isn't there yet is the\n 458|        // common deferred-install case (the binary/manifest is fetched/extracted\n 459|        // out of band — delegated / GhReleaseManaged). Surface it as an\n...\n 479|pub fn all_records(adapters_dir: &Path) -> Vec<AdapterRecord> {\n*480|    let Ok(entries) = std::fs::read_dir(adapters_dir) else {\n 481|        return Vec::new();\n 482|    };\n 483|    let mut out: Vec<AdapterRecord> = entries\n...\n 495|// [impl->REQ-INSTALL-4]\n*496|pub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {\n 497|    all_records(adapters_dir)\n 498|        .into_iter()\n 499|        .filter(|r| r.active)\n...\n 561|    }\n*562|    let s = std::fs::read_to_string(&path)?;\n 563|    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(format!(\"profiles/{profile}\")))\n 564|}\n 565|\n...\n 568|pub fn local_profile_names(adapters_dir: &Path, adapter: &str) -> Vec<String> {\n*569|    let Ok(entries) = std::fs::read_dir(local_profiles_dir(adapters_dir, adapter)) else {\n 570|        return Vec::new();\n 571|    };\n 572|    let mut out: Vec<String> = entries\n...\n 900|        std::fs::create_dir_all(&src).unwrap();\n*901|        std::fs::write(src.join(\"manifest.toml\"), toml_src).unwrap();\n 902|        src\n 903|    }\n 904|\n...\n 948|        assert!(\n*949|            adapters.join(\"mock-harness\").join(\"manifest.toml\").exists(),\n 950|            \"copy held\"\n 951|        );\n 952|\n...\n 955|        assert!(\n*956|            !adapters.join(\"mock-shell\").join(\"manifest.toml\").exists(),\n 957|            \"no copy\"\n 958|        );\n 959|        // Pointer reads live: mutate the source, the loaded manifest follows.\n 960|        std::fs::write(\n*961|            shell_src.join(\"manifest.toml\"),\n 962|            SHELL_DELEGATED.replace(\"1.0.0\", \"1.1.0\"),\n 963|        )\n 964|        .unwrap();\n...\n 1052|        assert!(\n*1053|            adapters.join(\"equalfloor\").join(\"manifest.toml\").exists(),\n 1054|            \"a copy-mode admit holds the manifest copy\"\n 1055|        );\n 1056|    }\n...\n 1132|        // Simulate the deferred/un-extracted state: the source manifest vanishes.\n*1133|        std::fs::remove_file(shell_src.join(\"manifest.toml\")).unwrap();\n 1134|\n 1135|        // load_manifest → actionable DeferredManifest naming the adapter, NOT Io.\n 1136|        match load_manifest(&adapters, &rec) {\n...\n 1155|    /// Register the profiled shell adapter into a fresh `adapters/` dir.\n*1156|    fn registered_shell(tmp: &Path) -> PathBuf {\n 1157|        let adapters = tmp.join(\"adapters\");\n 1158|        let src = seed_source(tmp, \"ms-src\", SHELL_COPY_PROFILED);\n 1159|        register(&adapters, &src, 1000).unwrap();"}],"details":{"scopePath":"crates/spt-runtime/src/registry.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-runtime\\src\\registry.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":21,"fileCount":1,"files":["crates/spt-runtime/src/registry.rs"],"fileMatches":[{"path":"crates/spt-runtime/src/registry.rs","count":21}],"truncated":false,"displayContent":"   25│//!\n  *26│//! Layout per adapter: `adapters/<name>/record.toml` (+ `manifest.toml` in\n   27│//! copy mode). Records write atomically (REQ-HAZARD-EBUSY-RENAME stance via\n   28│//! `spt_store::atomic`).\n   29│// [impl->REQ-INSTALL-4]\n     │...\n  150│fn manifest_copy_file(adapters_dir: &Path, name: &str) -> PathBuf {\n *151│    adapters_dir.join(name).join(\"manifest.toml\")\n  152│}\n  153│\n *154│/// Resolve the manifest file inside a source: `<dir>/manifest.toml`, or the\n  155│/// path itself when it already names a file.\n  156│fn source_manifest_file(source: &Path) -> PathBuf {\n  157│    if source.is_dir() {\n *158│        source.join(\"manifest.toml\")\n  159│    } else {\n  160│        source.to_path_buf()\n  161│    }\n     │...\n  219│    };\n *220│    match std::fs::read_to_string(&full) {\n  221│        Ok(contents) => Some(Value::String(contents)),\n  222│        Err(e) => {\n  223│            eprintln!(\n     │...\n  278│    std::fs::create_dir_all(dst)?;\n *279│    for entry in std::fs::read_dir(src)? {\n  280│        let entry = entry?;\n  281│        let from = entry.path();\n  282│        let to = dst.join(entry.file_name());\n     │...\n  292│/// Register (or re-register) the adapter whose manifest lives at `source`\n *293│/// (a directory containing `manifest.toml`, or the manifest path itself).\n  294│/// Validate-first: an invalid manifest records **nothing**. Mode falls out of\n  295│/// the `[update]` avenue — `delegated` ⇒ pointer, `file_pull`/absent ⇒ copy.\n  296│/// Re-adding an existing name refreshes the record and re-activates a\n     │...\n  319│    let manifest_file = source_manifest_file(source);\n *320│    let toml_src = std::fs::read_to_string(&manifest_file)?;\n  321│    let manifest = Manifest::from_toml_str(&toml_src).map_err(RegistryError::Manifest)?;\n  322│\n  323│    // F-5 (REQ-ADAPTER-FLOOR-ENFORCE): refuse BEFORE writing anything if the installed\n     │...\n  433│    }\n *434│    let s = std::fs::read_to_string(&path)?;\n  435│    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))\n  436│}\n  437│\n     │...\n  455│    };\n *456│    let s = std::fs::read_to_string(&path).map_err(|e| {\n  457│        // A POINTER-mode adapter whose source manifest isn't there yet is the\n  458│        // common deferred-install case (the binary/manifest is fetched/extracted\n  459│        // out of band — delegated / GhReleaseManaged). Surface it as an\n     │...\n  479│pub fn all_records(adapters_dir: &Path) -> Vec<AdapterRecord> {\n *480│    let Ok(entries) = std::fs::read_dir(adapters_dir) else {\n  481│        return Vec::new();\n  482│    };\n  483│    let mut out: Vec<AdapterRecord> = entries\n     │...\n  495│// [impl->REQ-INSTALL-4]\n *496│pub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {\n  497│    all_records(adapters_dir)\n  498│        .into_iter()\n  499│        .filter(|r| r.active)\n     │...\n  561│    }\n *562│    let s = std::fs::read_to_string(&path)?;\n  563│    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(format!(\"profiles/{profile}\")))\n  564│}\n  565│\n     │...\n  568│pub fn local_profile_names(adapters_dir: &Path, adapter: &str) -> Vec<String> {\n *569│    let Ok(entries) = std::fs::read_dir(local_profiles_dir(adapters_dir, adapter)) else {\n  570│        return Vec::new();\n  571│    };\n  572│    let mut out: Vec<String> = entries\n     │...\n  900│        std::fs::create_dir_all(&src).unwrap();\n *901│        std::fs::write(src.join(\"manifest.toml\"), toml_src).unwrap();\n  902│        src\n  903│    }\n  904│\n     │...\n  948│        assert!(\n *949│            adapters.join(\"mock-harness\").join(\"manifest.toml\").exists(),\n  950│            \"copy held\"\n  951│        );\n  952│\n     │...\n  955│        assert!(\n *956│            !adapters.join(\"mock-shell\").join(\"manifest.toml\").exists(),\n  957│            \"no copy\"\n  958│        );\n  959│        // Pointer reads live: mutate the source, the loaded manifest follows.\n  960│        std::fs::write(\n *961│            shell_src.join(\"manifest.toml\"),\n  962│            SHELL_DELEGATED.replace(\"1.0.0\", \"1.1.0\"),\n  963│        )\n  964│        .unwrap();\n     │...\n 1052│        assert!(\n*1053│            adapters.join(\"equalfloor\").join(\"manifest.toml\").exists(),\n 1054│            \"a copy-mode admit holds the manifest copy\"\n 1055│        );\n 1056│    }\n     │...\n 1132│        // Simulate the deferred/un-extracted state: the source manifest vanishes.\n*1133│        std::fs::remove_file(shell_src.join(\"manifest.toml\")).unwrap();\n 1134│\n 1135│        // load_manifest → actionable DeferredManifest naming the adapter, NOT Io.\n 1136│        match load_manifest(&adapters, &rec) {\n     │...\n 1155│    /// Register the profiled shell adapter into a fresh `adapters/` dir.\n*1156│    fn registered_shell(tmp: &Path) -> PathBuf {\n 1157│        let adapters = tmp.join(\"adapters\");\n 1158│        let src = seed_source(tmp, \"ms-src\", SHELL_COPY_PROFILED);\n 1159│        register(&adapters, &src, 1000).unwrap();"},"isError":false,"timestamp":1784271550120}}
{"type":"message","id":"4b198a52","parentId":"20386722","timestamp":"2026-07-17T06:59:10.121Z","message":{"role":"toolResult","toolCallId":"call_vxlcm4KGFpBwUaK15wnvpy7L|fc_038f28d3e93bbfd4016a59d2be02708196bbb52c2e0468a14f","toolName":"read","content":[{"type":"text","text":"//! Project-attribution derivation kernel — the ONE implementation of the\n//! precedence contract `session-cwd → origin-cwd → context-recency`\n//! (ADR-0037 §4: behavioral parity is binding; rendered project IDs/display\n//! names must not change).\n//!\n//! Lifted verbatim from the picker's `project_refs_from` (PROJECT-INDEX W2) so\n//! the daemon's materialized-index writer and the legacy synchronous reader\n//! (until the W3 cutover) share one kernel — parity by construction, not by\n//! parallel maintenance. The picker wraps [`project_refs_from`] and drops the\n//! [`SourceLeg`]; the writer persists it as `EndpointProject.source`\n//! (observability, never rendered).\n// [impl->REQ-PROJECT-INDEX-WRITER]\n\nuse std::path::Path;\n\nuse crate::sessions;\n\n/// Which precedence leg decided an attribution (ADR-0037 §4). Serialized into\n/// `EndpointProject.source` by the daemon writer; never rendered to users.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum SourceLeg {\n    /// Derived from the newest usable session-ledger cwd.\n    SessionCwd,\n    /// Derived from the endpoint's `info.cwd` origin (fresh endpoint, empty ledger).\n    OriginCwd,\n    /// A committed-context `p-*` branch membership, by tip recency.\n    ContextRecency,\n}\n\nimpl SourceLeg {\n    /// The wire token (`EndpointProject.source`).\n    pub fn as_str(self) -> &'static str {\n        match self {\n            SourceLeg::SessionCwd => \"session-cwd\",\n            SourceLeg::OriginCwd => \"origin-cwd\",\n            SourceLeg::ContextRecency => \"context-recency\",\n        }\n    }\n}\n\n/// One derived project reference: the stable id, the cwd that derived it\n/// (empty for a store-only / branch-membership ref), the human display name,\n/// and which precedence leg produced it.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct DerivedRef {\n    pub id: String,\n    pub dir: String,\n    pub display: String,\n    pub source: SourceLeg,\n}\n\n/// Whether `path` is inside (or equal to) `base` — the owlery-internal\n/// exclusion test (REQ-PICKER-PROJECT-HISTORY-TRUTH 1c: psyche-host sessions\n/// bind `owlery/*/nested/*-psyche` cwds that must never be presented as\n/// projects). Normalizes separators + case (Windows) before the prefix check.\npub fn path_under(path: &Path, base: &Path) -> bool {\n    let (p, b) = (normalize_path(path), normalize_path(base));\n    !b.is_empty() && (p == b || p.starts_with(&format!(\"{b}/\")))\n}\n\n/// The normalized spelling of a path used for cwd identity/dedup: forward\n/// slashes, no trailing slash, lowercased (Windows-style case folding — two\n/// spellings of one dir must not derive twice, and the derivation itself is\n/// case-preserving where it matters because it re-reads the ORIGINAL path).\npub fn normalize_path(p: &Path) -> String {\n    p.to_string_lossy()\n        .replace('\\\\', \"/\")\n        .trim_end_matches('/')\n        .to_lowercase()\n}\n\n/// Project history newest→oldest for one endpoint — the parity kernel\n/// (REQ-PICKER-PROJECT-HISTORY-TRUTH semantics, unchanged): the sessions.log\n/// cwds (deduped by cwd then by `project_id`, owlery-internal EXCLUDED) UNION\n/// the endpoint's `info.cwd` origin UNION the committed-context `p-*` branch\n/// memberships (`store_ids`, already in recency order). Pure over its inputs\n/// (`derive` injected so it unit-tests without a git subprocess — and so the\n/// daemon writer can inject its cwd cache). `entries` arrive oldest→newest\n/// (the ledger order); walked REVERSED so the most recent project sorts\n/// first. A store-only project (in the branches but never a session cwd) is\n/// appended with an empty dir and the id as its display.\n// [impl->REQ-PROJECT-INDEX-WRITER]\npub fn project_refs_from(\n    entries: &[sessions::SessionEntry],\n    origin_cwd: Option<&str>,\n    store_ids: Vec<String>,\n    owlery: &Path,\n    derive: impl Fn(&Path) -> (String, String),\n) -> Vec<DerivedRef> {\n    let mut refs: Vec<DerivedRef> = Vec::new();\n    let mut seen_id: std::collections::HashSet<String> = std::collections::HashSet::new();\n    let mut seen_cwd: std::collections::HashSet<String> = std::collections::HashSet::new();\n\n    // Candidate cwds newest→oldest: the reversed ledger, then the info.cwd origin\n    // (covers a fresh endpoint whose ledger is still empty).\n    let mut cwds: Vec<(&str, SourceLeg)> = entries\n        .iter()\n        .rev()\n        .filter_map(|e| e.cwd.as_deref())\n        .map(|c| (c, SourceLeg::SessionCwd))\n        .collect();\n    // A fresh endpoint (empty ledger) still belongs to the project it was\n    // created in — the origin cwd unions into membership.\n    // [impl->REQ-PICKER-HISTORY-FRESH]\n    if let Some(o) = origin_cwd {\n        cwds.push((o, SourceLeg::OriginCwd));\n    }\n    for (cwd, source) in cwds {\n        if !seen_cwd.insert(cwd.to_string()) {\n            continue; // same dir already resolved (skip the repeat + its git cost)\n        }\n        let dir = Path::new(cwd);\n        if path_under(dir, owlery) {\n            continue; // owlery-internal cwd is not a project (1c)\n        }\n        let (pid, display) = derive(dir);\n        if pid.is_empty() {\n            continue;\n        }\n        if seen_id.insert(pid.clone()) {\n            refs.push(DerivedRef { id: pid, dir: cwd.to_string(), display, source });\n        }\n    }\n    // UNION the committed-context branches (project ids only; no session dir → the\n    // display falls back to the id verbatim, REQ-PICKER-PROJECT-DISPLAY-NAME).\n    for pid in store_ids {\n        if seen_id.insert(pid.clone()) {\n            refs.push(DerivedRef {\n                id: pid.clone(),\n                dir: String::new(),\n                display: pid,\n                source: SourceLeg::ContextRecency,\n            });\n        }\n    }\n    refs\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::sessions::{SessionEntry, SessionTrigger};\n\n    fn entry(cwd: Option<&str>) -> SessionEntry {\n        SessionEntry {\n            ts: \"2026-07-15T00:00:00Z\".to_string(),\n            session_id: \"s\".to_string(),\n            trigger: SessionTrigger::Boot,\n            cwd: cwd.map(str::to_string),\n            ordinal: None,\n            adapter: None,\n        }\n    }\n\n    /// A derive stub: id = lowercased folder name, display = folder name.\n    fn stub_derive(p: &Path) -> (String, String) {\n        let name = p\n            .file_name()\n            .map(|n| n.to_string_lossy().to_string())\n            .unwrap_or_default();\n        (name.to_lowercase(), name)\n    }\n\n    // [unit->REQ-PROJECT-INDEX-WRITER] the precedence contract: newest session\n    // cwd wins over origin, origin wins over branch membership, and each leg is\n    // tagged with its SourceLeg — the writer's `source` observability field.\n    #[test]\n    fn precedence_session_then_origin_then_context() {\n        let owlery = Path::new(\"/home/owlery\");\n\n        // Newest session cwd wins (ledger arrives oldest→newest; reversed walk).\n        let entries = vec![entry(Some(\"/p/Older\")), entry(Some(\"/p/Newest\"))];\n        let refs = project_refs_from(\n            &entries,\n            Some(\"/p/Origin\"),\n            vec![\"store-proj\".to_string()],\n            owlery,\n            stub_derive,\n        );\n        assert_eq!(refs[0].id, \"newest\");\n        assert_eq!(refs[0].source, SourceLeg::SessionCwd);\n        assert_eq!(refs[0].display, \"Newest\");\n        // Origin is a later (older) candidate, still present + tagged.\n        let origin = refs.iter().find(|r| r.id == \"origin\").expect(\"origin ref\");\n        assert_eq!(origin.source, SourceLeg::OriginCwd);\n        // Branch-only membership appends last, display = id verbatim.\n        let store = refs.iter().find(|r| r.id == \"store-proj\").expect(\"store ref\");\n        assert_eq!(store.source, SourceLeg::ContextRecency);\n        assert_eq!(store.display, \"store-proj\");\n        assert_eq!(store.dir, \"\");\n\n        // Empty ledger → origin decides the head.\n        let refs = project_refs_from(&[], Some(\"/p/Origin\"), vec![], owlery, stub_derive);\n        assert_eq!(refs[0].source, SourceLeg::OriginCwd);\n\n        // Nothing but branches → context recency decides the head.\n        let refs =\n            project_refs_from(&[], None, vec![\"only-store\".to_string()], owlery, stub_derive);\n        assert_eq!(refs[0].source, SourceLeg::ContextRecency);\n    }\n\n    // [unit->REQ-PROJECT-INDEX-WRITER] dedup semantics unchanged from the picker\n    // kernel: repeated cwds derive once; two cwds deriving one project id keep\n    // the newest; owlery-internal cwds are excluded; empty derivations skipped.\n    #[test]\n    fn dedup_and_owlery_exclusion() {\n        let owlery = Path::new(\"/home/owlery\");\n        let derive_calls = std::cell::Cell::new(0);\n        let counting = |p: &Path| {\n            derive_calls.set(derive_calls.get() + 1);\n            stub_derive(p)\n        };\n        let entries = vec![\n            entry(Some(\"/p/proj-a\")),\n            entry(Some(\"/home/owlery/x/nested/x-psyche\")), // owlery-internal: excluded\n            entry(Some(\"/p/proj-a\")),                      // repeat cwd: one derivation\n            entry(Some(\"/other/PROJ-A\")),                  // same id, different dir: newest dir kept\n        ];\n        let refs = project_refs_from(&entries, None, vec![], owlery, counting);\n        assert_eq!(refs.len(), 1, \"one project id → one ref: {refs:?}\");\n        assert_eq!(refs[0].id, \"proj-a\");\n        assert_eq!(\n            refs[0].dir, \"/other/PROJ-A\",\n            \"newest cwd (reversed walk) carries the ref\"\n        );\n        assert_eq!(\n            derive_calls.get(),\n            2,\n            \"derive runs once per DISTINCT non-excluded cwd\"\n        );\n    }\n\n    // [unit->REQ-PROJECT-INDEX-WRITER] path normalization: separators + case +\n    // trailing slash fold together (the cwd-identity dedup key), and the\n    // owlery-exclusion prefix test rides the same normalization.\n    #[test]\n    fn normalization_and_path_under() {\n        assert_eq!(normalize_path(Path::new(\"C:\\\\Users\\\\X\\\\Proj\\\\\")), \"c:/users/x/proj\");\n        assert!(path_under(\n            Path::new(\"C:\\\\home\\\\OWLERY\\\\a\\\\nested\"),\n            Path::new(\"c:/home/owlery\")\n        ));\n        assert!(!path_under(Path::new(\"/home/owl\"), Path::new(\"/home/owlery\")));\n        assert!(!path_under(Path::new(\"/anything\"), Path::new(\"\")));\n    }\n}\n"}],"details":{"displayContent":{"text":"//! Project-attribution derivation kernel — the ONE implementation of the\r\n//! precedence contract `session-cwd → origin-cwd → context-recency`\r\n//! (ADR-0037 §4: behavioral parity is binding; rendered project IDs/display\r\n//! names must not change).\r\n//!\r\n//! Lifted verbatim from the picker's `project_refs_from` (PROJECT-INDEX W2) so\r\n//! the daemon's materialized-index writer and the legacy synchronous reader\r\n//! (until the W3 cutover) share one kernel — parity by construction, not by\r\n//! parallel maintenance. The picker wraps [`project_refs_from`] and drops the\r\n//! [`SourceLeg`]; the writer persists it as `EndpointProject.source`\r\n//! (observability, never rendered).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n\r\nuse std::path::Path;\r\n\r\nuse crate::sessions;\r\n\r\n/// Which precedence leg decided an attribution (ADR-0037 §4). Serialized into\r\n/// `EndpointProject.source` by the daemon writer; never rendered to users.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum SourceLeg {\r\n    /// Derived from the newest usable session-ledger cwd.\r\n    SessionCwd,\r\n    /// Derived from the endpoint's `info.cwd` origin (fresh endpoint, empty ledger).\r\n    OriginCwd,\r\n    /// A committed-context `p-*` branch membership, by tip recency.\r\n    ContextRecency,\r\n}\r\n\r\nimpl SourceLeg {\r\n    /// The wire token (`EndpointProject.source`).\r\n    pub fn as_str(self) -> &'static str {\r\n        match self {\r\n            SourceLeg::SessionCwd => \"session-cwd\",\r\n            SourceLeg::OriginCwd => \"origin-cwd\",\r\n            SourceLeg::ContextRecency => \"context-recency\",\r\n        }\r\n    }\r\n}\r\n\r\n/// One derived project reference: the stable id, the cwd that derived it\r\n/// (empty for a store-only / branch-membership ref), the human display name,\r\n/// and which precedence leg produced it.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct DerivedRef {\r\n    pub id: String,\r\n    pub dir: String,\r\n    pub display: String,\r\n    pub source: SourceLeg,\r\n}\r\n\r\n/// Whether `path` is inside (or equal to) `base` — the owlery-internal\r\n/// exclusion test (REQ-PICKER-PROJECT-HISTORY-TRUTH 1c: psyche-host sessions\r\n/// bind `owlery/*/nested/*-psyche` cwds that must never be presented as\r\n/// projects). Normalizes separators + case (Windows) before the prefix check.\r\npub fn path_under(path: &Path, base: &Path) -> bool {\r\n    let (p, b) = (normalize_path(path), normalize_path(base));\r\n    !b.is_empty() && (p == b || p.starts_with(&format!(\"{b}/\")))\r\n}\r\n\r\n/// The normalized spelling of a path used for cwd identity/dedup: forward\r\n/// slashes, no trailing slash, lowercased (Windows-style case folding — two\r\n/// spellings of one dir must not derive twice, and the derivation itself is\r\n/// case-preserving where it matters because it re-reads the ORIGINAL path).\r\npub fn normalize_path(p: &Path) -> String {\r\n    p.to_string_lossy()\r\n        .replace('\\\\', \"/\")\r\n        .trim_end_matches('/')\r\n        .to_lowercase()\r\n}\r\n\r\n/// Project history newest→oldest for one endpoint — the parity kernel\r\n/// (REQ-PICKER-PROJECT-HISTORY-TRUTH semantics, unchanged): the sessions.log\r\n/// cwds (deduped by cwd then by `project_id`, owlery-internal EXCLUDED) UNION\r\n/// the endpoint's `info.cwd` origin UNION the committed-context `p-*` branch\r\n/// memberships (`store_ids`, already in recency order). Pure over its inputs\r\n/// (`derive` injected so it unit-tests without a git subprocess — and so the\r\n/// daemon writer can inject its cwd cache). `entries` arrive oldest→newest\r\n/// (the ledger order); walked REVERSED so the most recent project sorts\r\n/// first. A store-only project (in the branches but never a session cwd) is\r\n/// appended with an empty dir and the id as its display.\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\npub fn project_refs_from(\r\n    entries: &[sessions::SessionEntry],\r\n    origin_cwd: Option<&str>,\r\n    store_ids: Vec<String>,\r\n    owlery: &Path,\r\n    derive: impl Fn(&Path) -> (String, String),\r\n) -> Vec<DerivedRef> {\r\n    let mut refs: Vec<DerivedRef> = Vec::new();\r\n    let mut seen_id: std::collections::HashSet<String> = std::collections::HashSet::new();\r\n    let mut seen_cwd: std::collections::HashSet<String> = std::collections::HashSet::new();\r\n\r\n    // Candidate cwds newest→oldest: the reversed ledger, then the info.cwd origin\r\n    // (covers a fresh endpoint whose ledger is still empty).\r\n    let mut cwds: Vec<(&str, SourceLeg)> = entries\r\n        .iter()\r\n        .rev()\r\n        .filter_map(|e| e.cwd.as_deref())\r\n        .map(|c| (c, SourceLeg::SessionCwd))\r\n        .collect();\r\n    // A fresh endpoint (empty ledger) still belongs to the project it was\r\n    // created in — the origin cwd unions into membership.\r\n    // [impl->REQ-PICKER-HISTORY-FRESH]\r\n    if let Some(o) = origin_cwd {\r\n        cwds.push((o, SourceLeg::OriginCwd));\r\n    }\r\n    for (cwd, source) in cwds {\r\n        if !seen_cwd.insert(cwd.to_string()) {\r\n            continue; // same dir already resolved (skip the repeat + its git cost)\r\n        }\r\n        let dir = Path::new(cwd);\r\n        if path_under(dir, owlery) {\r\n            continue; // owlery-internal cwd is not a project (1c)\r\n        }\r\n        let (pid, display) = derive(dir);\r\n        if pid.is_empty() {\r\n            continue;\r\n        }\r\n        if seen_id.insert(pid.clone()) {\r\n            refs.push(DerivedRef { id: pid, dir: cwd.to_string(), display, source });\r\n        }\r\n    }\r\n    // UNION the committed-context branches (project ids only; no session dir → the\r\n    // display falls back to the id verbatim, REQ-PICKER-PROJECT-DISPLAY-NAME).\r\n    for pid in store_ids {\r\n        if seen_id.insert(pid.clone()) {\r\n            refs.push(DerivedRef {\r\n                id: pid.clone(),\r\n                dir: String::new(),\r\n                display: pid,\r\n                source: SourceLeg::ContextRecency,\r\n            });\r\n        }\r\n    }\r\n    refs\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::sessions::{SessionEntry, SessionTrigger};\r\n\r\n    fn entry(cwd: Option<&str>) -> SessionEntry {\r\n        SessionEntry {\r\n            ts: \"2026-07-15T00:00:00Z\".to_string(),\r\n            session_id: \"s\".to_string(),\r\n            trigger: SessionTrigger::Boot,\r\n            cwd: cwd.map(str::to_string),\r\n            ordinal: None,\r\n            adapter: None,\r\n        }\r\n    }\r\n\r\n    /// A derive stub: id = lowercased folder name, display = folder name.\r\n    fn stub_derive(p: &Path) -> (String, String) {\r\n        let name = p\r\n            .file_name()\r\n            .map(|n| n.to_string_lossy().to_string())\r\n            .unwrap_or_default();\r\n        (name.to_lowercase(), name)\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-WRITER] the precedence contract: newest session\r\n    // cwd wins over origin, origin wins over branch membership, and each leg is\r\n    // tagged with its SourceLeg — the writer's `source` observability field.\r\n    #[test]\r\n    fn precedence_session_then_origin_then_context() {\r\n        let owlery = Path::new(\"/home/owlery\");\r\n\r\n        // Newest session cwd wins (ledger arrives oldest→newest; reversed walk).\r\n        let entries = vec![entry(Some(\"/p/Older\")), entry(Some(\"/p/Newest\"))];\r\n        let refs = project_refs_from(\r\n            &entries,\r\n            Some(\"/p/Origin\"),\r\n            vec![\"store-proj\".to_string()],\r\n            owlery,\r\n            stub_derive,\r\n        );\r\n        assert_eq!(refs[0].id, \"newest\");\r\n        assert_eq!(refs[0].source, SourceLeg::SessionCwd);\r\n        assert_eq!(refs[0].display, \"Newest\");\r\n        // Origin is a later (older) candidate, still present + tagged.\r\n        let origin = refs.iter().find(|r| r.id == \"origin\").expect(\"origin ref\");\r\n        assert_eq!(origin.source, SourceLeg::OriginCwd);\r\n        // Branch-only membership appends last, display = id verbatim.\r\n        let store = refs.iter().find(|r| r.id == \"store-proj\").expect(\"store ref\");\r\n        assert_eq!(store.source, SourceLeg::ContextRecency);\r\n        assert_eq!(store.display, \"store-proj\");\r\n        assert_eq!(store.dir, \"\");\r\n\r\n        // Empty ledger → origin decides the head.\r\n        let refs = project_refs_from(&[], Some(\"/p/Origin\"), vec![], owlery, stub_derive);\r\n        assert_eq!(refs[0].source, SourceLeg::OriginCwd);\r\n\r\n        // Nothing but branches → context recency decides the head.\r\n        let refs =\r\n            project_refs_from(&[], None, vec![\"only-store\".to_string()], owlery, stub_derive);\r\n        assert_eq!(refs[0].source, SourceLeg::ContextRecency);\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-WRITER] dedup semantics unchanged from the picker\r\n    // kernel: repeated cwds derive once; two cwds deriving one project id keep\r\n    // the newest; owlery-internal cwds are excluded; empty derivations skipped.\r\n    #[test]\r\n    fn dedup_and_owlery_exclusion() {\r\n        let owlery = Path::new(\"/home/owlery\");\r\n        let derive_calls = std::cell::Cell::new(0);\r\n        let counting = |p: &Path| {\r\n            derive_calls.set(derive_calls.get() + 1);\r\n            stub_derive(p)\r\n        };\r\n        let entries = vec![\r\n            entry(Some(\"/p/proj-a\")),\r\n            entry(Some(\"/home/owlery/x/nested/x-psyche\")), // owlery-internal: excluded\r\n            entry(Some(\"/p/proj-a\")),                      // repeat cwd: one derivation\r\n            entry(Some(\"/other/PROJ-A\")),                  // same id, different dir: newest dir kept\r\n        ];\r\n        let refs = project_refs_from(&entries, None, vec![], owlery, counting);\r\n        assert_eq!(refs.len(), 1, \"one project id → one ref: {refs:?}\");\r\n        assert_eq!(refs[0].id, \"proj-a\");\r\n        assert_eq!(\r\n            refs[0].dir, \"/other/PROJ-A\",\r\n            \"newest cwd (reversed walk) carries the ref\"\r\n        );\r\n        assert_eq!(\r\n            derive_calls.get(),\r\n            2,\r\n            \"derive runs once per DISTINCT non-excluded cwd\"\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-PROJECT-INDEX-WRITER] path normalization: separators + case +\r\n    // trailing slash fold together (the cwd-identity dedup key), and the\r\n    // owlery-exclusion prefix test rides the same normalization.\r\n    #[test]\r\n    fn normalization_and_path_under() {\r\n        assert_eq!(normalize_path(Path::new(\"C:\\\\Users\\\\X\\\\Proj\\\\\")), \"c:/users/x/proj\");\r\n        assert!(path_under(\r\n            Path::new(\"C:\\\\home\\\\OWLERY\\\\a\\\\nested\"),\r\n            Path::new(\"c:/home/owlery\")\r\n        ));\r\n        assert!(!path_under(Path::new(\"/home/owl\"), Path::new(\"/home/owlery\")));\r\n        assert!(!path_under(Path::new(\"/anything\"), Path::new(\"\")));\r\n    }\r\n}\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\projderive.rs"}}},"isError":false,"timestamp":1784271550121}}
{"type":"message","id":"c021fbdb","parentId":"4b198a52","timestamp":"2026-07-17T06:59:10.121Z","message":{"role":"toolResult","toolCallId":"call_psLfL2MBdt1pi2SyL8A6J2KL|fc_038f28d3e93bbfd4016a59d2be02508196a8f56b7f0ba4b81b","toolName":"read","content":[{"type":"text","text":"//! `project_id` derivation (STORAGE §project_id) — the key that makes\n//! project-context sync match across machines.\n//!\n//! Derived **from the project's git remote URL** when it has one: stable and\n//! path-independent, so the same project at `/home/x/proj` and\n//! `C:\\Users\\x\\proj` resolves to the same id (which is what lets D6's\n//! `p-<project_id>` branches line up across nodes). A git repo without a\n//! remote falls back to its toplevel **folder name**; a non-git dir falls back\n//! to its own folder name (documented limitation: folder names are not\n//! guaranteed path-independent across machines).\n//!\n//! The emitted id is a slug in the endpoint-id charset (4.6) — lowercase\n//! `[a-z0-9-]` — so it is valid as a branch-name component and a directory\n//! name everywhere, and two URL spellings of one remote (SSH vs HTTPS,\n//! trailing `.git`, case) normalize to one id.\n// [impl->REQ-STORE-1]\n\nuse std::path::Path;\nuse std::time::Duration;\n\nuse crate::gitrun::run_git;\n\n/// Budget for the (local, fast) `git remote get-url` / `rev-parse` reads.\nconst PROJECT_GIT_TIMEOUT: Duration = Duration::from_secs(10);\n\n/// Slug a remote URL (or any name) into the project-id charset: lowercase,\n/// scheme/credentials/`.git` stripped, every non-`[a-z0-9]` run collapsed to\n/// one `-`, trimmed of leading/trailing `-`.\nfn slug(raw: &str) -> String {\n    let mut s = raw.trim().to_ascii_lowercase();\n    // Strip URL scheme and credentials: `https://user@host/p` → `host/p`,\n    // `git@host:p` → `host:p` (the `:` collapses to `-` below).\n    if let Some(idx) = s.find(\"://\") {\n        s = s[idx + 3..].to_string();\n    }\n    if let Some(idx) = s.find('@') {\n        s = s[idx + 1..].to_string();\n    }\n    if let Some(stripped) = s.strip_suffix(\".git\") {\n        s = stripped.to_string();\n    }\n    let mut out = String::with_capacity(s.len());\n    let mut last_dash = true; // suppress a leading '-'\n    for c in s.chars() {\n        if c.is_ascii_alphanumeric() {\n            out.push(c);\n            last_dash = false;\n        } else if !last_dash {\n            out.push('-');\n            last_dash = true;\n        }\n    }\n    while out.ends_with('-') {\n        out.pop();\n    }\n    out\n}\n\n/// Derive the `project_id` for a directory inside (or being) a project.\n/// Remote-URL first, then git-toplevel folder name, then the dir's own name;\n/// an empty derivation degrades to `\"unnamed-project\"` rather than erroring —\n/// a broken repo must not stall context routing.\n// [impl->REQ-STORE-1]\npub fn project_id_for_dir(dir: &Path) -> String {\n    project_id_and_display_for_dir(dir).0\n}\n\n/// The human-recognizable DISPLAY name for a remote URL: the last path segment,\n/// `.git` + trailing slash stripped, case preserved. `https://github.com/SaberMage/\n/// spt-core.git` → `spt-core`; `git@github.com:SaberMage/spt-core.git` → `spt-core`.\n/// NOT parsed back out of the slug (org/repo boundaries don't survive slugging —\n/// `my-org/x` and `my/org-x` slug identically). Empty URL tail degrades to the id.\nfn display_from_url(url: &str) -> String {\n    let s = url.trim().trim_end_matches('/');\n    let s = s.strip_suffix(\".git\").unwrap_or(s);\n    // Last segment after a `/` (path) or `:` (scp-style `host:org/repo`).\n    let tail = s.rsplit(['/', ':']).next().unwrap_or(s);\n    if tail.is_empty() {\n        slug(url)\n    } else {\n        tail.to_string()\n    }\n}\n\n/// Derive BOTH the `project_id` (the cross-machine KEY, slugged) and a friendly\n/// DISPLAY name for a directory, in ONE git-invocation pass (same ladder as the\n/// id). The display is human-recognizable — the remote URL's last path segment,\n/// else the git-toplevel folder, else the dir's own folder name — so a picker /\n/// list surface can render `spt-core` while keying on the `github-com-…` slug\n/// (REQ-PICKER-PROJECT-DISPLAY-NAME). A store-only caller with no dir renders the\n/// id verbatim (this fn is only reached with a real dir).\n// [impl->REQ-STORE-1] [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\npub fn project_id_and_display_for_dir(dir: &Path) -> (String, String) {\n    let dir_arg = dir.to_string_lossy().to_string();\n\n    // 1. Remote URL (stable + path-independent): id = slug(url), display = URL tail.\n    if let Ok(out) = run_git(\n        &[\"-C\", &dir_arg, \"remote\", \"get-url\", \"origin\"],\n        None,\n        None,\n        PROJECT_GIT_TIMEOUT,\n    ) {\n        if out.success() {\n            let url = out.stdout_trimmed();\n            let id = slug(&url);\n            if !id.is_empty() {\n                return (id, display_from_url(&url));\n            }\n        }\n    }\n\n    // 2. Git toplevel folder name (repo without a remote): id = slug, display = folder.\n    if let Ok(out) = run_git(\n        &[\"-C\", &dir_arg, \"rev-parse\", \"--show-toplevel\"],\n        None,\n        None,\n        PROJECT_GIT_TIMEOUT,\n    ) {\n        if out.success() {\n            if let Some(name) = Path::new(&out.stdout_trimmed())\n                .file_name()\n                .map(|n| n.to_string_lossy().to_string())\n            {\n                let id = slug(&name);\n                if !id.is_empty() {\n                    return (id, name);\n                }\n            }\n        }\n    }\n\n    // 3. The directory's own folder name.\n    let name = dir\n        .file_name()\n        .map(|n| n.to_string_lossy().to_string())\n        .unwrap_or_default();\n    let id = if name.is_empty() { String::new() } else { slug(&name) };\n    if id.is_empty() {\n        (\"unnamed-project\".to_string(), \"unnamed-project\".to_string())\n    } else {\n        (id, name)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::gitrun::run_git_ok;\n    use tempfile::tempdir;\n\n    #[test] // [unit->REQ-STORE-1] SSH and HTTPS spellings of one remote slug to ONE id.\n    fn url_spellings_normalize_to_one_id() {\n        let a = slug(\"git@github.com:SaberMage/spt-core.git\");\n        let b = slug(\"https://github.com/sabermage/spt-core\");\n        assert_eq!(a, \"github-com-sabermage-spt-core\");\n        assert_eq!(a, b);\n    }\n\n    #[test] // [unit->REQ-STORE-1] remote beats folder name; no remote falls to toplevel folder; no git falls to dir name.\n    fn derivation_ladder() {\n        // Non-git dir → its own folder name.\n        let plain = tempdir().unwrap();\n        let sub = plain.path().join(\"My Project\");\n        std::fs::create_dir_all(&sub).unwrap();\n        assert_eq!(project_id_for_dir(&sub), \"my-project\");\n\n        // Git repo without a remote → toplevel folder name (queried from a subdir).\n        let repo = tempdir().unwrap();\n        let root = repo.path().join(\"proj-x\");\n        std::fs::create_dir_all(root.join(\"nested\")).unwrap();\n        run_git_ok(&[\"init\", &root.to_string_lossy()], None, None).unwrap();\n        assert_eq!(project_id_for_dir(&root.join(\"nested\")), \"proj-x\");\n\n        // With a remote → the normalized URL wins.\n        run_git_ok(\n            &[\n                \"-C\",\n                &root.to_string_lossy(),\n                \"remote\",\n                \"add\",\n                \"origin\",\n                \"git@example.com:Team/Repo.git\",\n            ],\n            None,\n            None,\n        )\n        .unwrap();\n        assert_eq!(\n            project_id_for_dir(&root.join(\"nested\")),\n            \"example-com-team-repo\"\n        );\n    }\n\n    #[test] // [unit->REQ-PICKER-PROJECT-DISPLAY-NAME] display = URL tail / folder name, case preserved, never parsed out of the lossy slug.\n    fn display_derivation() {\n        // display_from_url: last path segment, `.git` + trailing slash stripped.\n        assert_eq!(display_from_url(\"https://github.com/SaberMage/spt-core.git\"), \"spt-core\");\n        assert_eq!(display_from_url(\"git@github.com:SaberMage/spt-core.git\"), \"spt-core\");\n        assert_eq!(display_from_url(\"https://example.com/Team/Repo/\"), \"Repo\");\n\n        // One-pass derivation: id keys on the slug, display stays recognizable.\n        let repo = tempdir().unwrap();\n        let root = repo.path().join(\"proj-x\");\n        std::fs::create_dir_all(root.join(\"nested\")).unwrap();\n        run_git_ok(&[\"init\", &root.to_string_lossy()], None, None).unwrap();\n        // No remote → display = toplevel folder.\n        let (id, disp) = project_id_and_display_for_dir(&root.join(\"nested\"));\n        assert_eq!((id.as_str(), disp.as_str()), (\"proj-x\", \"proj-x\"));\n        // With a remote → id slugged, display = URL tail (case preserved).\n        run_git_ok(\n            &[\"-C\", &root.to_string_lossy(), \"remote\", \"add\", \"origin\", \"git@example.com:Team/CoolRepo.git\"],\n            None,\n            None,\n        )\n        .unwrap();\n        let (id, disp) = project_id_and_display_for_dir(&root.join(\"nested\"));\n        assert_eq!((id.as_str(), disp.as_str()), (\"example-com-team-coolrepo\", \"CoolRepo\"));\n    }\n}\n"}],"details":{"displayContent":{"text":"//! `project_id` derivation (STORAGE §project_id) — the key that makes\r\n//! project-context sync match across machines.\r\n//!\r\n//! Derived **from the project's git remote URL** when it has one: stable and\r\n//! path-independent, so the same project at `/home/x/proj` and\r\n//! `C:\\Users\\x\\proj` resolves to the same id (which is what lets D6's\r\n//! `p-<project_id>` branches line up across nodes). A git repo without a\r\n//! remote falls back to its toplevel **folder name**; a non-git dir falls back\r\n//! to its own folder name (documented limitation: folder names are not\r\n//! guaranteed path-independent across machines).\r\n//!\r\n//! The emitted id is a slug in the endpoint-id charset (4.6) — lowercase\r\n//! `[a-z0-9-]` — so it is valid as a branch-name component and a directory\r\n//! name everywhere, and two URL spellings of one remote (SSH vs HTTPS,\r\n//! trailing `.git`, case) normalize to one id.\r\n// [impl->REQ-STORE-1]\r\n\r\nuse std::path::Path;\r\nuse std::time::Duration;\r\n\r\nuse crate::gitrun::run_git;\r\n\r\n/// Budget for the (local, fast) `git remote get-url` / `rev-parse` reads.\r\nconst PROJECT_GIT_TIMEOUT: Duration = Duration::from_secs(10);\r\n\r\n/// Slug a remote URL (or any name) into the project-id charset: lowercase,\r\n/// scheme/credentials/`.git` stripped, every non-`[a-z0-9]` run collapsed to\r\n/// one `-`, trimmed of leading/trailing `-`.\r\nfn slug(raw: &str) -> String {\r\n    let mut s = raw.trim().to_ascii_lowercase();\r\n    // Strip URL scheme and credentials: `https://user@host/p` → `host/p`,\r\n    // `git@host:p` → `host:p` (the `:` collapses to `-` below).\r\n    if let Some(idx) = s.find(\"://\") {\r\n        s = s[idx + 3..].to_string();\r\n    }\r\n    if let Some(idx) = s.find('@') {\r\n        s = s[idx + 1..].to_string();\r\n    }\r\n    if let Some(stripped) = s.strip_suffix(\".git\") {\r\n        s = stripped.to_string();\r\n    }\r\n    let mut out = String::with_capacity(s.len());\r\n    let mut last_dash = true; // suppress a leading '-'\r\n    for c in s.chars() {\r\n        if c.is_ascii_alphanumeric() {\r\n            out.push(c);\r\n            last_dash = false;\r\n        } else if !last_dash {\r\n            out.push('-');\r\n            last_dash = true;\r\n        }\r\n    }\r\n    while out.ends_with('-') {\r\n        out.pop();\r\n    }\r\n    out\r\n}\r\n\r\n/// Derive the `project_id` for a directory inside (or being) a project.\r\n/// Remote-URL first, then git-toplevel folder name, then the dir's own name;\r\n/// an empty derivation degrades to `\"unnamed-project\"` rather than erroring —\r\n/// a broken repo must not stall context routing.\r\n// [impl->REQ-STORE-1]\r\npub fn project_id_for_dir(dir: &Path) -> String {\r\n    project_id_and_display_for_dir(dir).0\r\n}\r\n\r\n/// The human-recognizable DISPLAY name for a remote URL: the last path segment,\r\n/// `.git` + trailing slash stripped, case preserved. `https://github.com/SaberMage/\r\n/// spt-core.git` → `spt-core`; `git@github.com:SaberMage/spt-core.git` → `spt-core`.\r\n/// NOT parsed back out of the slug (org/repo boundaries don't survive slugging —\r\n/// `my-org/x` and `my/org-x` slug identically). Empty URL tail degrades to the id.\r\nfn display_from_url(url: &str) -> String {\r\n    let s = url.trim().trim_end_matches('/');\r\n    let s = s.strip_suffix(\".git\").unwrap_or(s);\r\n    // Last segment after a `/` (path) or `:` (scp-style `host:org/repo`).\r\n    let tail = s.rsplit(['/', ':']).next().unwrap_or(s);\r\n    if tail.is_empty() {\r\n        slug(url)\r\n    } else {\r\n        tail.to_string()\r\n    }\r\n}\r\n\r\n/// Derive BOTH the `project_id` (the cross-machine KEY, slugged) and a friendly\r\n/// DISPLAY name for a directory, in ONE git-invocation pass (same ladder as the\r\n/// id). The display is human-recognizable — the remote URL's last path segment,\r\n/// else the git-toplevel folder, else the dir's own folder name — so a picker /\r\n/// list surface can render `spt-core` while keying on the `github-com-…` slug\r\n/// (REQ-PICKER-PROJECT-DISPLAY-NAME). A store-only caller with no dir renders the\r\n/// id verbatim (this fn is only reached with a real dir).\r\n// [impl->REQ-STORE-1] [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\r\npub fn project_id_and_display_for_dir(dir: &Path) -> (String, String) {\r\n    let dir_arg = dir.to_string_lossy().to_string();\r\n\r\n    // 1. Remote URL (stable + path-independent): id = slug(url), display = URL tail.\r\n    if let Ok(out) = run_git(\r\n        &[\"-C\", &dir_arg, \"remote\", \"get-url\", \"origin\"],\r\n        None,\r\n        None,\r\n        PROJECT_GIT_TIMEOUT,\r\n    ) {\r\n        if out.success() {\r\n            let url = out.stdout_trimmed();\r\n            let id = slug(&url);\r\n            if !id.is_empty() {\r\n                return (id, display_from_url(&url));\r\n            }\r\n        }\r\n    }\r\n\r\n    // 2. Git toplevel folder name (repo without a remote): id = slug, display = folder.\r\n    if let Ok(out) = run_git(\r\n        &[\"-C\", &dir_arg, \"rev-parse\", \"--show-toplevel\"],\r\n        None,\r\n        None,\r\n        PROJECT_GIT_TIMEOUT,\r\n    ) {\r\n        if out.success() {\r\n            if let Some(name) = Path::new(&out.stdout_trimmed())\r\n                .file_name()\r\n                .map(|n| n.to_string_lossy().to_string())\r\n            {\r\n                let id = slug(&name);\r\n                if !id.is_empty() {\r\n                    return (id, name);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    // 3. The directory's own folder name.\r\n    let name = dir\r\n        .file_name()\r\n        .map(|n| n.to_string_lossy().to_string())\r\n        .unwrap_or_default();\r\n    let id = if name.is_empty() { String::new() } else { slug(&name) };\r\n    if id.is_empty() {\r\n        (\"unnamed-project\".to_string(), \"unnamed-project\".to_string())\r\n    } else {\r\n        (id, name)\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::gitrun::run_git_ok;\r\n    use tempfile::tempdir;\r\n\r\n    #[test] // [unit->REQ-STORE-1] SSH and HTTPS spellings of one remote slug to ONE id.\r\n    fn url_spellings_normalize_to_one_id() {\r\n        let a = slug(\"git@github.com:SaberMage/spt-core.git\");\r\n        let b = slug(\"https://github.com/sabermage/spt-core\");\r\n        assert_eq!(a, \"github-com-sabermage-spt-core\");\r\n        assert_eq!(a, b);\r\n    }\r\n\r\n    #[test] // [unit->REQ-STORE-1] remote beats folder name; no remote falls to toplevel folder; no git falls to dir name.\r\n    fn derivation_ladder() {\r\n        // Non-git dir → its own folder name.\r\n        let plain = tempdir().unwrap();\r\n        let sub = plain.path().join(\"My Project\");\r\n        std::fs::create_dir_all(&sub).unwrap();\r\n        assert_eq!(project_id_for_dir(&sub), \"my-project\");\r\n\r\n        // Git repo without a remote → toplevel folder name (queried from a subdir).\r\n        let repo = tempdir().unwrap();\r\n        let root = repo.path().join(\"proj-x\");\r\n        std::fs::create_dir_all(root.join(\"nested\")).unwrap();\r\n        run_git_ok(&[\"init\", &root.to_string_lossy()], None, None).unwrap();\r\n        assert_eq!(project_id_for_dir(&root.join(\"nested\")), \"proj-x\");\r\n\r\n        // With a remote → the normalized URL wins.\r\n        run_git_ok(\r\n            &[\r\n                \"-C\",\r\n                &root.to_string_lossy(),\r\n                \"remote\",\r\n                \"add\",\r\n                \"origin\",\r\n                \"git@example.com:Team/Repo.git\",\r\n            ],\r\n            None,\r\n            None,\r\n        )\r\n        .unwrap();\r\n        assert_eq!(\r\n            project_id_for_dir(&root.join(\"nested\")),\r\n            \"example-com-team-repo\"\r\n        );\r\n    }\r\n\r\n    #[test] // [unit->REQ-PICKER-PROJECT-DISPLAY-NAME] display = URL tail / folder name, case preserved, never parsed out of the lossy slug.\r\n    fn display_derivation() {\r\n        // display_from_url: last path segment, `.git` + trailing slash stripped.\r\n        assert_eq!(display_from_url(\"https://github.com/SaberMage/spt-core.git\"), \"spt-core\");\r\n        assert_eq!(display_from_url(\"git@github.com:SaberMage/spt-core.git\"), \"spt-core\");\r\n        assert_eq!(display_from_url(\"https://example.com/Team/Repo/\"), \"Repo\");\r\n\r\n        // One-pass derivation: id keys on the slug, display stays recognizable.\r\n        let repo = tempdir().unwrap();\r\n        let root = repo.path().join(\"proj-x\");\r\n        std::fs::create_dir_all(root.join(\"nested\")).unwrap();\r\n        run_git_ok(&[\"init\", &root.to_string_lossy()], None, None).unwrap();\r\n        // No remote → display = toplevel folder.\r\n        let (id, disp) = project_id_and_display_for_dir(&root.join(\"nested\"));\r\n        assert_eq!((id.as_str(), disp.as_str()), (\"proj-x\", \"proj-x\"));\r\n        // With a remote → id slugged, display = URL tail (case preserved).\r\n        run_git_ok(\r\n            &[\"-C\", &root.to_string_lossy(), \"remote\", \"add\", \"origin\", \"git@example.com:Team/CoolRepo.git\"],\r\n            None,\r\n            None,\r\n        )\r\n        .unwrap();\r\n        let (id, disp) = project_id_and_display_for_dir(&root.join(\"nested\"));\r\n        assert_eq!((id.as_str(), disp.as_str()), (\"example-com-team-coolrepo\", \"CoolRepo\"));\r\n    }\r\n}\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\project.rs"}}},"isError":false,"timestamp":1784271550121}}
{"type":"message","id":"ed84f721","parentId":"c021fbdb","timestamp":"2026-07-17T06:59:10.121Z","message":{"role":"toolResult","toolCallId":"call_rBDqnkNGujYy897xf8mrgWn6|fc_038f28d3e93bbfd4016a59d2be0278819697294527a1d9d928","toolName":"grep","content":[{"type":"text","text":"# crates/spt-store/src/\n## project.rs\n 63|// [impl->REQ-STORE-1]\n*64|pub fn project_id_for_dir(dir: &Path) -> String {\n 65|    project_id_and_display_for_dir(dir).0\n 66|}\n 67|\n...\n 92|// [impl->REQ-STORE-1] [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\n*93|pub fn project_id_and_display_for_dir(dir: &Path) -> (String, String) {\n 94|    let dir_arg = dir.to_string_lossy().to_string();\n 95|\n 96|    // 1. Remote URL (stable + path-independent): id = slug(url), display = URL tail."}],"details":{"scopePath":"crates/spt-store/src/project.rs, crates/spt-store/src/projderive.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":2,"fileCount":1,"files":["crates/spt-store/src/project.rs"],"fileMatches":[{"path":"crates/spt-store/src/project.rs","count":2}],"truncated":false,"displayContent":"# crates/spt-store/src/\n## project.rs\n 63│// [impl->REQ-STORE-1]\n*64│pub fn project_id_for_dir(dir: &Path) -> String {\n 65│    project_id_and_display_for_dir(dir).0\n 66│}\n 67│\n   │...\n 92│// [impl->REQ-STORE-1] [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\n*93│pub fn project_id_and_display_for_dir(dir: &Path) -> (String, String) {\n 94│    let dir_arg = dir.to_string_lossy().to_string();\n 95│\n 96│    // 1. Remote URL (stable + path-independent): id = slug(url), display = URL tail."},"isError":false,"timestamp":1784271550121}}
{"type":"message","id":"53287639","parentId":"ed84f721","timestamp":"2026-07-17T06:59:10.124Z","message":{"role":"toolResult","toolCallId":"call_vmE6gbB6NSga5sNCGwqn9JSP|fc_038f28d3e93bbfd4016a59d2be028881968bf4236f6360182d","toolName":"read","content":[{"type":"text","text":"//! pulse heartbeat (M2b T5) — the interim per-tick driver.\n//!\n//! In the no-daemon interim model, the long-running `api listen` process\n//! (T8) hosts a pulse loop. Each tick does the lightweight, recurring work:\n//! - **poll the drop dirs** → fold any commune/signoff drops into context\n//!   (single-writer ingest→delete — T4);\n//! - **fire the pending echo** → if the echo-gate (`.more-done`, armed by\n//!   `api state idle`) is set, signal that an echo is due and **clear the gate**\n//!   so it fires *exactly once*;\n//! - surface the **idle** flag for the caller's capsule/idle decisions.\n//!\n//! The pulse is deliberately thin — it decides *when* an echo is due and\n//! guarantees once-only; the heavy echo run ([`crate::echo::run_echo_commune`])\n//! is the caller's (T8), which holds the runtime + history + commune dir. Keep\n//! all of this behind this one module so M3's daemon re-host (Psyche-as-loop,\n//! daemon-owned period) is a localized swap, not a rewrite (mirrors M2a's\n//! seed-file discipline).\n//\n// Interim only: the durable pulse loop + its global/per-endpoint period config\n// belong to the M3 daemon (CONTEXT R-DAEMON-1). No dedicated REQ — the pulse is\n// throwaway glue under REQ-SEAM-PSYCHE (see M2b-PLAN §New requirements).\n\nuse crate::context::DEFAULT_PROTECTION_WINDOW_MS;\nuse crate::ingest::{ingest_drops, Ingested};\nuse spt_store::perch::{resolve_echo_gate_file, resolve_idle_file, ParentHint};\nuse std::path::Path;\nuse std::time::Duration;\n\n/// Interim default pulse period. The durable, configurable knob (global +\n/// per-endpoint override) is the M3 daemon's; this is the interim constant.\npub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n\n/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`\n/// is threaded into the drop-file precedence guard (6.5).\n#[derive(Debug, Clone, Copy)]\npub struct PulseConfig {\n    pub period: Duration,\n    pub protection_window_ms: u64,\n}\n\nimpl Default for PulseConfig {\n    fn default() -> Self {\n        PulseConfig {\n            period: DEFAULT_PULSE_PERIOD,\n            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\n        }\n    }\n}\n\n/// What one tick observed/did. `echo_due` means the gate was armed and has now\n/// been cleared — the caller should fire exactly one echo-commune.\n#[derive(Debug, Clone)]\npub struct TickOutcome {\n    pub ingested: Vec<Ingested>,\n    pub echo_due: bool,\n    pub idle: bool,\n}\n\n/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed\n/// under `project_id`), then read+clear the echo-gate (once-only fire) and the\n/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.\npub fn tick(\n    id: &str,\n    project_id: &str,\n    hint: ParentHint<'_>,\n    drop_dir: &Path,\n    now_ms: u64,\n    cfg: &PulseConfig,\n) -> std::io::Result<TickOutcome> {\n    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;\n    let echo_due = take_echo_gate(id, hint.clone());\n    let idle = resolve_idle_file(id, hint).exists();\n    Ok(TickOutcome {\n        ingested,\n        echo_due,\n        idle,\n    })\n}\n\n/// Read-and-clear the echo-gate: returns whether it was armed, removing the\n/// sentinel so the echo fires exactly once. Clearing a missing gate is a no-op.\npub fn take_echo_gate(id: &str, hint: ParentHint<'_>) -> bool {\n    let gate = resolve_echo_gate_file(id, hint);\n    if gate.exists() {\n        // Best-effort clear; if removal fails the next tick re-fires (at-least-\n        // once degrades safely — a duplicate echo is harmless, a lost one less so).\n        let _ = std::fs::remove_file(&gate);\n        true\n    } else {\n        false\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_support::with_home;\n    use spt_store::perch::resolve_perch_path;\n\n    fn arm_gate(id: &str) {\n        let perch = resolve_perch_path(id, ParentHint::Infer);\n        std::fs::create_dir_all(&perch).unwrap();\n        std::fs::write(resolve_echo_gate_file(id, ParentHint::Infer), \"\").unwrap();\n    }\n\n    #[test] // [unit->REQ-SEAM-PSYCHE] an armed echo-gate fires exactly once, then clears.\n    fn echo_gate_fires_exactly_once() {\n        with_home(|_| {\n            let drops = tempfile::tempdir().unwrap();\n            arm_gate(\"doyle\");\n\n            let t1 = tick(\n                \"doyle\",\n                \"proj-x\",\n                ParentHint::Infer,\n                drops.path(),\n                1000,\n                &PulseConfig::default(),\n            )\n            .expect(\"tick\");\n            assert!(t1.echo_due, \"armed gate should fire\");\n\n            // Second tick: gate cleared, no re-fire.\n…\n        });\n    }\n…\n}\n\n[Showing lines 1-123 of 253. Use :124 to continue]"}],"details":{"truncation":{"content":"//! pulse heartbeat (M2b T5) — the interim per-tick driver.\r\n//!\r\n//! In the no-daemon interim model, the long-running `api listen` process\r\n//! (T8) hosts a pulse loop. Each tick does the lightweight, recurring work:\r\n//! - **poll the drop dirs** → fold any commune/signoff drops into context\r\n//!   (single-writer ingest→delete — T4);\r\n//! - **fire the pending echo** → if the echo-gate (`.more-done`, armed by\r\n//!   `api state idle`) is set, signal that an echo is due and **clear the gate**\r\n//!   so it fires *exactly once*;\r\n//! - surface the **idle** flag for the caller's capsule/idle decisions.\r\n//!\r\n//! The pulse is deliberately thin — it decides *when* an echo is due and\r\n//! guarantees once-only; the heavy echo run ([`crate::echo::run_echo_commune`])\r\n//! is the caller's (T8), which holds the runtime + history + commune dir. Keep\r\n//! all of this behind this one module so M3's daemon re-host (Psyche-as-loop,\r\n//! daemon-owned period) is a localized swap, not a rewrite (mirrors M2a's\r\n//! seed-file discipline).\r\n//\r\n// Interim only: the durable pulse loop + its global/per-endpoint period config\r\n// belong to the M3 daemon (CONTEXT R-DAEMON-1). No dedicated REQ — the pulse is\r\n// throwaway glue under REQ-SEAM-PSYCHE (see M2b-PLAN §New requirements).\r\n\r\nuse crate::context::DEFAULT_PROTECTION_WINDOW_MS;\r\nuse crate::ingest::{ingest_drops, Ingested};\r\nuse spt_store::perch::{resolve_echo_gate_file, resolve_idle_file, ParentHint};\r\nuse std::path::Path;\r\nuse std::time::Duration;\r\n\r\n/// Interim default pulse period. The durable, configurable knob (global +\r\n/// per-endpoint override) is the M3 daemon's; this is the interim constant.\r\npub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\r\n\r\n/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`\r\n/// is threaded into the drop-file precedence guard (6.5).\r\n#[derive(Debug, Clone, Copy)]\r\npub struct PulseConfig {\r\n    pub period: Duration,\r\n    pub protection_window_ms: u64,\r\n}\r\n\r\nimpl Default for PulseConfig {\r\n    fn default() -> Self {\r\n        PulseConfig {\r\n            period: DEFAULT_PULSE_PERIOD,\r\n            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\r\n        }\r\n    }\r\n}\r\n\r\n/// What one tick observed/did. `echo_due` means the gate was armed and has now\r\n/// been cleared — the caller should fire exactly one echo-commune.\r\n#[derive(Debug, Clone)]\r\npub struct TickOutcome {\r\n    pub ingested: Vec<Ingested>,\r\n    pub echo_due: bool,\r\n    pub idle: bool,\r\n}\r\n\r\n/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed\r\n/// under `project_id`), then read+clear the echo-gate (once-only fire) and the\r\n/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.\r\npub fn tick(\r\n    id: &str,\r\n    project_id: &str,\r\n    hint: ParentHint<'_>,\r\n    drop_dir: &Path,\r\n    now_ms: u64,\r\n    cfg: &PulseConfig,\r\n) -> std::io::Result<TickOutcome> {\r\n    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;\r\n    let echo_due = take_echo_gate(id, hint.clone());\r\n    let idle = resolve_idle_file(id, hint).exists();\r\n    Ok(TickOutcome {\r\n        ingested,\r\n        echo_due,\r\n        idle,\r\n    })\r\n}\r\n\r\n/// Read-and-clear the echo-gate: returns whether it was armed, removing the\r\n/// sentinel so the echo fires exactly once. Clearing a missing gate is a no-op.\r\npub fn take_echo_gate(id: &str, hint: ParentHint<'_>) -> bool {\r\n    let gate = resolve_echo_gate_file(id, hint);\r\n    if gate.exists() {\r\n        // Best-effort clear; if removal fails the next tick re-fires (at-least-\r\n        // once degrades safely — a duplicate echo is harmless, a lost one less so).\r\n        let _ = std::fs::remove_file(&gate);\r\n        true\r\n    } else {\r\n        false\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::test_support::with_home;\r\n    use spt_store::perch::resolve_perch_path;\r\n\r\n    fn arm_gate(id: &str) {\r\n        let perch = resolve_perch_path(id, ParentHint::Infer);\r\n        std::fs::create_dir_all(&perch).unwrap();\r\n        std::fs::write(resolve_echo_gate_file(id, ParentHint::Infer), \"\").unwrap();\r\n    }\r\n\r\n    #[test] // [unit->REQ-SEAM-PSYCHE] an armed echo-gate fires exactly once, then clears.\r\n    fn echo_gate_fires_exactly_once() {\r\n        with_home(|_| {\r\n            let drops = tempfile::tempdir().unwrap();\r\n            arm_gate(\"doyle\");\r\n\r\n            let t1 = tick(\r\n                \"doyle\",\r\n                \"proj-x\",\r\n                ParentHint::Infer,\r\n                drops.path(),\r\n                1000,\r\n                &PulseConfig::default(),\r\n            )\r\n            .expect(\"tick\");\r\n            assert!(t1.echo_due, \"armed gate should fire\");\r\n\r\n            // Second tick: gate cleared, no re-fire.\r","truncated":true,"truncatedBy":"lines","totalLines":253,"totalBytes":4787,"outputLines":123,"outputBytes":4787,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! pulse heartbeat (M2b T5) — the interim per-tick driver.\r\n//!\r\n//! In the no-daemon interim model, the long-running `api listen` process\r\n//! (T8) hosts a pulse loop. Each tick does the lightweight, recurring work:\r\n//! - **poll the drop dirs** → fold any commune/signoff drops into context\r\n//!   (single-writer ingest→delete — T4);\r\n//! - **fire the pending echo** → if the echo-gate (`.more-done`, armed by\r\n//!   `api state idle`) is set, signal that an echo is due and **clear the gate**\r\n//!   so it fires *exactly once*;\r\n//! - surface the **idle** flag for the caller's capsule/idle decisions.\r\n//!\r\n//! The pulse is deliberately thin — it decides *when* an echo is due and\r\n//! guarantees once-only; the heavy echo run ([`crate::echo::run_echo_commune`])\r\n//! is the caller's (T8), which holds the runtime + history + commune dir. Keep\r\n//! all of this behind this one module so M3's daemon re-host (Psyche-as-loop,\r\n//! daemon-owned period) is a localized swap, not a rewrite (mirrors M2a's\r\n//! seed-file discipline).\r\n//\r\n// Interim only: the durable pulse loop + its global/per-endpoint period config\r\n// belong to the M3 daemon (CONTEXT R-DAEMON-1). No dedicated REQ — the pulse is\r\n// throwaway glue under REQ-SEAM-PSYCHE (see M2b-PLAN §New requirements).\r\n\r\nuse crate::context::DEFAULT_PROTECTION_WINDOW_MS;\r\nuse crate::ingest::{ingest_drops, Ingested};\r\nuse spt_store::perch::{resolve_echo_gate_file, resolve_idle_file, ParentHint};\r\nuse std::path::Path;\r\nuse std::time::Duration;\r\n\r\n/// Interim default pulse period. The durable, configurable knob (global +\r\n/// per-endpoint override) is the M3 daemon's; this is the interim constant.\r\npub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\r\n\r\n/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`\r\n/// is threaded into the drop-file precedence guard (6.5).\r\n#[derive(Debug, Clone, Copy)]\r\npub struct PulseConfig {\r\n    pub period: Duration,\r\n    pub protection_window_ms: u64,\r\n}\r\n\r\nimpl Default for PulseConfig {\r\n    fn default() -> Self {\r\n        PulseConfig {\r\n            period: DEFAULT_PULSE_PERIOD,\r\n            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,\r\n        }\r\n    }\r\n}\r\n\r\n/// What one tick observed/did. `echo_due` means the gate was armed and has now\r\n/// been cleared — the caller should fire exactly one echo-commune.\r\n#[derive(Debug, Clone)]\r\npub struct TickOutcome {\r\n    pub ingested: Vec<Ingested>,\r\n    pub echo_due: bool,\r\n    pub idle: bool,\r\n}\r\n\r\n/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed\r\n/// under `project_id`), then read+clear the echo-gate (once-only fire) and the\r\n/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.\r\npub fn tick(\r\n    id: &str,\r\n    project_id: &str,\r\n    hint: ParentHint<'_>,\r\n    drop_dir: &Path,\r\n    now_ms: u64,\r\n    cfg: &PulseConfig,\r\n) -> std::io::Result<TickOutcome> {\r\n    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;\r\n    let echo_due = take_echo_gate(id, hint.clone());\r\n    let idle = resolve_idle_file(id, hint).exists();\r\n    Ok(TickOutcome {\r\n        ingested,\r\n        echo_due,\r\n        idle,\r\n    })\r\n}\r\n\r\n/// Read-and-clear the echo-gate: returns whether it was armed, removing the\r\n/// sentinel so the echo fires exactly once. Clearing a missing gate is a no-op.\r\npub fn take_echo_gate(id: &str, hint: ParentHint<'_>) -> bool {\r\n    let gate = resolve_echo_gate_file(id, hint);\r\n    if gate.exists() {\r\n        // Best-effort clear; if removal fails the next tick re-fires (at-least-\r\n        // once degrades safely — a duplicate echo is harmless, a lost one less so).\r\n        let _ = std::fs::remove_file(&gate);\r\n        true\r\n    } else {\r\n        false\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::test_support::with_home;\r\n    use spt_store::perch::resolve_perch_path;\r\n\r\n    fn arm_gate(id: &str) {\r\n        let perch = resolve_perch_path(id, ParentHint::Infer);\r\n        std::fs::create_dir_all(&perch).unwrap();\r\n        std::fs::write(resolve_echo_gate_file(id, ParentHint::Infer), \"\").unwrap();\r\n    }\r\n\r\n    #[test] // [unit->REQ-SEAM-PSYCHE] an armed echo-gate fires exactly once, then clears.\r\n    fn echo_gate_fires_exactly_once() {\r\n        with_home(|_| {\r\n            let drops = tempfile::tempdir().unwrap();\r\n            arm_gate(\"doyle\");\r\n\r\n            let t1 = tick(\r\n                \"doyle\",\r\n                \"proj-x\",\r\n                ParentHint::Infer,\r\n                drops.path(),\r\n                1000,\r\n                &PulseConfig::default(),\r\n            )\r\n            .expect(\"tick\");\r\n            assert!(t1.echo_due, \"armed gate should fire\");\r\n\r\n            // Second tick: gate cleared, no re-fire.\r\n…\n        });\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,null,134,135,null,252]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-live\\src\\pulse.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":253,"totalBytes":4787,"outputLines":123,"outputBytes":4787,"shownRange":{"start":1,"end":123},"nextOffset":124}}},"isError":false,"timestamp":1784271550124}}
{"type":"message","id":"b9714548","parentId":"53287639","timestamp":"2026-07-17T06:59:21.054Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting info.json setters for project index nudges**\n\n**Analyzing project invalidation frequency and impact**"},{"type":"toolCall","id":"call_PeB1DWYMTtjmj38Yos4xYqOm|fc_038f28d3e93bbfd4016a59d2c89000819685f43b5b7594f1e8","name":"grep","arguments":{"i":"Tracing invalidation producers","pattern":"projinval::nudge|InvalScope::|nudge_at","path":"crates","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Gys2iD5OncEMqM6OQNbE5mKQ|fc_038f28d3e93bbfd4016a59d2c890148196bc2d1f2d70cbbeda","name":"grep","arguments":{"i":"Tracing info writes","pattern":"fn set_last_active|pub fn set_last_active|fn write_info|pub fn write_info|projinval","path":"crates/spt-store/src/info.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_4RzbqTrT4J8qLgbqtTDUrqy8|fc_038f28d3e93bbfd4016a59d2c8901c8196aad2f064013d70e0","name":"read","arguments":{"i":"Reading info mutation semantics","path":"crates/spt-store/src/info.rs","selector":"1-220,300-460"}},{"type":"toolCall","id":"call_fa2VmlIsU2oEiJTXzO8NHEs5|fc_038f28d3e93bbfd4016a59d2c8902c8196bcef3c34bc73e74e","name":"grep","arguments":{"i":"Finding project invalidation events","pattern":"InvalScope::Endpoint|InvalScope::Global","path":"crates/spt-daemon/src; crates/spt-store/src; crates/spt-live/src; crates/spt/src","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8842,"output":423,"cacheRead":179712,"cacheWrite":0,"totalTokens":188977,"reasoningTokens":161,"cost":{"input":0.044210000000000006,"output":0.01269,"cacheRead":0.08985599999999999,"cacheWrite":0,"total":0.146756}},"stopReason":"toolUse","timestamp":1784271550174,"responseId":"resp_038f28d3e93bbfd4016a59d2be966081969bd1ebe0518de084","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2c0be348196a064a44209307cef","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdLIR7hjC73e_loVB8P_X6zXu4BUugyFWdYLPWFM4zwAWS4CRV2Da-UIo-M3YhOIX7xo9Ts7dxAkxEDM0X6CxKS82KFuVimDkqZFZvfrC3mVeskFFjfpAz9XQ7pfryeHbGOYH8i0n8oWcQQ0CUo4xbdxtpNK7b3maa1x0PFkBvmxI5lmUJyaNns5KbvhY_1KyqrgVYStKuZ1omde0I0ui6Y8Zb_Q3z-siIB6k-SfWOTYjJcgVCDIvoG7NgRlaJOG7G7BYW5QDT57pkciLrOuQ5rMWWQgPDGhdHDMik5divMIKgJh66v6gxaMpNUEN2iDU2TsBhOZiu_b9rto0p7V7QfRiZdlWTGlvuJIxrMFHy8eEQWqEJNoSduPaEyjG06U22q4po7No3Cpp_TvJjsSUvRnbcckkPPejTR0dxaB02e2WwRt1CcLEf_bj8e6Ap4kFCUCSCFUu6V9P78hDe-r5zpLRqiybb6G6yfFL3l35FMpiNj0EJLhtVQQOftpwMoggDGo-_QnCsIz-FxXs9gZizv6zpOl52IQYlpdWzc8q8p7NlA9RzsScZsd4_pmdLZCuR234tZC9Q8O0NkYNJKl2MX3F7ImA2IRZjsQtd_Yrcfu1r1JsJ1f-2rQrasqfUM14W0AD2dWFMayJPKcmokTAR86jwgRBpfAF9YPalEwPEic-9KBTQYmnd2NsngDw7Tuw300CZpEGq5KKH5-epxjEufpwRMymPFOYGUcbAe29a94Fq8VkitUujEpBOLc1lyxoadGaChTlVUG7Ni6bIsA0xGkPEmEIFc4dQH5G9yHAPWLZlwI4mP8SUdPHKCJfJXhId64izA-2a2YBLVzHk5FzkDUOV-UZW1nthPepI55C9smYpOISpy1mUbfh9dYsrCKCy2oQeEjkj0FwmoD-e-NNemScWu-Tg9BCufuNbr25BqW2v-omRMTuYnGb0tPAzOPCtXzK5EV2HzCd2RvjoAFIM5x_GdX5Mkkt8xh7_pLGicCgL045kQzs1aQhp5t5p9ZQY4EBsFgVhFj-KaBeJWq44dxsRFF1AXLKHbUFqoVLRgfjnkNGjfpQ4cOdd6ii8NstcT8aN8G7V3bPydolxE1uktoXwiy3jtZEk4JJAGPk4DRSRhMIZJ_sRyKAmrXGGVc9f-RfVr7Xv9IVyRlFDNS99da1m7NWYZzS612V0XVqDOL6LZFplTktQe8hC3L8BfrXLwrufJQeBbm8hL37uoFazhNzsd2R3ORfXvavIlT5FzD0y73o0W1J1BfBdkAPTnXkinGi_BpNB1k710Ngc3PyQKT4702LmZIL4uS1VlBFlbu_i4Ad-hp18-jBjGePnfdsuhoCxAvRr7V6lUmtlrqrsvk1OC_cJTetIdg1zN-vRKpDoN69wHJgrGKhifuqe-lO1jNmxN23JImJA2-HHGWxpgnhVk_LfpBl0paJj5PJgMFAO7xKIJGamdmJ3YBicKPQQoQm54uPE5xIteBPNsHqBg1YhiwwKTxyfkKK5qYwo7nqD2kAVAC1ppKmt2QodKhSR1iAHN1mDpB2IzydlqCgWBGDbf9LTfc1SxFM2Za0OHrp2SBHgo7-qUBNYjHUjI4xnaPZ-mKSSbWXVEhtj0OVOekc1iyaUnYWiU-BaneaPTxPuldZZdFCgiaxftcxDhUdL7c0GgpU4XnCf6No9JjYIy5Faw6YB2_7UTi1mnRDh_5bsx7hfhR8D10xz6rvDN8-mfq9uyK_mo9q9eMJkcNS57oukk_MLsZ3GOfatnD_aYn9eu0wKkMGxlcu4afcaSVtCVH_pDUIMoZx_9idU7x-M__jIdW6FJtmh3MSYmxVj1ikWmsBlVir5K3FxfHhjtGiVGPbhIuNF4kMw9794WiE0nIv4ApqUYViOfIurzl0G-AIJqXkSfvMwHZpGEzF60avJQ9","summary":[{"type":"summary_text","text":"**Inspecting info.json setters for project index nudges**"},{"type":"summary_text","text":"**Analyzing project invalidation frequency and impact**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2c89000819685f43b5b7594f1e8","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing invalidation producers\",\"pattern\":\"projinval::nudge|InvalScope::|nudge_at\",\"path\":\"crates\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_PeB1DWYMTtjmj38Yos4xYqOm","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2c890148196bc2d1f2d70cbbeda","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing info writes\",\"pattern\":\"fn set_last_active|pub fn set_last_active|fn write_info|pub fn write_info|projinval\",\"path\":\"crates/spt-store/src/info.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Gys2iD5OncEMqM6OQNbE5mKQ","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2c8901c8196aad2f064013d70e0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading info mutation semantics\",\"path\":\"crates/spt-store/src/info.rs\",\"selector\":\"1-220,300-460\"}","call_id":"call_4RzbqTrT4J8qLgbqtTDUrqy8","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2c8902c8196bcef3c34bc73e74e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding project invalidation events\",\"pattern\":\"InvalScope::Endpoint|InvalScope::Global\",\"path\":\"crates/spt-daemon/src; crates/spt-store/src; crates/spt-live/src; crates/spt/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_fa2VmlIsU2oEiJTXzO8NHEs5","name":"grep"}]},"duration":10880.098199993372,"ttft":2586.120600000024,"contextSnapshot":{"promptTokens":188554,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PeB1DWYMTtjmj38Yos4xYqOm|fc_038f28d3e93bbfd4016a59d2c89000819685f43b5b7594f1e8","toolName":"grep","startedAt":"2026-07-17T06:59:21.055Z","args":{"path":"crates"},"intent":"Tracing invalidation producers"},"id":"76194032","parentId":"b9714548","timestamp":"2026-07-17T06:59:21.055Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Gys2iD5OncEMqM6OQNbE5mKQ|fc_038f28d3e93bbfd4016a59d2c890148196bc2d1f2d70cbbeda","toolName":"grep","startedAt":"2026-07-17T06:59:21.055Z","args":{"path":"crates/spt-store/src/info.rs"},"intent":"Tracing info writes"},"id":"1616db82","parentId":"76194032","timestamp":"2026-07-17T06:59:21.055Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4RzbqTrT4J8qLgbqtTDUrqy8|fc_038f28d3e93bbfd4016a59d2c8901c8196aad2f064013d70e0","toolName":"read","startedAt":"2026-07-17T06:59:21.055Z","args":{"path":"crates/spt-store/src/info.rs"},"intent":"Reading info mutation semantics"},"id":"fefefb4b","parentId":"1616db82","timestamp":"2026-07-17T06:59:21.055Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fa2VmlIsU2oEiJTXzO8NHEs5|fc_038f28d3e93bbfd4016a59d2c8902c8196bcef3c34bc73e74e","toolName":"grep","startedAt":"2026-07-17T06:59:21.055Z","args":{"path":"crates/spt-daemon/src; crates/spt-store/src; crates/spt-live/src; crates/spt/src"},"intent":"Finding project invalidation events"},"id":"0ebee013","parentId":"fefefb4b","timestamp":"2026-07-17T06:59:21.055Z"}
{"type":"message","id":"5aa647ec","parentId":"0ebee013","timestamp":"2026-07-17T06:59:21.057Z","message":{"role":"toolResult","toolCallId":"call_Gys2iD5OncEMqM6OQNbE5mKQ|fc_038f28d3e93bbfd4016a59d2c890148196bc2d1f2d70cbbeda","toolName":"grep","content":[{"type":"text","text":" 350|// [impl->REQ-UPD-4]\n*351|pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {\n 352|    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))\n 353|}\n 354|\n...\n 509|/// [`write_info`].\n*510|fn write_info_unlocked(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {\n 511|    let json = serde_json::to_string(info)\n 512|        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;\n 513|    atomic_write_string_durable(&perch_path.join(\"info.json\"), &json)\n...\n 532|// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]\n*533|pub fn write_info(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {\n 534|    let _lock = lock_perch_sentinel(perch_path)?;\n 535|    write_info_unlocked(perch_path, info)\n 536|}\n...\n 832|    #[test]\n*833|    fn set_last_active_stamps_recency() {\n 834|        let d = perch();\n 835|        write_info(\n 836|            d.path(),\n...\n 1212|    #[test]\n*1213|    fn write_info_racing_mutate_info_never_lost_update() {\n 1214|        use std::sync::{Arc, Barrier};\n 1215|\n 1216|        let d = perch();"}],"details":{"scopePath":"crates/spt-store/src/info.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\info.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":5,"fileCount":1,"files":["crates/spt-store/src/info.rs"],"fileMatches":[{"path":"crates/spt-store/src/info.rs","count":5}],"truncated":false,"displayContent":"  350│// [impl->REQ-UPD-4]\n *351│pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {\n  352│    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))\n  353│}\n  354│\n     │...\n  509│/// [`write_info`].\n *510│fn write_info_unlocked(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {\n  511│    let json = serde_json::to_string(info)\n  512│        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;\n  513│    atomic_write_string_durable(&perch_path.join(\"info.json\"), &json)\n     │...\n  532│// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]\n *533│pub fn write_info(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {\n  534│    let _lock = lock_perch_sentinel(perch_path)?;\n  535│    write_info_unlocked(perch_path, info)\n  536│}\n     │...\n  832│    #[test]\n *833│    fn set_last_active_stamps_recency() {\n  834│        let d = perch();\n  835│        write_info(\n  836│            d.path(),\n     │...\n 1212│    #[test]\n*1213│    fn write_info_racing_mutate_info_never_lost_update() {\n 1214│        use std::sync::{Arc, Barrier};\n 1215│\n 1216│        let d = perch();"},"isError":false,"timestamp":1784271561056}}
{"type":"message","id":"91da3134","parentId":"5aa647ec","timestamp":"2026-07-17T06:59:21.068Z","message":{"role":"toolResult","toolCallId":"call_4RzbqTrT4J8qLgbqtTDUrqy8|fc_038f28d3e93bbfd4016a59d2c8901c8196aad2f064013d70e0","toolName":"read","content":[{"type":"text","text":"//! `info.json` — the per-perch identity/status record at `<perch_path>/info.json`.\n//!\n//! Adapted from sister `types.rs` (`InfoJson` / `PidValue`) per ADR-0001:\n//! - [`PidValue`] (untagged numeric-or-`\"BUSY\"`) is copied verbatim — the\n//!   `\"BUSY\"` sentinel is load-bearing (a busy poll-listener writes it; numeric\n//!   readers must treat it as \"no usable pid\").\n//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)\n//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch\n//!   kinds the spt-core taxonomy drops — see T4).\n//!\n//! ## Torn-read tolerance (KNOWN-HAZARDS 1.2, REQ-HAZARD-INFO-JSON-TORN-READ)\n//! A reader may hit the file mid-write. Two layers guarantee it never observes a\n//! truncated record:\n//! 1. **Writers go through [`write_info`]** → [`crate::atomic::atomic_write_string_durable`]\n//!    (tmp + rename): a same-volume rename is atomic, so a concurrent reader\n//!    sees the *complete* old file or the *complete* new file, never a torn one.\n//! 2. **Readers tolerate failure**: [`read_info`] / [`read_pid`] parse via a\n//!    bounded retry and return `None` on persistent failure — never panic.\n\nuse 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…\n}\n…\n/// so the un-host never fired and a signed-off agent kept its Psyche.\n///\n/// **The lock is taken on a STABLE per-perch sentinel `<perch>/.info.lock`\n/// (created once, NEVER renamed) — NOT on `info.json`.** This is load-bearing:\n/// `info.json` is replaced by a tmp+rename on every write (a NEW inode each\n/// time), and advisory locks (`flock`/`LockFileEx`) lock the INODE — so a lock\n/// on `info.json`'s fd does NOT serialize across the rename (a second writer\n/// could open+lock the old, now-unlinked inode, read a stale record, and clobber\n/// the first writer's rename — a *fake* lock). Locking the constant sentinel\n/// inode is what actually serializes the RMW. The OS advisory lock auto-releases\n/// on fd drop / process exit, so a crashed holder strands nothing (no stale-lock\n/// reaping needed).\n///\n/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a\n/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS\n/// only. Errors if the record is absent/unreadable — the daemon only mutates\n/// perches it bound.\n///\n/// **Public RMW primitive** (REQ-HAZARD-INFO-RMW-LOST-UPDATE): any caller that\n/// reads-then-modifies an EXISTING record must funnel through here, never a raw\n/// `read_info` + [`write_info`] pair — the unlocked gap between the read and the write\n/// is a silent lost update against a concurrent stamp. Errors `NotFound` on an absent\n/// record (the daemon/caller only mutates a perch it knows exists); callers that\n/// tolerated an absent record with a raw read map that back at their call site. To\n/// CREATE-or-replace a whole record (no read-modify) use [`write_info`]; for a\n/// read→check→write establish, use [`establish_locked`].\n// [impl->REQ-DAEMON-1]\n// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]\npub fn mutate_info(perch_path: &Path, mutate: impl FnOnce(&mut InfoJson)) -> std::io::Result<()> {\n    // Hold the stable per-perch sentinel across the whole read→mutate→write RMW.\n    let _lock = lock_perch_sentinel(perch_path)?;\n    let mut rec = read_info(perch_path).ok_or_else(|| {\n        std::io::Error::new(std::io::ErrorKind::NotFound, \"info.json absent or unreadable\")\n    })?;\n    mutate(&mut rec);\n    // Already holding the lock — use the UNLOCKED writer (re-locking would deadlock).\n    write_info_unlocked(perch_path, &rec)\n}\n\n/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness\n/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp\n/// never clobbers it. Errors if the record is absent — the daemon only marks\n/// perches it bound.\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\npub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))\n}\n\n/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via\n/// [`mutate_info`].\n// [impl->REQ-UPD-4]\npub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))\n}\n\n/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`\n/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when\n/// `state` is dormant, so a reader never sees a dormant record without its anchor.\n/// Serialized via [`mutate_info`].\n// [impl->REQ-INST-3]\npub fn set_rest_state(\n    perch_path: &Path,\n    state: &str,\n    dormant_since_ms: Option<u64>,\n) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.rest_state = Some(state.to_string());\n        rec.dormant_since_ms = dormant_since_ms;\n    })\n}\n\n/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,\n/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].\n// [impl->REQ-INST-14]\npub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))\n}\n\n/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):\n/// which node's operator is driving this endpoint's session. Serialized via\n/// [`mutate_info`].\n// [impl->REQ-REACH-1]\npub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))\n}\n\n/// Stamp the `controlled` any-controller marker (#3, REQ-GOSSIP-CONTROLLED-ANY):\n/// whether ANY operator (local or remote) currently drives this endpoint. The\n/// broker is the single writer, stamped alongside [`set_driven_by`]. Kept SEPARATE\n/// from `driven_by` (which is remote-only — a local controller must not latch it,\n/// KH 7.15) so a locally-controlled endpoint still gossips as controlled.\n/// Serialized via [`mutate_info`].\n// [impl->REQ-GOSSIP-CONTROLLED-ANY]\npub fn set_controlled(perch_path: &Path, controlled: bool) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.controlled = controlled)\n}\n\n/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single\n/// logical writer (it owns the viewers map); `0` clears the field. Serialized via\n/// [`mutate_info`].\n// [impl->REQ-RCVIEW-1]\npub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.viewer_count = if count == 0 { None } else { Some(count) }\n    })\n}\n\n/// Stamp (`true`) or clear (`false`) the session's PTY `input_backpressure`\n/// signal (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The broker is the\n/// single writer (it owns the per-session input-writer thread): `true` is set on\n/// a full-FIFO DROP, `false` clears the field on the next accepted enqueue\n/// (heal-on-resume). Serialized via [`mutate_info`]; best-effort like\n/// [`set_viewer_count`].\n// [impl->REQ-HAZARD-PTY-INPUT-WRITER-WEDGE]\npub fn set_input_backpressure(perch_path: &Path, dropping: bool) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.input_backpressure = if dropping { Some(true) } else { None }\n    })\n}\n\n/// Stamp (`Some(reason)`) or clear (`None`) the translation-binary fault surface\n/// on an spt-hosted perch (F029 C-1, REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH) —\n/// the harness-reachable mirror of the daemon's invisible `TRANSLATION_FAULT`\n/// stderr line (the F-019 unread channel). The BROKER is the single writer:\n/// `Some(reason)` on a REAL fault (binary death or commit-miss strike-budget\n/// exhaustion — never a mere miss, which preserves the binary), `None` on a\n/// healthy respawn / adapter-apply restart. `status` is **never** touched —\n/// daemon liveness stays authoritative (KH 2.5). Serialized via [`mutate_info`];\n/// best-effort like [`set_input_backpressure`].\n// [impl->REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH]\npub fn set_translation_fault(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.translation_fault = reason.map(|r| r.to_string())\n    })\n}\n\n/// Stamp (`Some(reason)`) or clear (`None`) the psyche-host-failure state on a\n/// live perch (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE) — the harness-reachable\n/// mirror of the per-event Psyche turn's otherwise-invisible brain-stderr failure.\n///\n/// - `Some(reason)` — set/overwrite the CURRENT-STATE stamp: `reason` is the\n///   Psyche-turn error string, `ts` is now ([`crate::timefmt::now_rfc3339`]\n///   — the same clock every other info.json string stamp uses), and `attempts`\n///   is the prior `attempts + 1` (read inside the locked RMW so concurrent\n///   retries can't lose a count), `1` on the first failure.\n/// - `None` — clear it (a later successful host shows clean).\n///\n/// `status` is **never** touched — daemon liveness stays authoritative\n/// (KH 2.5). Serialized via [`mutate_info`]; errors if the record is absent —\n/// the daemon only stamps perches it hosts.\n// [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]\npub fn set_psyche_host_error(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| match reason {\n        Some(r) => {\n            let attempts = rec\n                .psyche_host_error\n                .as_ref()\n                .map(|e| e.attempts.saturating_add(1))\n                .unwrap_or(1);\n            rec.psyche_host_error = Some(PsycheHostError {\n                reason: r.to_string(),\n…\n            });\n        }\n…\n    })\n}"}],"details":{"displayContent":{"text":"//! `info.json` — the per-perch identity/status record at `<perch_path>/info.json`.\n//!\n//! Adapted from sister `types.rs` (`InfoJson` / `PidValue`) per ADR-0001:\n//! - [`PidValue`] (untagged numeric-or-`\"BUSY\"`) is copied verbatim — the\n//!   `\"BUSY\"` sentinel is load-bearing (a busy poll-listener writes it; numeric\n//!   readers must treat it as \"no usable pid\").\n//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)\n//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch\n//!   kinds the spt-core taxonomy drops — see T4).\n//!\n//! ## Torn-read tolerance (KNOWN-HAZARDS 1.2, REQ-HAZARD-INFO-JSON-TORN-READ)\n//! A reader may hit the file mid-write. Two layers guarantee it never observes a\n//! truncated record:\n//! 1. **Writers go through [`write_info`]** → [`crate::atomic::atomic_write_string_durable`]\n//!    (tmp + rename): a same-volume rename is atomic, so a concurrent reader\n//!    sees the *complete* old file or the *complete* new file, never a torn one.\n//! 2. **Readers tolerate failure**: [`read_info`] / [`read_pid`] parse via a\n//!    bounded retry and return `None` on persistent failure — never panic.\n\nuse 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…\n}\n…\n/// so the un-host never fired and a signed-off agent kept its Psyche.\n///\n/// **The lock is taken on a STABLE per-perch sentinel `<perch>/.info.lock`\n/// (created once, NEVER renamed) — NOT on `info.json`.** This is load-bearing:\n/// `info.json` is replaced by a tmp+rename on every write (a NEW inode each\n/// time), and advisory locks (`flock`/`LockFileEx`) lock the INODE — so a lock\n/// on `info.json`'s fd does NOT serialize across the rename (a second writer\n/// could open+lock the old, now-unlinked inode, read a stale record, and clobber\n/// the first writer's rename — a *fake* lock). Locking the constant sentinel\n/// inode is what actually serializes the RMW. The OS advisory lock auto-releases\n/// on fd drop / process exit, so a crashed holder strands nothing (no stale-lock\n/// reaping needed).\n///\n/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a\n/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS\n/// only. Errors if the record is absent/unreadable — the daemon only mutates\n/// perches it bound.\n///\n/// **Public RMW primitive** (REQ-HAZARD-INFO-RMW-LOST-UPDATE): any caller that\n/// reads-then-modifies an EXISTING record must funnel through here, never a raw\n/// `read_info` + [`write_info`] pair — the unlocked gap between the read and the write\n/// is a silent lost update against a concurrent stamp. Errors `NotFound` on an absent\n/// record (the daemon/caller only mutates a perch it knows exists); callers that\n/// tolerated an absent record with a raw read map that back at their call site. To\n/// CREATE-or-replace a whole record (no read-modify) use [`write_info`]; for a\n/// read→check→write establish, use [`establish_locked`].\n// [impl->REQ-DAEMON-1]\n// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]\npub fn mutate_info(perch_path: &Path, mutate: impl FnOnce(&mut InfoJson)) -> std::io::Result<()> {\n    // Hold the stable per-perch sentinel across the whole read→mutate→write RMW.\n    let _lock = lock_perch_sentinel(perch_path)?;\n    let mut rec = read_info(perch_path).ok_or_else(|| {\n        std::io::Error::new(std::io::ErrorKind::NotFound, \"info.json absent or unreadable\")\n    })?;\n    mutate(&mut rec);\n    // Already holding the lock — use the UNLOCKED writer (re-locking would deadlock).\n    write_info_unlocked(perch_path, &rec)\n}\n\n/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness\n/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp\n/// never clobbers it. Errors if the record is absent — the daemon only marks\n/// perches it bound.\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\npub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))\n}\n\n/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via\n/// [`mutate_info`].\n// [impl->REQ-UPD-4]\npub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))\n}\n\n/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`\n/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when\n/// `state` is dormant, so a reader never sees a dormant record without its anchor.\n/// Serialized via [`mutate_info`].\n// [impl->REQ-INST-3]\npub fn set_rest_state(\n    perch_path: &Path,\n    state: &str,\n    dormant_since_ms: Option<u64>,\n) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.rest_state = Some(state.to_string());\n        rec.dormant_since_ms = dormant_since_ms;\n    })\n}\n\n/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,\n/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].\n// [impl->REQ-INST-14]\npub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))\n}\n\n/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):\n/// which node's operator is driving this endpoint's session. Serialized via\n/// [`mutate_info`].\n// [impl->REQ-REACH-1]\npub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))\n}\n\n/// Stamp the `controlled` any-controller marker (#3, REQ-GOSSIP-CONTROLLED-ANY):\n/// whether ANY operator (local or remote) currently drives this endpoint. The\n/// broker is the single writer, stamped alongside [`set_driven_by`]. Kept SEPARATE\n/// from `driven_by` (which is remote-only — a local controller must not latch it,\n/// KH 7.15) so a locally-controlled endpoint still gossips as controlled.\n/// Serialized via [`mutate_info`].\n// [impl->REQ-GOSSIP-CONTROLLED-ANY]\npub fn set_controlled(perch_path: &Path, controlled: bool) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| rec.controlled = controlled)\n}\n\n/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single\n/// logical writer (it owns the viewers map); `0` clears the field. Serialized via\n/// [`mutate_info`].\n// [impl->REQ-RCVIEW-1]\npub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.viewer_count = if count == 0 { None } else { Some(count) }\n    })\n}\n\n/// Stamp (`true`) or clear (`false`) the session's PTY `input_backpressure`\n/// signal (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The broker is the\n/// single writer (it owns the per-session input-writer thread): `true` is set on\n/// a full-FIFO DROP, `false` clears the field on the next accepted enqueue\n/// (heal-on-resume). Serialized via [`mutate_info`]; best-effort like\n/// [`set_viewer_count`].\n// [impl->REQ-HAZARD-PTY-INPUT-WRITER-WEDGE]\npub fn set_input_backpressure(perch_path: &Path, dropping: bool) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.input_backpressure = if dropping { Some(true) } else { None }\n    })\n}\n\n/// Stamp (`Some(reason)`) or clear (`None`) the translation-binary fault surface\n/// on an spt-hosted perch (F029 C-1, REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH) —\n/// the harness-reachable mirror of the daemon's invisible `TRANSLATION_FAULT`\n/// stderr line (the F-019 unread channel). The BROKER is the single writer:\n/// `Some(reason)` on a REAL fault (binary death or commit-miss strike-budget\n/// exhaustion — never a mere miss, which preserves the binary), `None` on a\n/// healthy respawn / adapter-apply restart. `status` is **never** touched —\n/// daemon liveness stays authoritative (KH 2.5). Serialized via [`mutate_info`];\n/// best-effort like [`set_input_backpressure`].\n// [impl->REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH]\npub fn set_translation_fault(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| {\n        rec.translation_fault = reason.map(|r| r.to_string())\n    })\n}\n\n/// Stamp (`Some(reason)`) or clear (`None`) the psyche-host-failure state on a\n/// live perch (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE) — the harness-reachable\n/// mirror of the per-event Psyche turn's otherwise-invisible brain-stderr failure.\n///\n/// - `Some(reason)` — set/overwrite the CURRENT-STATE stamp: `reason` is the\n///   Psyche-turn error string, `ts` is now ([`crate::timefmt::now_rfc3339`]\n///   — the same clock every other info.json string stamp uses), and `attempts`\n///   is the prior `attempts + 1` (read inside the locked RMW so concurrent\n///   retries can't lose a count), `1` on the first failure.\n/// - `None` — clear it (a later successful host shows clean).\n///\n/// `status` is **never** touched — daemon liveness stays authoritative\n/// (KH 2.5). Serialized via [`mutate_info`]; errors if the record is absent —\n/// the daemon only stamps perches it hosts.\n// [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]\npub fn set_psyche_host_error(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {\n    mutate_info(perch_path, |rec| match reason {\n        Some(r) => {\n            let attempts = rec\n                .psyche_host_error\n                .as_ref()\n                .map(|e| e.attempts.saturating_add(1))\n                .unwrap_or(1);\n            rec.psyche_host_error = Some(PsycheHostError {\n                reason: r.to_string(),\n…\n            });\n        }\n…\n    })\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,null,258,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,451,452,453,454,455,456,457,458,459,460,null,463,464,null,466,467]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\info.rs"}}},"isError":false,"timestamp":1784271561068}}
{"type":"message","id":"7c567c11","parentId":"91da3134","timestamp":"2026-07-17T06:59:21.070Z","message":{"role":"toolResult","toolCallId":"call_PeB1DWYMTtjmj38Yos4xYqOm|fc_038f28d3e93bbfd4016a59d2c89000819685f43b5b7594f1e8","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### projwriter.rs\n 1088|            for i in 0..10 {\n*1089|                spt_store::projinval::nudge(&InvalScope::Endpoint {\n 1090|                    id: format!(\"b{}\", (i % 3) + 1),\n 1091|                    cwd: None,\n 1092|                });\n### sync.rs\n 453|                            if report.applied.iter().any(|a| a.branch.starts_with(\"p-\")) {\n*454|                                spt_store::projinval::nudge(\n*455|                                    &spt_store::projinval::InvalScope::Global,\n 456|                                );\n 457|                            }\n 458|                            return Ok(report);\n\n## spt-msg/src/\n### ready.rs\n 137|            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*138|            spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 139|                id: id.to_string(),\n 140|                cwd: rec.cwd.clone(),\n 141|            });\n\n## spt-store/src/\n### contextstore.rs\n 155|        if let Some(home) = self.root.parent() {\n*156|            let _ = crate::projinval::nudge_at(\n 157|                &crate::projinval::invalidations_dir_under(home),\n 158|                &scope,\n 159|            );\n...\n 238|        if committed.is_some() {\n*239|            self.nudge_index(crate::projinval::InvalScope::Global);\n 240|        }\n 241|        Ok(committed)\n 242|    }\n...\n 381|        // membership view. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*382|        self.nudge_index(crate::projinval::InvalScope::Global);\n*383|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 384|            id: old_id.to_string(),\n 385|            cwd: None,\n 386|        });\n*387|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 388|            id: new_id.to_string(),\n 389|            cwd: None,\n 390|        });\n...\n 447|        // Global where membership grew). [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*448|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 449|            id: new.to_string(),\n 450|            cwd: None,\n 451|        });\n...\n 507|        // the endpoint's row. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*508|        self.nudge_index(crate::projinval::InvalScope::Global);\n*509|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 510|            id: id.to_string(),\n 511|            cwd: None,\n 512|        });\n### projinval.rs\n 65|/// event file is skipped by the tolerant drain and the backstop covers it).\n*66|pub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\n 67|    std::fs::create_dir_all(dir)?;\n 68|    let nanos = std::time::SystemTime::now()\n 69|        .duration_since(std::time::UNIX_EPOCH)\n...\n 81|\n*82|/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\n 83|/// the fire-and-forget form every mutation seam calls (an unwritable queue\n 84|/// must never fail the mutation; the periodic reconcile is the backstop).\n 85|// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n 86|pub fn nudge(scope: &InvalScope) {\n*87|    let _ = nudge_at(&invalidations_dir(), scope);\n 88|}\n 89|\n 90|/// Whether any event is pending in `dir` — the writer's cheap poll (one\n...\n 124|pub struct Coalesced {\n*125|    /// Any [`InvalScope::Global`] event was present → membership refresh\n 126|    /// (fingerprint-gated).\n 127|    pub global: bool,\n 128|    /// Endpoint ids needing a row refresh (deduped, sorted).\n...\n 147|        match e {\n*148|            InvalScope::Global => out.global = true,\n*149|            InvalScope::Endpoint { id, cwd } => {\n 150|                out.endpoints.insert(id);\n 151|                if let Some(c) = cwd {\n 152|                    out.cwds.insert(c);\n...\n 173|\n*174|        nudge_at(&dir, &InvalScope::Global).unwrap();\n*175|        nudge_at(\n 176|            &dir,\n*177|            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\n 178|        )\n 179|        .unwrap();\n*180|        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\n 181|        assert!(pending_at(&dir));\n 182|\n 183|        let drained = drain_at(&dir);\n...\n 195|        let batch = vec![\n*196|            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*197|            InvalScope::Global,\n*198|            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*199|            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\n*200|            InvalScope::Global,\n 201|        ];\n 202|        let one = coalesce(batch);\n 203|        assert!(one.global);\n...\n 221|        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\n*222|        nudge_at(&dir, &InvalScope::Global).unwrap();\n 223|\n 224|        let drained = drain_at(&dir);\n*225|        assert_eq!(drained, vec![InvalScope::Global]);\n 226|        assert!(!pending_at(&dir), \"garbage is consumed too, not left to wedge\");\n 227|    }\n 228|}\n\n## spt/\n\n### src/\n#### cli.rs\n 12688|    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*12689|    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 12690|        id: id.to_string(),\n 12691|        cwd: None,\n 12692|    });\n\n#### api/\n##### reporting.rs\n 135|    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*136|    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 137|        id: id.to_string(),\n 138|        cwd: cwd_for_nudge,\n 139|    });\n##### startup.rs\n 459|        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*460|        spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 461|            id: id.to_string(),\n 462|            cwd: rec.cwd.clone(),\n 463|        });\n\n### tests/\n#### projindex_writer_e2e.rs\n 293|    .unwrap();\n*294|    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 295|        id: \"ep3\".to_string(),\n 296|        cwd: Some(dir_c.to_string_lossy().to_string()),\n 297|    });"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":33,"fileCount":9,"files":["crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/sync.rs","crates/spt-msg/src/ready.rs","crates/spt-store/src/contextstore.rs","crates/spt-store/src/projinval.rs","crates/spt/src/api/reporting.rs","crates/spt/src/api/startup.rs","crates/spt/src/cli.rs","crates/spt/tests/projindex_writer_e2e.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/projwriter.rs","count":1},{"path":"crates/spt-daemon/src/sync.rs","count":2},{"path":"crates/spt-msg/src/ready.rs","count":1},{"path":"crates/spt-store/src/contextstore.rs","count":8},{"path":"crates/spt-store/src/projinval.rs","count":17},{"path":"crates/spt/src/api/reporting.rs","count":1},{"path":"crates/spt/src/api/startup.rs","count":1},{"path":"crates/spt/src/cli.rs","count":1},{"path":"crates/spt/tests/projindex_writer_e2e.rs","count":1}],"truncated":false,"displayContent":"# crates/\n\n## spt-daemon/src/\n### projwriter.rs\n 1088│            for i in 0..10 {\n*1089│                spt_store::projinval::nudge(&InvalScope::Endpoint {\n 1090│                    id: format!(\"b{}\", (i % 3) + 1),\n 1091│                    cwd: None,\n 1092│                });\n### sync.rs\n 453│                            if report.applied.iter().any(|a| a.branch.starts_with(\"p-\")) {\n*454│                                spt_store::projinval::nudge(\n*455│                                    &spt_store::projinval::InvalScope::Global,\n 456│                                );\n 457│                            }\n 458│                            return Ok(report);\n\n## spt-msg/src/\n### ready.rs\n 137│            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*138│            spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 139│                id: id.to_string(),\n 140│                cwd: rec.cwd.clone(),\n 141│            });\n\n## spt-store/src/\n### contextstore.rs\n 155│        if let Some(home) = self.root.parent() {\n*156│            let _ = crate::projinval::nudge_at(\n 157│                &crate::projinval::invalidations_dir_under(home),\n 158│                &scope,\n 159│            );\n    │...\n 238│        if committed.is_some() {\n*239│            self.nudge_index(crate::projinval::InvalScope::Global);\n 240│        }\n 241│        Ok(committed)\n 242│    }\n    │...\n 381│        // membership view. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*382│        self.nudge_index(crate::projinval::InvalScope::Global);\n*383│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 384│            id: old_id.to_string(),\n 385│            cwd: None,\n 386│        });\n*387│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 388│            id: new_id.to_string(),\n 389│            cwd: None,\n 390│        });\n    │...\n 447│        // Global where membership grew). [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*448│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 449│            id: new.to_string(),\n 450│            cwd: None,\n 451│        });\n    │...\n 507│        // the endpoint's row. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*508│        self.nudge_index(crate::projinval::InvalScope::Global);\n*509│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 510│            id: id.to_string(),\n 511│            cwd: None,\n 512│        });\n### projinval.rs\n  65│/// event file is skipped by the tolerant drain and the backstop covers it).\n *66│pub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\n  67│    std::fs::create_dir_all(dir)?;\n  68│    let nanos = std::time::SystemTime::now()\n  69│        .duration_since(std::time::UNIX_EPOCH)\n    │...\n  81│\n *82│/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\n  83│/// the fire-and-forget form every mutation seam calls (an unwritable queue\n  84│/// must never fail the mutation; the periodic reconcile is the backstop).\n  85│// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n  86│pub fn nudge(scope: &InvalScope) {\n *87│    let _ = nudge_at(&invalidations_dir(), scope);\n  88│}\n  89│\n  90│/// Whether any event is pending in `dir` — the writer's cheap poll (one\n    │...\n 124│pub struct Coalesced {\n*125│    /// Any [`InvalScope::Global`] event was present → membership refresh\n 126│    /// (fingerprint-gated).\n 127│    pub global: bool,\n 128│    /// Endpoint ids needing a row refresh (deduped, sorted).\n    │...\n 147│        match e {\n*148│            InvalScope::Global => out.global = true,\n*149│            InvalScope::Endpoint { id, cwd } => {\n 150│                out.endpoints.insert(id);\n 151│                if let Some(c) = cwd {\n 152│                    out.cwds.insert(c);\n    │...\n 173│\n*174│        nudge_at(&dir, &InvalScope::Global).unwrap();\n*175│        nudge_at(\n 176│            &dir,\n*177│            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\n 178│        )\n 179│        .unwrap();\n*180│        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\n 181│        assert!(pending_at(&dir));\n 182│\n 183│        let drained = drain_at(&dir);\n    │...\n 195│        let batch = vec![\n*196│            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*197│            InvalScope::Global,\n*198│            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*199│            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\n*200│            InvalScope::Global,\n 201│        ];\n 202│        let one = coalesce(batch);\n 203│        assert!(one.global);\n    │...\n 221│        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\n*222│        nudge_at(&dir, &InvalScope::Global).unwrap();\n 223│\n 224│        let drained = drain_at(&dir);\n*225│        assert_eq!(drained, vec![InvalScope::Global]);\n 226│        assert!(!pending_at(&dir), \"garbage is consumed too, not left to wedge\");\n 227│    }\n 228│}\n\n## spt/\n\n### src/\n#### cli.rs\n 12688│    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*12689│    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 12690│        id: id.to_string(),\n 12691│        cwd: None,\n 12692│    });\n\n#### api/\n##### reporting.rs\n 135│    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*136│    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 137│        id: id.to_string(),\n 138│        cwd: cwd_for_nudge,\n 139│    });\n##### startup.rs\n 459│        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*460│        spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 461│            id: id.to_string(),\n 462│            cwd: rec.cwd.clone(),\n 463│        });\n\n### tests/\n#### projindex_writer_e2e.rs\n 293│    .unwrap();\n*294│    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 295│        id: \"ep3\".to_string(),\n 296│        cwd: Some(dir_c.to_string_lossy().to_string()),\n 297│    });"},"isError":false,"timestamp":1784271561070}}
{"type":"message","id":"565a0375","parentId":"7c567c11","timestamp":"2026-07-17T06:59:21.084Z","message":{"role":"toolResult","toolCallId":"call_fa2VmlIsU2oEiJTXzO8NHEs5|fc_038f28d3e93bbfd4016a59d2c8902c8196bcef3c34bc73e74e","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/src/\n### projwriter.rs\n 1088|            for i in 0..10 {\n*1089|                spt_store::projinval::nudge(&InvalScope::Endpoint {\n 1090|                    id: format!(\"b{}\", (i % 3) + 1),\n 1091|                    cwd: None,\n 1092|                });\n### sync.rs\n 454|                                spt_store::projinval::nudge(\n*455|                                    &spt_store::projinval::InvalScope::Global,\n 456|                                );\n 457|                            }\n 458|                            return Ok(report);\n\n## spt-store/src/\n### contextstore.rs\n 238|        if committed.is_some() {\n*239|            self.nudge_index(crate::projinval::InvalScope::Global);\n 240|        }\n 241|        Ok(committed)\n 242|    }\n...\n 381|        // membership view. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*382|        self.nudge_index(crate::projinval::InvalScope::Global);\n*383|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 384|            id: old_id.to_string(),\n 385|            cwd: None,\n 386|        });\n*387|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 388|            id: new_id.to_string(),\n 389|            cwd: None,\n 390|        });\n...\n 447|        // Global where membership grew). [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*448|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 449|            id: new.to_string(),\n 450|            cwd: None,\n 451|        });\n...\n 507|        // the endpoint's row. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*508|        self.nudge_index(crate::projinval::InvalScope::Global);\n*509|        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 510|            id: id.to_string(),\n 511|            cwd: None,\n 512|        });\n### projinval.rs\n 124|pub struct Coalesced {\n*125|    /// Any [`InvalScope::Global`] event was present → membership refresh\n 126|    /// (fingerprint-gated).\n 127|    pub global: bool,\n 128|    /// Endpoint ids needing a row refresh (deduped, sorted).\n...\n 147|        match e {\n*148|            InvalScope::Global => out.global = true,\n*149|            InvalScope::Endpoint { id, cwd } => {\n 150|                out.endpoints.insert(id);\n 151|                if let Some(c) = cwd {\n 152|                    out.cwds.insert(c);\n...\n 173|\n*174|        nudge_at(&dir, &InvalScope::Global).unwrap();\n 175|        nudge_at(\n 176|            &dir,\n*177|            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\n 178|        )\n 179|        .unwrap();\n*180|        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\n 181|        assert!(pending_at(&dir));\n 182|\n 183|        let drained = drain_at(&dir);\n...\n 195|        let batch = vec![\n*196|            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*197|            InvalScope::Global,\n*198|            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*199|            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\n*200|            InvalScope::Global,\n 201|        ];\n 202|        let one = coalesce(batch);\n 203|        assert!(one.global);\n...\n 221|        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\n*222|        nudge_at(&dir, &InvalScope::Global).unwrap();\n 223|\n 224|        let drained = drain_at(&dir);\n*225|        assert_eq!(drained, vec![InvalScope::Global]);\n 226|        assert!(!pending_at(&dir), \"garbage is consumed too, not left to wedge\");\n 227|    }\n 228|}\n\n## spt/src/\n### cli.rs\n 12688|    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*12689|    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 12690|        id: id.to_string(),\n 12691|        cwd: None,\n 12692|    });\n\n### api/\n#### reporting.rs\n 135|    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*136|    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 137|        id: id.to_string(),\n 138|        cwd: cwd_for_nudge,\n 139|    });\n#### startup.rs\n 459|        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*460|        spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 461|            id: id.to_string(),\n 462|            cwd: rec.cwd.clone(),\n 463|        });"}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt-store/src, crates/spt-live/src, crates/spt/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":25,"fileCount":7,"files":["crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/sync.rs","crates/spt-store/src/contextstore.rs","crates/spt-store/src/projinval.rs","crates/spt/src/api/reporting.rs","crates/spt/src/api/startup.rs","crates/spt/src/cli.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/projwriter.rs","count":1},{"path":"crates/spt-daemon/src/sync.rs","count":1},{"path":"crates/spt-store/src/contextstore.rs","count":7},{"path":"crates/spt-store/src/projinval.rs","count":13},{"path":"crates/spt/src/api/reporting.rs","count":1},{"path":"crates/spt/src/api/startup.rs","count":1},{"path":"crates/spt/src/cli.rs","count":1}],"truncated":false,"displayContent":"# crates/\n\n## spt-daemon/src/\n### projwriter.rs\n 1088│            for i in 0..10 {\n*1089│                spt_store::projinval::nudge(&InvalScope::Endpoint {\n 1090│                    id: format!(\"b{}\", (i % 3) + 1),\n 1091│                    cwd: None,\n 1092│                });\n### sync.rs\n 454│                                spt_store::projinval::nudge(\n*455│                                    &spt_store::projinval::InvalScope::Global,\n 456│                                );\n 457│                            }\n 458│                            return Ok(report);\n\n## spt-store/src/\n### contextstore.rs\n 238│        if committed.is_some() {\n*239│            self.nudge_index(crate::projinval::InvalScope::Global);\n 240│        }\n 241│        Ok(committed)\n 242│    }\n    │...\n 381│        // membership view. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*382│        self.nudge_index(crate::projinval::InvalScope::Global);\n*383│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 384│            id: old_id.to_string(),\n 385│            cwd: None,\n 386│        });\n*387│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 388│            id: new_id.to_string(),\n 389│            cwd: None,\n 390│        });\n    │...\n 447│        // Global where membership grew). [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*448│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 449│            id: new.to_string(),\n 450│            cwd: None,\n 451│        });\n    │...\n 507│        // the endpoint's row. [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*508│        self.nudge_index(crate::projinval::InvalScope::Global);\n*509│        self.nudge_index(crate::projinval::InvalScope::Endpoint {\n 510│            id: id.to_string(),\n 511│            cwd: None,\n 512│        });\n### projinval.rs\n 124│pub struct Coalesced {\n*125│    /// Any [`InvalScope::Global`] event was present → membership refresh\n 126│    /// (fingerprint-gated).\n 127│    pub global: bool,\n 128│    /// Endpoint ids needing a row refresh (deduped, sorted).\n    │...\n 147│        match e {\n*148│            InvalScope::Global => out.global = true,\n*149│            InvalScope::Endpoint { id, cwd } => {\n 150│                out.endpoints.insert(id);\n 151│                if let Some(c) = cwd {\n 152│                    out.cwds.insert(c);\n    │...\n 173│\n*174│        nudge_at(&dir, &InvalScope::Global).unwrap();\n 175│        nudge_at(\n 176│            &dir,\n*177│            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\n 178│        )\n 179│        .unwrap();\n*180│        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\n 181│        assert!(pending_at(&dir));\n 182│\n 183│        let drained = drain_at(&dir);\n    │...\n 195│        let batch = vec![\n*196│            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*197│            InvalScope::Global,\n*198│            InvalScope::Endpoint { id: \"a\".into(), cwd: Some(\"/p/x\".into()) },\n*199│            InvalScope::Endpoint { id: \"b\".into(), cwd: None },\n*200│            InvalScope::Global,\n 201│        ];\n 202│        let one = coalesce(batch);\n 203│        assert!(one.global);\n    │...\n 221│        std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();\n*222│        nudge_at(&dir, &InvalScope::Global).unwrap();\n 223│\n 224│        let drained = drain_at(&dir);\n*225│        assert_eq!(drained, vec![InvalScope::Global]);\n 226│        assert!(!pending_at(&dir), \"garbage is consumed too, not left to wedge\");\n 227│    }\n 228│}\n\n## spt/src/\n### cli.rs\n 12688│    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*12689│    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 12690│        id: id.to_string(),\n 12691│        cwd: None,\n 12692│    });\n\n### api/\n#### reporting.rs\n 135│    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*136│    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 137│        id: id.to_string(),\n 138│        cwd: cwd_for_nudge,\n 139│    });\n#### startup.rs\n 459│        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*460│        spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n 461│            id: id.to_string(),\n 462│            cwd: rec.cwd.clone(),\n 463│        });"},"isError":false,"timestamp":1784271561084}}
{"type":"message","id":"15ef9c21","parentId":"565a0375","timestamp":"2026-07-17T06:59:39.992Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating unexpected high I/O readings**\n\n**Analyzing transcript read frequency and contextstore layout**"},{"type":"toolCall","id":"call_ozHD27jvTDWQInU0om8Bv67Z|fc_038f28d3e93bbfd4016a59d2dafdb48196a2ad03c0f604fcc5","name":"grep","arguments":{"i":"Locating context store layout","pattern":"seed_git_dir|pub fn open_or_init_in|struct ContextStore|worktree|\\.seed\\.git","path":"crates/spt-store/src/contextstore.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_CteezvMzy0C2vdfza7PL7KIq|fc_038f28d3e93bbfd4016a59d2dafdc08196977841e4a51de578","name":"read","arguments":{"i":"Reading context store paths","path":"crates/spt-store/src/contextstore.rs","selector":"1-120,170-260"}},{"type":"toolCall","id":"call_FAauOuSBqfHq9xFJj2B4Rj2l|fc_038f28d3e93bbfd4016a59d2dafdc881969e867e3dbac53362","name":"read","arguments":{"i":"Reading sync bundle streaming","path":"crates/spt-daemon/src/sync.rs","selector":"180-300,310-445"}},{"type":"toolCall","id":"call_HLKmXxRFi6ffFk8nV76RkYRE|fc_038f28d3e93bbfd4016a59d2dafdcc8196acbd422ffaa10e4e","name":"read","arguments":{"i":"Reading branch tree scan","path":"crates/spt-store/src/branchstore.rs","selector":"330-370"}},{"type":"toolCall","id":"call_XKlajKMPcCWnM1XBMWnEipXF|fc_038f28d3e93bbfd4016a59d2dafdd081969398bb1f2adbef74","name":"read","arguments":{"i":"Reading adapter registry reload","path":"crates/spt-runtime/src/registry.rs","selector":"420-515"}},{"type":"toolCall","id":"call_zxUFyeUUBdU82iTcmoJSrXnb|fc_038f28d3e93bbfd4016a59d2dafdd88196876c67ccb02fb770","name":"read","arguments":{"i":"Reading notif DB query","path":"crates/spt-store/src/notif.rs","selector":"220-275"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10351,"output":662,"cacheRead":187904,"cacheWrite":0,"totalTokens":198917,"reasoningTokens":388,"cost":{"input":0.051755,"output":0.01986,"cacheRead":0.093952,"cacheWrite":0,"total":0.165567}},"stopReason":"toolUse","timestamp":1784271561090,"responseId":"resp_038f28d3e93bbfd4016a59d2c98b98819682e9908681652a34","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2cb7ee08196a2255e6b08d4bd75","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdLaGt5GfwqXO5LOeSeu5HrpKVma5ls_y5K4yzGXgu5bDCSN9g_-4Gp1H7zFJ5LmO8FmoMRhwi7JoLDzbbPqd2fz_hO4pa_Qq0-rk6Bi5zhpfBAw-r1QJXbugJr6RqoNa3SZkE-S-d2_XHpR21LzXOC4GvVj5fkf_7CDcGk3fjj-G3Lv1MfrQdbfIzJ60jNTTarmc3X8kbN8Xb1cZKeYuG4XTbrh88YSEqs1YGY6m1vgIJa_7fCaCeDcTShi2wj5iCLrLFpyHMWlwcrsI6CLpsicjwuEOsko78mu4VzwAb4cGoGG7XywuwgLaQZCDkxyVHbVn-bxBsFR8ioNdzcr3akg70ySJ_NiUbPK7I29kJHMJ4uNhA98Msz0wHOOYr1MeNi6Gtsu1d4SoIBS_PV0JHEypCMqLQeYoEyls_7FV33Wm9OozNrE9nwCCY9KBKbYOiDjwbQiCYAyNG8_idnoQVpXeoffkzVz-IubQig4Em7WjptKCn8oBJ3NKGMwvcyNcN41smA3iMLtPMUT3WMtY7y24PrJ40rok2sxZWLkPYT6hbyWRmyESXHJs4JSzgOHYI7zFgpZuKOt0BApdJm3wN56nIgm5a-eOixOHYdgcCFX6Aka0bL-brCAuhckjyXzn9unBOrMFU43bCAn2s28wT077reVji1AFuX1vLourRQBJl0v2TiCvPdFivEZV4bYIsHWGkdIAQ_hA7mmcW_uH-I1ViZcDozxPcnn5cwfdwORJXDLFelXMHxBZ7MTUu1nhJCCAfz71Y_7JoDXALw38_CYIr4HeOFjMfNN7wl8UwyxpZ63eicdUlO5VQgFr9BZd3s2U9Lo4jqzPDIfFzeYmzZLtcK-okRqLBJNYj7wuIVFTt_ShEeXWLQblylSoSJHEuRWqoqWpJBqRaXzTPL0F5rcIKY-bTRS_iE9e2DZ9krx9jZwJ3yoqGFF1dlvDFTgwTjhHbkAS4b7EW-RVyxSeuEZXGodcEPLl-S9GFDHxGuFsnRubsyIj-xPUiFnesAFapbvu9aZmyKDe5_xMiJ_w7qzBbVZYM4JIvDc18kLxl1e4VEd5bbgFOSUwkQUoWFX7tbG7hAGNPNHcPuuVD8KC0Gx84fGyavcDRprPBTd6Sew5vmS0QI7XLeeJix7ANbsL6Pxl_ALVE221PKN2c8RNMTfbFjTvgmJGHkqMQLZykHLf7g0IjbNlO0PA-8lKIcxHhoz3kju3MnGa1JMw9Ueu0scZVAZdqfF6HOrmru-2nCilXjCIIgxWJlXvCFqon5Z6zCy8n5eBRPCGE2Bbth6dm8sQDN22kVJraAzF9UCfcgOKtrJ5RJtidLbQJqdWIwEhyyRLFQ0l_WvZrEKZIezesC-oqU0ZOLcWScYOBWaQsJyVWpQQVkmiR14WXmlxPKiAaTTjUMXC9h4ASQ1kk7Hk5v-HQVnLE4Gd96jZ9_A5u5fb3Hg0hZ8KrO6aVPPY-6XcWsi--LRCtqtNMnwSbEqITQjJ58C8fcakvEyofp-8HHsFfPDIQZqVeqQEJuIMw4lfE9Cb9LdSuXxzgph44Dqa7s1SR6h0t9hzJ6sulRCjQFZJpJ5OJxl56_4ySoRRCTdiEY5be9WycBmJtVlulcBGhWnCQew0sa-N-H-3rlargGv7yVGNJMVGOvfy-90Rlf0uJ80FbaX5CHah-VHcRcD-zx0F-Kf87sH1Ix5Db3HoAnk-tmTCwBUnY6m7e84AgTIqb8Zzt0XQkHxBQorU_K601Ev4cIiSDlzVafV9jlW3zKklLsZiSI0TTWOX7gwTBr7QLQPtTfaPjnGBy843mgwitsfxeDwue7fU3Rd5zIqVsUzM-Vq2RtO7Ii4BZ4NT7JiabJKwQcT4jfDTQGYLQtfIvCTzi0hUsmCbL5bPJRnbQPIqWsGQ-9frAUJjTd8i8o7XVqMt6q-Lqq7xCItBE32JTsaFBKjf2ug2GqVBOBgGIb96Ngt8vl3WWSWSFgJGfB5AhQnq4inDIg52zwfPyS8RmBa-_VFE8H3A39Gat2fu_GbTfcrHL0XdP26V4obWVtImmRndX2HMYYhbNYWU0O3btAzWAip7urkpSj69DQpcYJ1KTgvwbaIf8TLfUScNbJMrhu2o8j69lKpchSX9udoaDi0pIPtEYZCn9axQjmOKN7H9WJ_PxY5fJHsijqvug44istZ6nUGgH1_YG7ZDo6h5U7kATxj1CDEo1UssC8uuPZ9tTpPykVXa0p3LprMENsE1QAgFGwH-Dj3xa8YkrVXCGRrm5aNtlBR0obIC_p7zChLPqdpA48widril-fgMX2MjjKbVqgTyJywXARFAc168rnuqj2LTVga9YboAZ-ibkNJ56F5gJpVtIg9abutlcosuEI84DDoD1gZ-yluHZsAMLWTGe7k-ax81arOgGIOS2BgxilN_3sqXRLFVFdvZxKs6t8-wfd4dnNu3zzlase36Y5Uwrviean4ocTybAOKfKzX65nkR6Y6MA2VvLb3ICZG8O8GvH1g4bbVI_3Ak1i8GdFlkjCFrwByBsAPU4KulOtSOmgbdiQwYi_l-ASdKRlJgl_5qCfxGYuaX-0TwRc0Oci2NpbQ7FT5T6Yd32L18tH-RiTJRd7VnXBLkxOedPGjskn4GLVaF2Pw54WQ-P3RHdJq3uCPueYgscD0C-RWPsK423uTCMsnDv9qdL3qk8aguteeyJdOVHooAN8XapWRT72ecgnHl-peELwoYqrhMEx94d2pcoZMD5fh-NJw9wXIK5C18u_hU3QLLOyMWjSnh1Fb3wB6mxcW1JOJ6TP446IXbv0wDL7NphO9i-tmStkxC31okgfjhq77LTmJ_MqfS3l01cZ8CiN7KY5rhXx9u4xWRI9dCMROdcZLZPZl7TtrnFYpN_9yU156oVGG5rSLyvgAGYwlsWvJ37a3gTEsFK0wE0HP3ljFheeUYQ_nEl5TJKIP3uYxa3T6GCl5Kw8MOeiVVs7ouSVAO3oZxkZVq_1OwrmOJF6-YJrRQp359ZAPW2f4XC0XHFJebVphXiZJxXWJytUddLZoB1V8CsTp2J6qvnDqzYJU6ISV60DkC6ZBUISMUEQ7VYHMZ3P90VEyb8r5gR8C93kBfk0Y95wkNEPXi-YLC4lz_wRo5G_V-CK701z1dGUjSiIRo9gMVwiMc4wkbTXHqaWtT788-9WwxLzcfXAeCz_xYbQVSgU4MxfIMkvZ","summary":[{"type":"summary_text","text":"**Investigating unexpected high I/O readings**"},{"type":"summary_text","text":"**Analyzing transcript read frequency and contextstore layout**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2dafdb48196a2ad03c0f604fcc5","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating context store layout\",\"pattern\":\"seed_git_dir|pub fn open_or_init_in|struct ContextStore|worktree|\\\\.seed\\\\.git\",\"path\":\"crates/spt-store/src/contextstore.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_ozHD27jvTDWQInU0om8Bv67Z","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d2dafdc08196977841e4a51de578","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading context store paths\",\"path\":\"crates/spt-store/src/contextstore.rs\",\"selector\":\"1-120,170-260\"}","call_id":"call_CteezvMzy0C2vdfza7PL7KIq","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2dafdc881969e867e3dbac53362","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading sync bundle streaming\",\"path\":\"crates/spt-daemon/src/sync.rs\",\"selector\":\"180-300,310-445\"}","call_id":"call_FAauOuSBqfHq9xFJj2B4Rj2l","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2dafdcc8196acbd422ffaa10e4e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading branch tree scan\",\"path\":\"crates/spt-store/src/branchstore.rs\",\"selector\":\"330-370\"}","call_id":"call_HLKmXxRFi6ffFk8nV76RkYRE","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2dafdd081969398bb1f2adbef74","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading adapter registry reload\",\"path\":\"crates/spt-runtime/src/registry.rs\",\"selector\":\"420-515\"}","call_id":"call_XKlajKMPcCWnM1XBMWnEipXF","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d2dafdd88196876c67ccb02fb770","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading notif DB query\",\"path\":\"crates/spt-store/src/notif.rs\",\"selector\":\"220-275\"}","call_id":"call_zxUFyeUUBdU82iTcmoJSrXnb","name":"read"}]},"duration":18901.416099995375,"ttft":2430.711099997163,"contextSnapshot":{"promptTokens":198255,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ozHD27jvTDWQInU0om8Bv67Z|fc_038f28d3e93bbfd4016a59d2dafdb48196a2ad03c0f604fcc5","toolName":"grep","startedAt":"2026-07-17T06:59:39.993Z","args":{"path":"crates/spt-store/src/contextstore.rs"},"intent":"Locating context store layout"},"id":"f0f29708","parentId":"15ef9c21","timestamp":"2026-07-17T06:59:39.993Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CteezvMzy0C2vdfza7PL7KIq|fc_038f28d3e93bbfd4016a59d2dafdc08196977841e4a51de578","toolName":"read","startedAt":"2026-07-17T06:59:39.993Z","args":{"path":"crates/spt-store/src/contextstore.rs"},"intent":"Reading context store paths"},"id":"00a1c7e5","parentId":"f0f29708","timestamp":"2026-07-17T06:59:39.993Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FAauOuSBqfHq9xFJj2B4Rj2l|fc_038f28d3e93bbfd4016a59d2dafdc881969e867e3dbac53362","toolName":"read","startedAt":"2026-07-17T06:59:39.993Z","args":{"path":"crates/spt-daemon/src/sync.rs"},"intent":"Reading sync bundle streaming"},"id":"5cdab67b","parentId":"00a1c7e5","timestamp":"2026-07-17T06:59:39.993Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HLKmXxRFi6ffFk8nV76RkYRE|fc_038f28d3e93bbfd4016a59d2dafdcc8196acbd422ffaa10e4e","toolName":"read","startedAt":"2026-07-17T06:59:39.993Z","args":{"path":"crates/spt-store/src/branchstore.rs"},"intent":"Reading branch tree scan"},"id":"62ecda23","parentId":"5cdab67b","timestamp":"2026-07-17T06:59:39.993Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XKlajKMPcCWnM1XBMWnEipXF|fc_038f28d3e93bbfd4016a59d2dafdd081969398bb1f2adbef74","toolName":"read","startedAt":"2026-07-17T06:59:39.993Z","args":{"path":"crates/spt-runtime/src/registry.rs"},"intent":"Reading adapter registry reload"},"id":"ad575c5b","parentId":"62ecda23","timestamp":"2026-07-17T06:59:39.993Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zxUFyeUUBdU82iTcmoJSrXnb|fc_038f28d3e93bbfd4016a59d2dafdd88196876c67ccb02fb770","toolName":"read","startedAt":"2026-07-17T06:59:39.993Z","args":{"path":"crates/spt-store/src/notif.rs"},"intent":"Reading notif DB query"},"id":"5c6fb1a2","parentId":"ad575c5b","timestamp":"2026-07-17T06:59:39.993Z"}
{"type":"message","id":"ec8000aa","parentId":"5c6fb1a2","timestamp":"2026-07-17T06:59:40.001Z","message":{"role":"toolResult","toolCallId":"call_CteezvMzy0C2vdfza7PL7KIq|fc_038f28d3e93bbfd4016a59d2dafdc08196977841e4a51de578","toolName":"read","content":[{"type":"text","text":"//! The two-tier context store — `tracked/` realized over [`BranchStore`]\n//! (M4-D6a; STORAGE §tracked/, ADR-0011, ADR-0013).\n//!\n//! Layout (the synced *mind*, physically separate from node-local `perches/`):\n//!\n//! ```text\n//! tracked/\n//!   .seed.git                    bare BranchStore repo (the one true object DB)\n//!   agents/<id>/                 linked worktree of branch  a-<id>\n//!     live-context.md            per-agent live context  → syncs to ALL instances\n//!   projects/<project_id>/       linked worktree of branch  p-<project_id>\n//!     <id>/project-context.md    per-(agent,project)      → same-project instances\n//! ```\n//!\n//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle\n//! exchange ships); worktrees are the read/write view. Tier routing is the\n//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind\n//! everywhere, the `p-` branch holds every agent's context for one project\n//! (cross-agent synthesis + query-routing ride that shape).\n//!\n//! Writes here are **paths + commits only** — the precedence-marker guard\n//! (6.5) stays in `spt-live::context`, which writes through the paths this\n//! store hands out, then calls the commit hook. Single writer per branch\n//! (active-instance-authoritative; 6.4).\n// [impl->REQ-STORE-1]\n\nuse std::path::{Path, PathBuf};\n\nuse crate::branchstore::BranchStore;\nuse crate::perch::tracked_dir;\nuse spt_proto::id::validate_endpoint_id;\n\n/// The per-agent live-context file inside an `a-<id>` worktree.\npub const LIVE_CONTEXT_FILE: &str = \"live-context.md\";\n/// The per-agent **live-role** file inside the same `a-<id>` worktree, beside\n/// [`LIVE_CONTEXT_FILE`] (CONTEXT §live role). A durable statement of the\n/// agent's broad purpose — replicates with the mind, renders FIRST at\n/// start-transition injection, and is written by **exactly one** path\n/// (`spt endpoint role --overwrite`); no automated writer ever touches it.\npub const LIVE_ROLE_FILE: &str = \"live-role.md\";\n/// Per-worktree directory holding surfaced concurrent-write conflict artifacts\n/// (ADR-0013; tracked, so conflict state replicates).\npub const CONFLICT_DIR: &str = \".conflicts\";\n/// The per-(agent,project) context file inside a `p-<project>` worktree,\n/// nested under the agent id.\npub const PROJECT_CONTEXT_FILE: &str = \"project-context.md\";\n\n/// The branch carrying `id`'s live context (the per-agent view).\npub fn agent_branch(id: &str) -> String {\n    format!(\"a-{id}\")\n}\n\n/// The branch carrying `project_id`'s per-project context (all agents).\npub fn project_branch(project_id: &str) -> String {\n    format!(\"p-{project_id}\")\n}\n\n/// Pure path of the seed bare repo under a `tracked/` root — the single\n/// layout authority (6.1) shared by [`ContextStore::open_or_init_in`] and the\n/// daemon's boot-time lock sweep (KH 1.3), which must reach the git-dir\n/// without opening the store.\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\npub fn seed_git_dir(root: &Path) -> PathBuf {\n    root.join(\".seed.git\")\n}\n\n/// Pure path of `id`'s live-context file under a `tracked/` root — the\n/// **read** form (no branch/worktree creation; readers must not mint store\n/// state). The single layout authority both the ensure-paths and every reader\n/// route through (6.1).\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\npub fn live_context_file(root: &Path, id: &str) -> PathBuf {\n    root.join(\"agents\").join(id).join(LIVE_CONTEXT_FILE)\n}\n\n/// Pure path of `id`'s live-**role** file under a `tracked/` root — the **read**\n/// form (no branch/worktree creation). Sibling of [`live_context_file`] in the\n/// same `a-<id>` worktree (`tracked/agents/<id>/live-role.md`). Single layout\n/// authority both the ensure-path and every reader route through (6.1).\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n// [impl->REQ-EP-7]\npub fn live_role_file(root: &Path, id: &str) -> PathBuf {\n    root.join(\"agents\").join(id).join(LIVE_ROLE_FILE)\n}\n\n/// Pure path of the per-(agent,project) context file under a `tracked/` root —\n/// the **read** form (no creation).\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\npub fn project_context_file(root: &Path, project_id: &str, id: &str) -> PathBuf {\n    root.join(\"projects\")\n        .join(project_id)\n        .join(id)\n        .join(PROJECT_CONTEXT_FILE)\n}\n\n/// First 8 hex chars of the SHA-256 of `bytes` — the deterministic conflict\n/// artifact name component (every node minting an artifact for the same\n/// incoming version produces the same name → recording is idempotent).\nfn content_hash8(bytes: &[u8]) -> String {\n    use sha2::{Digest, Sha256};\n    let digest = Sha256::digest(bytes);\n    digest[..4].iter().map(|b| format!(\"{b:02x}\")).collect()\n}\n\n/// The two-tier context store rooted at a `tracked/` directory.\npub struct ContextStore {\n    store: BranchStore,\n    root: PathBuf,\n}\n\nimpl ContextStore {\n    /// Open (or initialize) the node's canonical store at\n    /// [`tracked_dir`](crate::perch::tracked_dir).\n    pub fn open_or_init() -> std::io::Result<ContextStore> {\n        Self::open_or_init_in(&tracked_dir())\n    }\n\n    /// Open (or initialize) a store rooted at an explicit `tracked/` dir —\n    /// the testable seam.\n    // [impl->REQ-STORE-1]\n…\n    pub fn live_context_path(&self, id: &str) -> std::io::Result<PathBuf> {\n…\n        let file = live_context_file(&self.root, id);\n        let wt = file\n            .parent()\n            .expect(\"live-context file has a worktree parent\");\n        self.store.ensure_worktree(&agent_branch(id), wt)?;\n        Ok(file)\n    }\n\n    /// Ensure the `a-<id>` branch + worktree exist; return the live-**role**\n    /// file path inside it (`tracked/agents/<id>/live-role.md`). Shares the\n    /// `a-<id>` branch + worktree with [`live_context_path`], so a written role\n    /// rides the same [`commit_live`](Self::commit_live) and replicates to all\n    /// instances. The **only** writer is the `spt endpoint role --overwrite`\n    /// path (CONTEXT §live role; no automated writer resolves this).\n    // [impl->REQ-STORE-1]\n    // [impl->REQ-EP-7]\n    pub fn live_role_path(&self, id: &str) -> std::io::Result<PathBuf> {\n        Self::validated(id)?;\n        let file = live_role_file(&self.root, id);\n        let wt = file.parent().expect(\"live-role file has a worktree parent\");\n        self.store.ensure_worktree(&agent_branch(id), wt)?;\n        Ok(file)\n    }\n\n    /// Ensure the `p-<project_id>` branch + worktree exist; return the\n    /// per-(agent,project) context file path inside it\n    /// (`tracked/projects/<project_id>/<id>/project-context.md`). Both\n    /// components are charset-validated (4.6; project ids come from\n    /// [`crate::project::project_id_for_dir`], which emits the same charset).\n    // [impl->REQ-STORE-1]\n    pub fn project_context_path(&self, project_id: &str, id: &str) -> std::io::Result<PathBuf> {\n        Self::validated(project_id)?;\n        Self::validated(id)?;\n        let file = project_context_file(&self.root, project_id, id);\n        let wt = self.root.join(\"projects\").join(project_id);\n        self.store\n            .ensure_worktree(&project_branch(project_id), &wt)?;\n        std::fs::create_dir_all(\n            file.parent()\n                .expect(\"project-context file has an agent dir\"),\n        )?;\n        Ok(file)\n    }\n\n    /// Commit `id`'s live-context worktree — one checkpoint commit on `a-<id>`\n    /// (no-op when nothing changed). Call after a guarded write through\n    /// [`live_context_path`](Self::live_context_path).\n    // [impl->REQ-STORE-1]\n    pub fn commit_live(&self, id: &str, message: &str) -> std::io::Result<Option<String>> {\n        Self::validated(id)?;\n        let wt = self.root.join(\"agents\").join(id);\n        self.store.commit_in_worktree(&wt, &[], message)\n    }\n\n    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op\n    /// when nothing changed).\n    // [impl->REQ-STORE-1]\n    pub fn commit_project(\n        &self,\n        project_id: &str,\n        message: &str,\n    ) -> std::io::Result<Option<String>> {\n        Self::validated(project_id)?;\n        let wt = self.root.join(\"projects\").join(project_id);\n        let committed = self.store.commit_in_worktree(&wt, &[], message)?;\n        // A REAL checkpoint moved a p-* tip → project membership may have\n        // changed; a no-op write moved nothing and nudges nothing.\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n        if committed.is_some() {\n            self.nudge_index(crate::projinval::InvalScope::Global);\n        }\n        Ok(committed)\n    }\n\n    /// Record a surfaced concurrent-write conflict (ADR-0013, hazard 6.6):\n    /// persist the **incoming** version as a tracked artifact beside the local\n    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and\n    /// checkpoint it. The local working file stays untouched (the active mind\n    /// is never clobbered by an unresolved conflict), and the artifact\n    /// replicates like any context file, so conflict state itself syncs.\n    /// Deterministic content-hash naming makes recording idempotent across\n    /// nodes — every holder of the same pair mints the same artifact.\n    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]\n    pub fn record_conflict(\n        &self,\n        worktree: &Path,\n        file_rel: &str,\n        incoming: &[u8],\n    ) -> std::io::Result<PathBuf> {\n        let dir = worktree.join(CONFLICT_DIR);\n        std::fs::create_dir_all(&dir)?;\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! The two-tier context store — `tracked/` realized over [`BranchStore`]\n//! (M4-D6a; STORAGE §tracked/, ADR-0011, ADR-0013).\n//!\n//! Layout (the synced *mind*, physically separate from node-local `perches/`):\n//!\n//! ```text\n//! tracked/\n//!   .seed.git                    bare BranchStore repo (the one true object DB)\n//!   agents/<id>/                 linked worktree of branch  a-<id>\n//!     live-context.md            per-agent live context  → syncs to ALL instances\n//!   projects/<project_id>/       linked worktree of branch  p-<project_id>\n//!     <id>/project-context.md    per-(agent,project)      → same-project instances\n//! ```\n//!\n//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle\n//! exchange ships); worktrees are the read/write view. Tier routing is the\n//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind\n//! everywhere, the `p-` branch holds every agent's context for one project\n//! (cross-agent synthesis + query-routing ride that shape).\n//!\n//! Writes here are **paths + commits only** — the precedence-marker guard\n//! (6.5) stays in `spt-live::context`, which writes through the paths this\n//! store hands out, then calls the commit hook. Single writer per branch\n//! (active-instance-authoritative; 6.4).\n// [impl->REQ-STORE-1]\n\nuse std::path::{Path, PathBuf};\n\nuse crate::branchstore::BranchStore;\nuse crate::perch::tracked_dir;\nuse spt_proto::id::validate_endpoint_id;\n\n/// The per-agent live-context file inside an `a-<id>` worktree.\npub const LIVE_CONTEXT_FILE: &str = \"live-context.md\";\n/// The per-agent **live-role** file inside the same `a-<id>` worktree, beside\n/// [`LIVE_CONTEXT_FILE`] (CONTEXT §live role). A durable statement of the\n/// agent's broad purpose — replicates with the mind, renders FIRST at\n/// start-transition injection, and is written by **exactly one** path\n/// (`spt endpoint role --overwrite`); no automated writer ever touches it.\npub const LIVE_ROLE_FILE: &str = \"live-role.md\";\n/// Per-worktree directory holding surfaced concurrent-write conflict artifacts\n/// (ADR-0013; tracked, so conflict state replicates).\npub const CONFLICT_DIR: &str = \".conflicts\";\n/// The per-(agent,project) context file inside a `p-<project>` worktree,\n/// nested under the agent id.\npub const PROJECT_CONTEXT_FILE: &str = \"project-context.md\";\n\n/// The branch carrying `id`'s live context (the per-agent view).\npub fn agent_branch(id: &str) -> String {\n    format!(\"a-{id}\")\n}\n\n/// The branch carrying `project_id`'s per-project context (all agents).\npub fn project_branch(project_id: &str) -> String {\n    format!(\"p-{project_id}\")\n}\n\n/// Pure path of the seed bare repo under a `tracked/` root — the single\n/// layout authority (6.1) shared by [`ContextStore::open_or_init_in`] and the\n/// daemon's boot-time lock sweep (KH 1.3), which must reach the git-dir\n/// without opening the store.\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\npub fn seed_git_dir(root: &Path) -> PathBuf {\n    root.join(\".seed.git\")\n}\n\n/// Pure path of `id`'s live-context file under a `tracked/` root — the\n/// **read** form (no branch/worktree creation; readers must not mint store\n/// state). The single layout authority both the ensure-paths and every reader\n/// route through (6.1).\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\npub fn live_context_file(root: &Path, id: &str) -> PathBuf {\n    root.join(\"agents\").join(id).join(LIVE_CONTEXT_FILE)\n}\n\n/// Pure path of `id`'s live-**role** file under a `tracked/` root — the **read**\n/// form (no branch/worktree creation). Sibling of [`live_context_file`] in the\n/// same `a-<id>` worktree (`tracked/agents/<id>/live-role.md`). Single layout\n/// authority both the ensure-path and every reader route through (6.1).\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n// [impl->REQ-EP-7]\npub fn live_role_file(root: &Path, id: &str) -> PathBuf {\n    root.join(\"agents\").join(id).join(LIVE_ROLE_FILE)\n}\n\n/// Pure path of the per-(agent,project) context file under a `tracked/` root —\n/// the **read** form (no creation).\n// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\npub fn project_context_file(root: &Path, project_id: &str, id: &str) -> PathBuf {\n    root.join(\"projects\")\n        .join(project_id)\n        .join(id)\n        .join(PROJECT_CONTEXT_FILE)\n}\n\n/// First 8 hex chars of the SHA-256 of `bytes` — the deterministic conflict\n/// artifact name component (every node minting an artifact for the same\n/// incoming version produces the same name → recording is idempotent).\nfn content_hash8(bytes: &[u8]) -> String {\n    use sha2::{Digest, Sha256};\n    let digest = Sha256::digest(bytes);\n    digest[..4].iter().map(|b| format!(\"{b:02x}\")).collect()\n}\n\n/// The two-tier context store rooted at a `tracked/` directory.\npub struct ContextStore {\n    store: BranchStore,\n    root: PathBuf,\n}\n\nimpl ContextStore {\n    /// Open (or initialize) the node's canonical store at\n    /// [`tracked_dir`](crate::perch::tracked_dir).\n    pub fn open_or_init() -> std::io::Result<ContextStore> {\n        Self::open_or_init_in(&tracked_dir())\n    }\n\n    /// Open (or initialize) a store rooted at an explicit `tracked/` dir —\n    /// the testable seam.\n    // [impl->REQ-STORE-1]\n…\n    pub fn live_context_path(&self, id: &str) -> std::io::Result<PathBuf> {\n…\n        let file = live_context_file(&self.root, id);\n        let wt = file\n            .parent()\n            .expect(\"live-context file has a worktree parent\");\n        self.store.ensure_worktree(&agent_branch(id), wt)?;\n        Ok(file)\n    }\n\n    /// Ensure the `a-<id>` branch + worktree exist; return the live-**role**\n    /// file path inside it (`tracked/agents/<id>/live-role.md`). Shares the\n    /// `a-<id>` branch + worktree with [`live_context_path`], so a written role\n    /// rides the same [`commit_live`](Self::commit_live) and replicates to all\n    /// instances. The **only** writer is the `spt endpoint role --overwrite`\n    /// path (CONTEXT §live role; no automated writer resolves this).\n    // [impl->REQ-STORE-1]\n    // [impl->REQ-EP-7]\n    pub fn live_role_path(&self, id: &str) -> std::io::Result<PathBuf> {\n        Self::validated(id)?;\n        let file = live_role_file(&self.root, id);\n        let wt = file.parent().expect(\"live-role file has a worktree parent\");\n        self.store.ensure_worktree(&agent_branch(id), wt)?;\n        Ok(file)\n    }\n\n    /// Ensure the `p-<project_id>` branch + worktree exist; return the\n    /// per-(agent,project) context file path inside it\n    /// (`tracked/projects/<project_id>/<id>/project-context.md`). Both\n    /// components are charset-validated (4.6; project ids come from\n    /// [`crate::project::project_id_for_dir`], which emits the same charset).\n    // [impl->REQ-STORE-1]\n    pub fn project_context_path(&self, project_id: &str, id: &str) -> std::io::Result<PathBuf> {\n        Self::validated(project_id)?;\n        Self::validated(id)?;\n        let file = project_context_file(&self.root, project_id, id);\n        let wt = self.root.join(\"projects\").join(project_id);\n        self.store\n            .ensure_worktree(&project_branch(project_id), &wt)?;\n        std::fs::create_dir_all(\n            file.parent()\n                .expect(\"project-context file has an agent dir\"),\n        )?;\n        Ok(file)\n    }\n\n    /// Commit `id`'s live-context worktree — one checkpoint commit on `a-<id>`\n    /// (no-op when nothing changed). Call after a guarded write through\n    /// [`live_context_path`](Self::live_context_path).\n    // [impl->REQ-STORE-1]\n    pub fn commit_live(&self, id: &str, message: &str) -> std::io::Result<Option<String>> {\n        Self::validated(id)?;\n        let wt = self.root.join(\"agents\").join(id);\n        self.store.commit_in_worktree(&wt, &[], message)\n    }\n\n    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op\n    /// when nothing changed).\n    // [impl->REQ-STORE-1]\n    pub fn commit_project(\n        &self,\n        project_id: &str,\n        message: &str,\n    ) -> std::io::Result<Option<String>> {\n        Self::validated(project_id)?;\n        let wt = self.root.join(\"projects\").join(project_id);\n        let committed = self.store.commit_in_worktree(&wt, &[], message)?;\n        // A REAL checkpoint moved a p-* tip → project membership may have\n        // changed; a no-op write moved nothing and nudges nothing.\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n        if committed.is_some() {\n            self.nudge_index(crate::projinval::InvalScope::Global);\n        }\n        Ok(committed)\n    }\n\n    /// Record a surfaced concurrent-write conflict (ADR-0013, hazard 6.6):\n    /// persist the **incoming** version as a tracked artifact beside the local\n    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and\n    /// checkpoint it. The local working file stays untouched (the active mind\n    /// is never clobbered by an unresolved conflict), and the artifact\n    /// replicates like any context file, so conflict state itself syncs.\n    /// Deterministic content-hash naming makes recording idempotent across\n    /// nodes — every holder of the same pair mints the same artifact.\n    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]\n    pub fn record_conflict(\n        &self,\n        worktree: &Path,\n        file_rel: &str,\n        incoming: &[u8],\n    ) -> std::io::Result<PathBuf> {\n        let dir = worktree.join(CONFLICT_DIR);\n        std::fs::create_dir_all(&dir)?;\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,null,168,null,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,null,273,null,393]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\contextstore.rs"}}},"isError":false,"timestamp":1784271580001}}
{"type":"message","id":"c3c39d56","parentId":"ec8000aa","timestamp":"2026-07-17T06:59:40.004Z","message":{"role":"toolResult","toolCallId":"call_ozHD27jvTDWQInU0om8Bv67Z|fc_038f28d3e93bbfd4016a59d2dafdb48196a2ad03c0f604fcc5","toolName":"grep","content":[{"type":"text","text":" 7|//! tracked/\n*8|//!   .seed.git                    bare BranchStore repo (the one true object DB)\n*9|//!   agents/<id>/                 linked worktree of branch  a-<id>\n 10|//!     live-context.md            per-agent live context  → syncs to ALL instances\n*11|//!   projects/<project_id>/       linked worktree of branch  p-<project_id>\n 12|//!     <id>/project-context.md    per-(agent,project)      → same-project instances\n 13|//! ```\n 14|//!\n 15|//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle\n*16|//! exchange ships); worktrees are the read/write view. Tier routing is the\n 17|//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind\n 18|//! everywhere, the `p-` branch holds every agent's context for one project\n 19|//! (cross-agent synthesis + query-routing ride that shape).\n...\n 32|\n*33|/// The per-agent live-context file inside an `a-<id>` worktree.\n 34|pub const LIVE_CONTEXT_FILE: &str = \"live-context.md\";\n*35|/// The per-agent **live-role** file inside the same `a-<id>` worktree, beside\n 36|/// [`LIVE_CONTEXT_FILE`] (CONTEXT §live role). A durable statement of the\n 37|/// agent's broad purpose — replicates with the mind, renders FIRST at\n 38|/// start-transition injection, and is written by **exactly one** path\n...\n 40|pub const LIVE_ROLE_FILE: &str = \"live-role.md\";\n*41|/// Per-worktree directory holding surfaced concurrent-write conflict artifacts\n 42|/// (ADR-0013; tracked, so conflict state replicates).\n 43|pub const CONFLICT_DIR: &str = \".conflicts\";\n*44|/// The per-(agent,project) context file inside a `p-<project>` worktree,\n 45|/// nested under the agent id.\n 46|pub const PROJECT_CONTEXT_FILE: &str = \"project-context.md\";\n 47|\n...\n 62|// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n*63|pub fn seed_git_dir(root: &Path) -> PathBuf {\n*64|    root.join(\".seed.git\")\n 65|}\n 66|\n 67|/// Pure path of `id`'s live-context file under a `tracked/` root — the\n*68|/// **read** form (no branch/worktree creation; readers must not mint store\n 69|/// state). The single layout authority both the ensure-paths and every reader\n 70|/// route through (6.1).\n 71|// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n...\n 76|/// Pure path of `id`'s live-**role** file under a `tracked/` root — the **read**\n*77|/// form (no branch/worktree creation). Sibling of [`live_context_file`] in the\n*78|/// same `a-<id>` worktree (`tracked/agents/<id>/live-role.md`). Single layout\n 79|/// authority both the ensure-path and every reader route through (6.1).\n 80|// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n 81|// [impl->REQ-EP-7]\n...\n 105|/// The two-tier context store rooted at a `tracked/` directory.\n*106|pub struct ContextStore {\n 107|    store: BranchStore,\n 108|    root: PathBuf,\n 109|}\n...\n 120|    // [impl->REQ-STORE-1]\n*121|    pub fn open_or_init_in(root: &Path) -> std::io::Result<ContextStore> {\n*122|        let store = BranchStore::open_or_init(&seed_git_dir(root))?;\n 123|        Ok(ContextStore {\n 124|            store,\n 125|            root: root.to_path_buf(),\n...\n 162|\n*163|    /// Ensure the `a-<id>` branch + worktree exist; return the live-context\n 164|    /// file path inside it (`tracked/agents/<id>/live-context.md`). The id is\n*165|    /// charset-validated (4.6) — branch names and worktree dirs are derived\n 166|    /// from it.\n 167|    // [impl->REQ-STORE-1]\n 168|    pub fn live_context_path(&self, id: &str) -> std::io::Result<PathBuf> {\n...\n 172|            .parent()\n*173|            .expect(\"live-context file has a worktree parent\");\n*174|        self.store.ensure_worktree(&agent_branch(id), wt)?;\n 175|        Ok(file)\n 176|    }\n 177|\n*178|    /// Ensure the `a-<id>` branch + worktree exist; return the live-**role**\n 179|    /// file path inside it (`tracked/agents/<id>/live-role.md`). Shares the\n*180|    /// `a-<id>` branch + worktree with [`live_context_path`], so a written role\n 181|    /// rides the same [`commit_live`](Self::commit_live) and replicates to all\n 182|    /// instances. The **only** writer is the `spt endpoint role --overwrite`\n 183|    /// path (CONTEXT §live role; no automated writer resolves this).\n...\n 188|        let file = live_role_file(&self.root, id);\n*189|        let wt = file.parent().expect(\"live-role file has a worktree parent\");\n*190|        self.store.ensure_worktree(&agent_branch(id), wt)?;\n 191|        Ok(file)\n 192|    }\n 193|\n*194|    /// Ensure the `p-<project_id>` branch + worktree exist; return the\n 195|    /// per-(agent,project) context file path inside it\n 196|    /// (`tracked/projects/<project_id>/<id>/project-context.md`). Both\n 197|    /// components are charset-validated (4.6; project ids come from\n...\n 205|        self.store\n*206|            .ensure_worktree(&project_branch(project_id), &wt)?;\n 207|        std::fs::create_dir_all(\n 208|            file.parent()\n 209|                .expect(\"project-context file has an agent dir\"),\n...\n 213|\n*214|    /// Commit `id`'s live-context worktree — one checkpoint commit on `a-<id>`\n 215|    /// (no-op when nothing changed). Call after a guarded write through\n 216|    /// [`live_context_path`](Self::live_context_path).\n 217|    // [impl->REQ-STORE-1]\n...\n 220|        let wt = self.root.join(\"agents\").join(id);\n*221|        self.store.commit_in_worktree(&wt, &[], message)\n 222|    }\n 223|\n*224|    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op\n 225|    /// when nothing changed).\n 226|    // [impl->REQ-STORE-1]\n 227|    pub fn commit_project(\n...\n 233|        let wt = self.root.join(\"projects\").join(project_id);\n*234|        let committed = self.store.commit_in_worktree(&wt, &[], message)?;\n 235|        // A REAL checkpoint moved a p-* tip → project membership may have\n 236|        // changed; a no-op write moved nothing and nudges nothing.\n 237|        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n...\n 245|    /// persist the **incoming** version as a tracked artifact beside the local\n*246|    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and\n 247|    /// checkpoint it. The local working file stays untouched (the active mind\n 248|    /// is never clobbered by an unresolved conflict), and the artifact\n 249|    /// replicates like any context file, so conflict state itself syncs.\n...\n 254|        &self,\n*255|        worktree: &Path,\n 256|        file_rel: &str,\n 257|        incoming: &[u8],\n 258|    ) -> std::io::Result<PathBuf> {\n*259|        let dir = worktree.join(CONFLICT_DIR);\n 260|        std::fs::create_dir_all(&dir)?;\n 261|        let name = format!(\n 262|            \"{}.{}.md\",\n...\n 270|        self.store\n*271|            .commit_in_worktree(worktree, &[], &format!(\"conflict: {file_rel} ({name})\"))?;\n 272|        Ok(path)\n 273|    }\n 274|\n*275|    /// The surfaced-and-unresolved conflicts in a worktree (artifact paths for\n 276|    /// `file_rel`, or all when `None`). Empty = no conflicts pending.\n 277|    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]\n 278|    pub fn list_conflicts(\n 279|        &self,\n*280|        worktree: &Path,\n 281|        file_rel: Option<&str>,\n 282|    ) -> std::io::Result<Vec<PathBuf>> {\n*283|        let dir = worktree.join(CONFLICT_DIR);\n 284|        let mut out = Vec::new();\n 285|        let Ok(entries) = std::fs::read_dir(&dir) else {\n 286|            return Ok(out); // no dir = no conflicts\n...\n 303|    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]\n*304|    pub fn clear_conflicts(&self, worktree: &Path, file_rel: &str) -> std::io::Result<usize> {\n*305|        let paths = self.list_conflicts(worktree, Some(file_rel))?;\n 306|        if paths.is_empty() {\n 307|            return Ok(0);\n 308|        }\n...\n 312|        self.store\n*313|            .commit_in_worktree(worktree, &[], &format!(\"conflict resolved: {file_rel}\"))?;\n 314|        Ok(paths.len())\n 315|    }\n 316|\n 317|    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the\n*318|    /// D3f-documented seam): move the `a-<old>` branch + its worktree to\n*319|    /// `a-<new>`, and rename `<old>/` → `<new>/` inside every project worktree\n 320|    /// (one rename commit per touched project). Refuses when `a-<new>` already\n 321|    /// exists (never merge two minds); an absent `a-<old>` is a no-op `Ok`\n 322|    /// (nothing tracked yet — perch-only endpoints rename cleanly).\n...\n 333|\n*334|        // a- arm: move worktree dir, then rename the branch (git updates the\n*335|        // worktree's HEAD symref). Skip when the old branch never existed.\n 336|        if self.store.has_branch(&agent_branch(old_id))? {\n 337|            let old_wt = self.root.join(\"agents\").join(old_id);\n 338|            let new_wt = self.root.join(\"agents\").join(new_id);\n...\n 342|                        \"-C\",\n*343|                        &self.root.join(\".seed.git\").to_string_lossy(),\n*344|                        \"worktree\",\n 345|                        \"move\",\n 346|                        &old_wt.to_string_lossy(),\n 347|                        &new_wt.to_string_lossy(),\n...\n 355|                    \"-C\",\n*356|                    &self.root.join(\".seed.git\").to_string_lossy(),\n 357|                    \"branch\",\n 358|                    \"-m\",\n 359|                    &agent_branch(old_id),\n...\n 366|\n*367|        // p- arm: rename the agent dir inside every project worktree holding one.\n 368|        let projects = self.root.join(\"projects\");\n 369|        if let Ok(entries) = std::fs::read_dir(&projects) {\n 370|            for entry in entries.flatten() {\n...\n 376|                self.store\n*377|                    .commit_in_worktree(&wt, &[], &format!(\"rename {old_id} -> {new_id}\"))?;\n 378|            }\n 379|        }\n 380|        // Membership rows moved id: refresh both affected rows + the global\n...\n 399|    /// as fresh seed commits on `a-<new>` / under `<new>/` in each project\n*400|    /// worktree (**no shared history, no ongoing sync**: a fork is a new\n 401|    /// identity, not an instance). Conflict artifacts are deliberately not\n 402|    /// copied — the fork starts clean. Refuses when `a-<new>` already exists\n 403|    /// (never merge two minds — the rename rule); a source with no tracked\n...\n 427|\n*428|        // Project tiers: copy src's slice in every project worktree holding one.\n 429|        let projects = self.root.join(\"projects\");\n 430|        if let Ok(entries) = std::fs::read_dir(&projects) {\n 431|            for entry in entries.flatten() {\n...\n 456|    /// `spt fork` (and nothing else: deletion is explicit, never implied).\n*457|    /// Removes the `a-<id>` worktree + branch and `<id>/` inside every\n*458|    /// project worktree (one removal commit per touched project). Absent\n 459|    /// pieces are no-ops — removing exactly what exists, idempotently.\n 460|    // [impl->REQ-INST-15]\n 461|    pub fn remove_endpoint(&self, id: &str) -> std::io::Result<()> {\n...\n 468|                        \"-C\",\n*469|                        &self.root.join(\".seed.git\").to_string_lossy(),\n*470|                        \"worktree\",\n 471|                        \"remove\",\n 472|                        \"--force\",\n 473|                        &wt.to_string_lossy(),\n...\n 481|                    \"-C\",\n*482|                    &self.root.join(\".seed.git\").to_string_lossy(),\n 483|                    \"branch\",\n 484|                    \"-D\",\n 485|                    &agent_branch(id),\n...\n 498|                crate::gitrun::run_git_ok(&[\"rm\", \"-r\", id], Some(&wt), None)?;\n*499|                self.store.commit_in_worktree(\n 500|                    &wt,\n 501|                    &[],\n 502|                    &format!(\"remove {id} (fork source deleted)\"),\n...\n 576|\n*577|    #[test] // [unit->REQ-INST-11] rename moves the a- branch + worktree and ripples every p- agent dir; target collision refuses.\n 578|    fn rename_ripples_both_tiers() {\n 579|        let tmp = tempdir().unwrap();\n 580|        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();\n...\n 589|\n*590|        // a- arm: branch + worktree moved, content intact, old gone.\n 591|        assert!(cs\n 592|            .branch_store()\n 593|            .has_branch(&agent_branch(\"arthur\"))\n...\n 604|\n*605|        // p- arm: agent dir renamed inside the project worktree + committed.\n 606|        assert_eq!(\n 607|            cs.branch_store()\n 608|                .read_at_tip(&project_branch(\"spt-core\"), \"arthur/project-context.md\")\n...\n 688|    // [unit->REQ-INST-15] delete-source removes EXACTLY the source: its a-\n*689|    // branch + worktree and its project slices go; the fork's mind and other\n 690|    // agents' slices stay. Idempotent on an already-removed id.\n 691|    #[test]\n 692|    fn remove_endpoint_removes_exactly_the_source() {\n...\n 713|            !tmp.path().join(\"agents\").join(\"doyle\").exists(),\n*714|            \"worktree gone\"\n 715|        );\n 716|        assert!(\n 717|            bs.read_at_tip(&project_branch(\"spt-core\"), \"doyle/project-context.md\")"}],"details":{"scopePath":"crates/spt-store/src/contextstore.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\contextstore.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":62,"fileCount":1,"files":["crates/spt-store/src/contextstore.rs"],"fileMatches":[{"path":"crates/spt-store/src/contextstore.rs","count":62}],"truncated":false,"displayContent":"   7│//! tracked/\n  *8│//!   .seed.git                    bare BranchStore repo (the one true object DB)\n  *9│//!   agents/<id>/                 linked worktree of branch  a-<id>\n  10│//!     live-context.md            per-agent live context  → syncs to ALL instances\n *11│//!   projects/<project_id>/       linked worktree of branch  p-<project_id>\n  12│//!     <id>/project-context.md    per-(agent,project)      → same-project instances\n  13│//! ```\n  14│//!\n  15│//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle\n *16│//! exchange ships); worktrees are the read/write view. Tier routing is the\n  17│//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind\n  18│//! everywhere, the `p-` branch holds every agent's context for one project\n  19│//! (cross-agent synthesis + query-routing ride that shape).\n    │...\n  32│\n *33│/// The per-agent live-context file inside an `a-<id>` worktree.\n  34│pub const LIVE_CONTEXT_FILE: &str = \"live-context.md\";\n *35│/// The per-agent **live-role** file inside the same `a-<id>` worktree, beside\n  36│/// [`LIVE_CONTEXT_FILE`] (CONTEXT §live role). A durable statement of the\n  37│/// agent's broad purpose — replicates with the mind, renders FIRST at\n  38│/// start-transition injection, and is written by **exactly one** path\n    │...\n  40│pub const LIVE_ROLE_FILE: &str = \"live-role.md\";\n *41│/// Per-worktree directory holding surfaced concurrent-write conflict artifacts\n  42│/// (ADR-0013; tracked, so conflict state replicates).\n  43│pub const CONFLICT_DIR: &str = \".conflicts\";\n *44│/// The per-(agent,project) context file inside a `p-<project>` worktree,\n  45│/// nested under the agent id.\n  46│pub const PROJECT_CONTEXT_FILE: &str = \"project-context.md\";\n  47│\n    │...\n  62│// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n *63│pub fn seed_git_dir(root: &Path) -> PathBuf {\n *64│    root.join(\".seed.git\")\n  65│}\n  66│\n  67│/// Pure path of `id`'s live-context file under a `tracked/` root — the\n *68│/// **read** form (no branch/worktree creation; readers must not mint store\n  69│/// state). The single layout authority both the ensure-paths and every reader\n  70│/// route through (6.1).\n  71│// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n    │...\n  76│/// Pure path of `id`'s live-**role** file under a `tracked/` root — the **read**\n *77│/// form (no branch/worktree creation). Sibling of [`live_context_file`] in the\n *78│/// same `a-<id>` worktree (`tracked/agents/<id>/live-role.md`). Single layout\n  79│/// authority both the ensure-path and every reader route through (6.1).\n  80│// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n  81│// [impl->REQ-EP-7]\n    │...\n 105│/// The two-tier context store rooted at a `tracked/` directory.\n*106│pub struct ContextStore {\n 107│    store: BranchStore,\n 108│    root: PathBuf,\n 109│}\n    │...\n 120│    // [impl->REQ-STORE-1]\n*121│    pub fn open_or_init_in(root: &Path) -> std::io::Result<ContextStore> {\n*122│        let store = BranchStore::open_or_init(&seed_git_dir(root))?;\n 123│        Ok(ContextStore {\n 124│            store,\n 125│            root: root.to_path_buf(),\n    │...\n 162│\n*163│    /// Ensure the `a-<id>` branch + worktree exist; return the live-context\n 164│    /// file path inside it (`tracked/agents/<id>/live-context.md`). The id is\n*165│    /// charset-validated (4.6) — branch names and worktree dirs are derived\n 166│    /// from it.\n 167│    // [impl->REQ-STORE-1]\n 168│    pub fn live_context_path(&self, id: &str) -> std::io::Result<PathBuf> {\n    │...\n 172│            .parent()\n*173│            .expect(\"live-context file has a worktree parent\");\n*174│        self.store.ensure_worktree(&agent_branch(id), wt)?;\n 175│        Ok(file)\n 176│    }\n 177│\n*178│    /// Ensure the `a-<id>` branch + worktree exist; return the live-**role**\n 179│    /// file path inside it (`tracked/agents/<id>/live-role.md`). Shares the\n*180│    /// `a-<id>` branch + worktree with [`live_context_path`], so a written role\n 181│    /// rides the same [`commit_live`](Self::commit_live) and replicates to all\n 182│    /// instances. The **only** writer is the `spt endpoint role --overwrite`\n 183│    /// path (CONTEXT §live role; no automated writer resolves this).\n    │...\n 188│        let file = live_role_file(&self.root, id);\n*189│        let wt = file.parent().expect(\"live-role file has a worktree parent\");\n*190│        self.store.ensure_worktree(&agent_branch(id), wt)?;\n 191│        Ok(file)\n 192│    }\n 193│\n*194│    /// Ensure the `p-<project_id>` branch + worktree exist; return the\n 195│    /// per-(agent,project) context file path inside it\n 196│    /// (`tracked/projects/<project_id>/<id>/project-context.md`). Both\n 197│    /// components are charset-validated (4.6; project ids come from\n    │...\n 205│        self.store\n*206│            .ensure_worktree(&project_branch(project_id), &wt)?;\n 207│        std::fs::create_dir_all(\n 208│            file.parent()\n 209│                .expect(\"project-context file has an agent dir\"),\n    │...\n 213│\n*214│    /// Commit `id`'s live-context worktree — one checkpoint commit on `a-<id>`\n 215│    /// (no-op when nothing changed). Call after a guarded write through\n 216│    /// [`live_context_path`](Self::live_context_path).\n 217│    // [impl->REQ-STORE-1]\n    │...\n 220│        let wt = self.root.join(\"agents\").join(id);\n*221│        self.store.commit_in_worktree(&wt, &[], message)\n 222│    }\n 223│\n*224│    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op\n 225│    /// when nothing changed).\n 226│    // [impl->REQ-STORE-1]\n 227│    pub fn commit_project(\n    │...\n 233│        let wt = self.root.join(\"projects\").join(project_id);\n*234│        let committed = self.store.commit_in_worktree(&wt, &[], message)?;\n 235│        // A REAL checkpoint moved a p-* tip → project membership may have\n 236│        // changed; a no-op write moved nothing and nudges nothing.\n 237│        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n    │...\n 245│    /// persist the **incoming** version as a tracked artifact beside the local\n*246│    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and\n 247│    /// checkpoint it. The local working file stays untouched (the active mind\n 248│    /// is never clobbered by an unresolved conflict), and the artifact\n 249│    /// replicates like any context file, so conflict state itself syncs.\n    │...\n 254│        &self,\n*255│        worktree: &Path,\n 256│        file_rel: &str,\n 257│        incoming: &[u8],\n 258│    ) -> std::io::Result<PathBuf> {\n*259│        let dir = worktree.join(CONFLICT_DIR);\n 260│        std::fs::create_dir_all(&dir)?;\n 261│        let name = format!(\n 262│            \"{}.{}.md\",\n    │...\n 270│        self.store\n*271│            .commit_in_worktree(worktree, &[], &format!(\"conflict: {file_rel} ({name})\"))?;\n 272│        Ok(path)\n 273│    }\n 274│\n*275│    /// The surfaced-and-unresolved conflicts in a worktree (artifact paths for\n 276│    /// `file_rel`, or all when `None`). Empty = no conflicts pending.\n 277│    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]\n 278│    pub fn list_conflicts(\n 279│        &self,\n*280│        worktree: &Path,\n 281│        file_rel: Option<&str>,\n 282│    ) -> std::io::Result<Vec<PathBuf>> {\n*283│        let dir = worktree.join(CONFLICT_DIR);\n 284│        let mut out = Vec::new();\n 285│        let Ok(entries) = std::fs::read_dir(&dir) else {\n 286│            return Ok(out); // no dir = no conflicts\n    │...\n 303│    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]\n*304│    pub fn clear_conflicts(&self, worktree: &Path, file_rel: &str) -> std::io::Result<usize> {\n*305│        let paths = self.list_conflicts(worktree, Some(file_rel))?;\n 306│        if paths.is_empty() {\n 307│            return Ok(0);\n 308│        }\n    │...\n 312│        self.store\n*313│            .commit_in_worktree(worktree, &[], &format!(\"conflict resolved: {file_rel}\"))?;\n 314│        Ok(paths.len())\n 315│    }\n 316│\n 317│    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the\n*318│    /// D3f-documented seam): move the `a-<old>` branch + its worktree to\n*319│    /// `a-<new>`, and rename `<old>/` → `<new>/` inside every project worktree\n 320│    /// (one rename commit per touched project). Refuses when `a-<new>` already\n 321│    /// exists (never merge two minds); an absent `a-<old>` is a no-op `Ok`\n 322│    /// (nothing tracked yet — perch-only endpoints rename cleanly).\n    │...\n 333│\n*334│        // a- arm: move worktree dir, then rename the branch (git updates the\n*335│        // worktree's HEAD symref). Skip when the old branch never existed.\n 336│        if self.store.has_branch(&agent_branch(old_id))? {\n 337│            let old_wt = self.root.join(\"agents\").join(old_id);\n 338│            let new_wt = self.root.join(\"agents\").join(new_id);\n    │...\n 342│                        \"-C\",\n*343│                        &self.root.join(\".seed.git\").to_string_lossy(),\n*344│                        \"worktree\",\n 345│                        \"move\",\n 346│                        &old_wt.to_string_lossy(),\n 347│                        &new_wt.to_string_lossy(),\n    │...\n 355│                    \"-C\",\n*356│                    &self.root.join(\".seed.git\").to_string_lossy(),\n 357│                    \"branch\",\n 358│                    \"-m\",\n 359│                    &agent_branch(old_id),\n    │...\n 366│\n*367│        // p- arm: rename the agent dir inside every project worktree holding one.\n 368│        let projects = self.root.join(\"projects\");\n 369│        if let Ok(entries) = std::fs::read_dir(&projects) {\n 370│            for entry in entries.flatten() {\n    │...\n 376│                self.store\n*377│                    .commit_in_worktree(&wt, &[], &format!(\"rename {old_id} -> {new_id}\"))?;\n 378│            }\n 379│        }\n 380│        // Membership rows moved id: refresh both affected rows + the global\n    │...\n 399│    /// as fresh seed commits on `a-<new>` / under `<new>/` in each project\n*400│    /// worktree (**no shared history, no ongoing sync**: a fork is a new\n 401│    /// identity, not an instance). Conflict artifacts are deliberately not\n 402│    /// copied — the fork starts clean. Refuses when `a-<new>` already exists\n 403│    /// (never merge two minds — the rename rule); a source with no tracked\n    │...\n 427│\n*428│        // Project tiers: copy src's slice in every project worktree holding one.\n 429│        let projects = self.root.join(\"projects\");\n 430│        if let Ok(entries) = std::fs::read_dir(&projects) {\n 431│            for entry in entries.flatten() {\n    │...\n 456│    /// `spt fork` (and nothing else: deletion is explicit, never implied).\n*457│    /// Removes the `a-<id>` worktree + branch and `<id>/` inside every\n*458│    /// project worktree (one removal commit per touched project). Absent\n 459│    /// pieces are no-ops — removing exactly what exists, idempotently.\n 460│    // [impl->REQ-INST-15]\n 461│    pub fn remove_endpoint(&self, id: &str) -> std::io::Result<()> {\n    │...\n 468│                        \"-C\",\n*469│                        &self.root.join(\".seed.git\").to_string_lossy(),\n*470│                        \"worktree\",\n 471│                        \"remove\",\n 472│                        \"--force\",\n 473│                        &wt.to_string_lossy(),\n    │...\n 481│                    \"-C\",\n*482│                    &self.root.join(\".seed.git\").to_string_lossy(),\n 483│                    \"branch\",\n 484│                    \"-D\",\n 485│                    &agent_branch(id),\n    │...\n 498│                crate::gitrun::run_git_ok(&[\"rm\", \"-r\", id], Some(&wt), None)?;\n*499│                self.store.commit_in_worktree(\n 500│                    &wt,\n 501│                    &[],\n 502│                    &format!(\"remove {id} (fork source deleted)\"),\n    │...\n 576│\n*577│    #[test] // [unit->REQ-INST-11] rename moves the a- branch + worktree and ripples every p- agent dir; target collision refuses.\n 578│    fn rename_ripples_both_tiers() {\n 579│        let tmp = tempdir().unwrap();\n 580│        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();\n    │...\n 589│\n*590│        // a- arm: branch + worktree moved, content intact, old gone.\n 591│        assert!(cs\n 592│            .branch_store()\n 593│            .has_branch(&agent_branch(\"arthur\"))\n    │...\n 604│\n*605│        // p- arm: agent dir renamed inside the project worktree + committed.\n 606│        assert_eq!(\n 607│            cs.branch_store()\n 608│                .read_at_tip(&project_branch(\"spt-core\"), \"arthur/project-context.md\")\n    │...\n 688│    // [unit->REQ-INST-15] delete-source removes EXACTLY the source: its a-\n*689│    // branch + worktree and its project slices go; the fork's mind and other\n 690│    // agents' slices stay. Idempotent on an already-removed id.\n 691│    #[test]\n 692│    fn remove_endpoint_removes_exactly_the_source() {\n    │...\n 713│            !tmp.path().join(\"agents\").join(\"doyle\").exists(),\n*714│            \"worktree gone\"\n 715│        );\n 716│        assert!(\n 717│            bs.read_at_tip(&project_branch(\"spt-core\"), \"doyle/project-context.md\")"},"isError":false,"timestamp":1784271580004}}
{"type":"message","id":"3333c80b","parentId":"c3c39d56","timestamp":"2026-07-17T06:59:40.013Z","message":{"role":"toolResult","toolCallId":"call_FAauOuSBqfHq9xFJj2B4Rj2l|fc_038f28d3e93bbfd4016a59d2dafdc881969e867e3dbac53362","toolName":"read","content":[{"type":"text","text":"/// `policy` **here**, fail-closed (REQ-INST-5: a peer must not be able to pull\n/// a hidden/unsynced mind by naming it). `scratch_dir` holds the transient\n/// bundle file.\n// [impl->REQ-NET-3]\n// [impl->REQ-INST-5]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\npub fn serve_sync(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin_node: &str,\n    net_from_seq: u64,\n    cs: &ContextStore,\n    policy: &SyncPolicy,\n    scratch_dir: &Path,\n) -> io::Result<SyncServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    let mut decoder = SyncDecoder::new();\n\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                let Some(rec) = decoder.push(&bytes).into_iter().next() else {\n                    continue;\n                };\n                let SyncRecord::Request {\n                    sync_id,\n                    refs,\n                    have_tips,\n                } = rec\n                else {\n                    continue; // not a request — protocol noise, keep waiting\n                };\n\n                // The gate: partition requested refs under the proven origin.\n                let (allowed, denied): (Vec<String>, Vec<String>) = refs\n                    .into_iter()\n                    .partition(|r| policy.allows(r, origin_node));\n                if !denied.is_empty() {\n                    let line = SyncRecord::Deny {\n                        sync_id: sync_id.clone(),\n                        refs: denied.clone(),\n                        message: \"refused: not synced/visible to this origin\".to_string(),\n                    }\n                    .encode_line();\n                    brain.net_stream_send(stream_id, &line, None, false)?;\n                }\n\n                // Incremental bundle for the allowed refs (absent branches and\n                // up-to-date tips are skipped inside create_bundle).\n                let wants: Vec<(String, Option<String>)> = allowed\n                    .iter()\n                    .map(|r| (r.clone(), have_tips.get(r).cloned()))\n                    .collect();\n                std::fs::create_dir_all(scratch_dir)?;\n                let bundle = scratch_dir.join(format!(\"serve-{}.bundle\", safe_stem(&sync_id)));\n                let included = match cs.branch_store().create_bundle(&bundle, &wants) {\n                    Ok(i) => i,\n                    Err(e) => {\n                        let line = SyncRecord::Err {\n                            sync_id: sync_id.clone(),\n                            message: format!(\"bundle failed: {e}\"),\n                        }\n                        .encode_line();\n                        brain.net_stream_send(stream_id, &line, None, true)?;\n                        return Ok(SyncServeOutcome::Failed(e.to_string()));\n                    }\n                };\n                if included.is_empty() {\n                    let line = SyncRecord::Done {\n                        sync_id,\n                        total: 0,\n                        refs: Vec::new(),\n                    }\n                    .encode_line();\n                    brain.net_stream_send(stream_id, &line, None, true)?;\n                    return Ok(SyncServeOutcome::UpToDate { denied });\n                }\n\n                // Pump the bundle file as positional chunks (fire-and-forget;\n                // the offset is the dedup, not a send journal — D5b lesson).\n                let bytes_total = {\n                    let mut file = std::fs::File::open(&bundle)?;\n                    let total = file.metadata()?.len();\n                    let mut offset = 0u64;\n                    let mut buf = vec![0u8; CHUNK_BYTES];\n                    loop {\n                        use std::io::Read;\n                        let n = file.read(&mut buf)?;\n                        if n == 0 {\n                            break;\n                        }\n                        let line = SyncRecord::Chunk {\n                            sync_id: sync_id.clone(),\n                            offset,\n                            data_b64: encode_bytes(&buf[..n]),\n                        }\n                        .encode_line();\n                        brain.net_stream_send(stream_id, &line, None, false)?;\n                        offset += n as u64;\n                    }\n                    total\n                };\n                let _ = std::fs::remove_file(&bundle);\n                let done = SyncRecord::Done {\n                    sync_id,\n                    total: bytes_total,\n                    refs: included.clone(),\n                }\n                .encode_line();\n                brain.net_stream_send(stream_id, &done, None, true)?;\n                return Ok(SyncServeOutcome::Sent {\n                    bytes: bytes_total,\n                    refs: included,\n                    denied,\n                });\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n…\n            }\n…\n        }\n    }\n}\n…\n/// The refs this node should request from `peer_node`: every local `a-<id>`\n/// branch the policy allows toward that peer (symmetric scoping — don't ask\n/// for a mind the peer's own gate would refuse to receive elsewhere) plus\n/// every hosted `p-<project>` branch. The pull model's requester half:\n/// two-tier scoping with no new registry fields (ADR-0013).\n// [impl->REQ-INST-5]\npub fn select_refs(\n    policy: &SyncPolicy,\n    cs: &ContextStore,\n    peer_node: &str,\n) -> io::Result<Vec<String>> {\n    Ok(cs\n        .branch_store()\n        .branches()?\n        .into_iter()\n        .filter(|b| policy.allows(b, peer_node))\n        .collect())\n}\n\n/// Requester side: pull `refs` from the peer over `conn_id`, apply each\n/// fetched tip with the vector driver, and report per-branch verdicts.\n/// `open_op` is the durable id for the exactly-once stream open; everything\n/// after rides fire-and-forget. A torn pull errors — recovery is a fresh\n/// `request_sync` (apply is idempotent; FAULT-MATRIX stream-loss row).\n// [impl->REQ-NET-3]\npub fn request_sync(\n    brain: &mut Brain,\n    conn_id: u64,\n    refs: &[String],\n    open_op: MintedOp,\n    cs: &ContextStore,\n    scratch_dir: &Path,\n) -> io::Result<SyncPullReport> {\n    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n    let stream_id = opened.stream_id;\n    brain.net_stream_subscribe(stream_id, 0)?;\n\n    let sync_id = format!(\"pull-{}\", open_op.seq);\n    let mut have_tips = BTreeMap::new();\n    for r in refs {\n        if let Some(tip) = cs.branch_store().tip(r)? {\n            have_tips.insert(r.clone(), tip);\n        }\n    }\n    let line = SyncRecord::Request {\n        sync_id: sync_id.clone(),\n        refs: refs.to_vec(),\n        have_tips,\n    }\n    .encode_line();\n    brain.net_stream_send(stream_id, &line, None, true)?;\n\n    std::fs::create_dir_all(scratch_dir)?;\n    let bundle: PathBuf = scratch_dir.join(format!(\"recv-{}.bundle\", safe_stem(&sync_id)));\n    let mut file = Some(\n        std::fs::OpenOptions::new()\n            .create(true)\n            .truncate(true)\n            .write(true)\n            .open(&bundle)?,\n    );\n    let mut report = SyncPullReport::default();\n    let mut decoder = SyncDecoder::new();\n\n    // Peer-reply reads use the DECOUPLED reply-read budget + reclassifying read\n    // (REQ-PUMP-DIAL-FASTFAIL): the LATENT TWIN of request_update's wedge — a\n    // non-empty want-set against a connect-then-silent peer would burn the 30s\n    // carrier deadline into a whole-round poison. Drop that peer PROMPTLY instead.\n    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n    let mut deadline = brain.reply_read_deadline();\n    loop {\n        let ev = brain.read_peer_reply_until(deadline)?;\n        // Re-arm the budget on each chunk that arrives on OUR stream: a whole\n        // reply-read budget of NO progress is a wedge; a streaming pull that keeps\n        // delivering stays healthy however long it runs (REQ-HAZARD-PUMP-IPC-DEADLINE).\n        if matches!(&ev, BrokerEvent::NetStreamData { stream_id: sid, .. } if *sid == stream_id) {\n            deadline = brain.reply_read_deadline();\n        }\n        match ev {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        SyncRecord::Deny {\n                            sync_id: xid, refs, ..\n                        } if xid == sync_id => {\n                            report.denied.extend(refs);\n                        }\n                        SyncRecord::Chunk {\n                            sync_id: xid,\n                            offset,\n                            data_b64,\n                        } if xid == sync_id => {\n                            let chunk = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            let f = file\n                                .as_mut()\n                                .ok_or_else(|| io::Error::other(\"chunk after Done\"))?;\n                            f.seek(SeekFrom::Start(offset))?;\n                            f.write_all(&chunk)?;\n                        }\n                        SyncRecord::Done {\n                            sync_id: xid,\n                            total,\n                            refs,\n                        } if xid == sync_id => {\n                            // Close our handle before git reads the bundle.\n                            if let Some(mut f) = file.take() {\n                                f.flush()?;\n                            }\n                            if total == 0 {\n                                let _ = std::fs::remove_file(&bundle);\n                                return Ok(report); // everything already up to date\n                            }\n                            let got = std::fs::metadata(&bundle)?.len();\n                            if got != total {\n                                let _ = std::fs::remove_file(&bundle);\n                                return Err(io::Error::new(\n                                    io::ErrorKind::UnexpectedEof,\n                                    format!(\"bundle incomplete: {got} of {total} bytes\"),\n                                ));\n                            }\n                            let tips = cs.branch_store().fetch_bundle(&bundle)?;\n                            let _ = std::fs::remove_file(&bundle);\n                            for branch in &refs {\n                                if let Some(tip) = tips.get(branch) {\n                                    report.applied.push(apply_fetched(cs, branch, tip)?);\n                                }\n                            }\n                            // PROJECT-INDEX W3 (the W2-carried gap, ruled at\n                            // cutover): a sync pull moves `p-*` tips on the RAW\n                            // BranchStore — `apply_fetched`'s adopt leg can even\n                            // CREATE a p-* branch (a membership change) — and\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }\n}"}],"details":{"displayContent":{"text":"/// `policy` **here**, fail-closed (REQ-INST-5: a peer must not be able to pull\n/// a hidden/unsynced mind by naming it). `scratch_dir` holds the transient\n/// bundle file.\n// [impl->REQ-NET-3]\n// [impl->REQ-INST-5]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\npub fn serve_sync(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin_node: &str,\n    net_from_seq: u64,\n    cs: &ContextStore,\n    policy: &SyncPolicy,\n    scratch_dir: &Path,\n) -> io::Result<SyncServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    let mut decoder = SyncDecoder::new();\n\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                let Some(rec) = decoder.push(&bytes).into_iter().next() else {\n                    continue;\n                };\n                let SyncRecord::Request {\n                    sync_id,\n                    refs,\n                    have_tips,\n                } = rec\n                else {\n                    continue; // not a request — protocol noise, keep waiting\n                };\n\n                // The gate: partition requested refs under the proven origin.\n                let (allowed, denied): (Vec<String>, Vec<String>) = refs\n                    .into_iter()\n                    .partition(|r| policy.allows(r, origin_node));\n                if !denied.is_empty() {\n                    let line = SyncRecord::Deny {\n                        sync_id: sync_id.clone(),\n                        refs: denied.clone(),\n                        message: \"refused: not synced/visible to this origin\".to_string(),\n                    }\n                    .encode_line();\n                    brain.net_stream_send(stream_id, &line, None, false)?;\n                }\n\n                // Incremental bundle for the allowed refs (absent branches and\n                // up-to-date tips are skipped inside create_bundle).\n                let wants: Vec<(String, Option<String>)> = allowed\n                    .iter()\n                    .map(|r| (r.clone(), have_tips.get(r).cloned()))\n                    .collect();\n                std::fs::create_dir_all(scratch_dir)?;\n                let bundle = scratch_dir.join(format!(\"serve-{}.bundle\", safe_stem(&sync_id)));\n                let included = match cs.branch_store().create_bundle(&bundle, &wants) {\n                    Ok(i) => i,\n                    Err(e) => {\n                        let line = SyncRecord::Err {\n                            sync_id: sync_id.clone(),\n                            message: format!(\"bundle failed: {e}\"),\n                        }\n                        .encode_line();\n                        brain.net_stream_send(stream_id, &line, None, true)?;\n                        return Ok(SyncServeOutcome::Failed(e.to_string()));\n                    }\n                };\n                if included.is_empty() {\n                    let line = SyncRecord::Done {\n                        sync_id,\n                        total: 0,\n                        refs: Vec::new(),\n                    }\n                    .encode_line();\n                    brain.net_stream_send(stream_id, &line, None, true)?;\n                    return Ok(SyncServeOutcome::UpToDate { denied });\n                }\n\n                // Pump the bundle file as positional chunks (fire-and-forget;\n                // the offset is the dedup, not a send journal — D5b lesson).\n                let bytes_total = {\n                    let mut file = std::fs::File::open(&bundle)?;\n                    let total = file.metadata()?.len();\n                    let mut offset = 0u64;\n                    let mut buf = vec![0u8; CHUNK_BYTES];\n                    loop {\n                        use std::io::Read;\n                        let n = file.read(&mut buf)?;\n                        if n == 0 {\n                            break;\n                        }\n                        let line = SyncRecord::Chunk {\n                            sync_id: sync_id.clone(),\n                            offset,\n                            data_b64: encode_bytes(&buf[..n]),\n                        }\n                        .encode_line();\n                        brain.net_stream_send(stream_id, &line, None, false)?;\n                        offset += n as u64;\n                    }\n                    total\n                };\n                let _ = std::fs::remove_file(&bundle);\n                let done = SyncRecord::Done {\n                    sync_id,\n                    total: bytes_total,\n                    refs: included.clone(),\n                }\n                .encode_line();\n                brain.net_stream_send(stream_id, &done, None, true)?;\n                return Ok(SyncServeOutcome::Sent {\n                    bytes: bytes_total,\n                    refs: included,\n                    denied,\n                });\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n…\n            }\n…\n        }\n    }\n}\n…\n/// The refs this node should request from `peer_node`: every local `a-<id>`\n/// branch the policy allows toward that peer (symmetric scoping — don't ask\n/// for a mind the peer's own gate would refuse to receive elsewhere) plus\n/// every hosted `p-<project>` branch. The pull model's requester half:\n/// two-tier scoping with no new registry fields (ADR-0013).\n// [impl->REQ-INST-5]\npub fn select_refs(\n    policy: &SyncPolicy,\n    cs: &ContextStore,\n    peer_node: &str,\n) -> io::Result<Vec<String>> {\n    Ok(cs\n        .branch_store()\n        .branches()?\n        .into_iter()\n        .filter(|b| policy.allows(b, peer_node))\n        .collect())\n}\n\n/// Requester side: pull `refs` from the peer over `conn_id`, apply each\n/// fetched tip with the vector driver, and report per-branch verdicts.\n/// `open_op` is the durable id for the exactly-once stream open; everything\n/// after rides fire-and-forget. A torn pull errors — recovery is a fresh\n/// `request_sync` (apply is idempotent; FAULT-MATRIX stream-loss row).\n// [impl->REQ-NET-3]\npub fn request_sync(\n    brain: &mut Brain,\n    conn_id: u64,\n    refs: &[String],\n    open_op: MintedOp,\n    cs: &ContextStore,\n    scratch_dir: &Path,\n) -> io::Result<SyncPullReport> {\n    let opened = brain.net_open_stream(conn_id, Some(open_op))?;\n    let stream_id = opened.stream_id;\n    brain.net_stream_subscribe(stream_id, 0)?;\n\n    let sync_id = format!(\"pull-{}\", open_op.seq);\n    let mut have_tips = BTreeMap::new();\n    for r in refs {\n        if let Some(tip) = cs.branch_store().tip(r)? {\n            have_tips.insert(r.clone(), tip);\n        }\n    }\n    let line = SyncRecord::Request {\n        sync_id: sync_id.clone(),\n        refs: refs.to_vec(),\n        have_tips,\n    }\n    .encode_line();\n    brain.net_stream_send(stream_id, &line, None, true)?;\n\n    std::fs::create_dir_all(scratch_dir)?;\n    let bundle: PathBuf = scratch_dir.join(format!(\"recv-{}.bundle\", safe_stem(&sync_id)));\n    let mut file = Some(\n        std::fs::OpenOptions::new()\n            .create(true)\n            .truncate(true)\n            .write(true)\n            .open(&bundle)?,\n    );\n    let mut report = SyncPullReport::default();\n    let mut decoder = SyncDecoder::new();\n\n    // Peer-reply reads use the DECOUPLED reply-read budget + reclassifying read\n    // (REQ-PUMP-DIAL-FASTFAIL): the LATENT TWIN of request_update's wedge — a\n    // non-empty want-set against a connect-then-silent peer would burn the 30s\n    // carrier deadline into a whole-round poison. Drop that peer PROMPTLY instead.\n    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n    let mut deadline = brain.reply_read_deadline();\n    loop {\n        let ev = brain.read_peer_reply_until(deadline)?;\n        // Re-arm the budget on each chunk that arrives on OUR stream: a whole\n        // reply-read budget of NO progress is a wedge; a streaming pull that keeps\n        // delivering stays healthy however long it runs (REQ-HAZARD-PUMP-IPC-DEADLINE).\n        if matches!(&ev, BrokerEvent::NetStreamData { stream_id: sid, .. } if *sid == stream_id) {\n            deadline = brain.reply_read_deadline();\n        }\n        match ev {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        SyncRecord::Deny {\n                            sync_id: xid, refs, ..\n                        } if xid == sync_id => {\n                            report.denied.extend(refs);\n                        }\n                        SyncRecord::Chunk {\n                            sync_id: xid,\n                            offset,\n                            data_b64,\n                        } if xid == sync_id => {\n                            let chunk = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            let f = file\n                                .as_mut()\n                                .ok_or_else(|| io::Error::other(\"chunk after Done\"))?;\n                            f.seek(SeekFrom::Start(offset))?;\n                            f.write_all(&chunk)?;\n                        }\n                        SyncRecord::Done {\n                            sync_id: xid,\n                            total,\n                            refs,\n                        } if xid == sync_id => {\n                            // Close our handle before git reads the bundle.\n                            if let Some(mut f) = file.take() {\n                                f.flush()?;\n                            }\n                            if total == 0 {\n                                let _ = std::fs::remove_file(&bundle);\n                                return Ok(report); // everything already up to date\n                            }\n                            let got = std::fs::metadata(&bundle)?.len();\n                            if got != total {\n                                let _ = std::fs::remove_file(&bundle);\n                                return Err(io::Error::new(\n                                    io::ErrorKind::UnexpectedEof,\n                                    format!(\"bundle incomplete: {got} of {total} bytes\"),\n                                ));\n                            }\n                            let tips = cs.branch_store().fetch_bundle(&bundle)?;\n                            let _ = std::fs::remove_file(&bundle);\n                            for branch in &refs {\n                                if let Some(tip) = tips.get(branch) {\n                                    report.applied.push(apply_fetched(cs, branch, tip)?);\n                                }\n                            }\n                            // PROJECT-INDEX W3 (the W2-carried gap, ruled at\n                            // cutover): a sync pull moves `p-*` tips on the RAW\n                            // BranchStore — `apply_fetched`'s adopt leg can even\n                            // CREATE a p-* branch (a membership change) — and\n…\n                        }\n…\n                    }\n                }\n            }\n…\n        }\n    }\n}","startLine":180,"lineNumbers":[180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,null,303,null,306,307,308,null,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,null,459,null,468,469,470,null,480,481,482]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\sync.rs"}}},"isError":false,"timestamp":1784271580013}}
{"type":"message","id":"cf800893","parentId":"3333c80b","timestamp":"2026-07-17T06:59:40.017Z","message":{"role":"toolResult","toolCallId":"call_zxUFyeUUBdU82iTcmoJSrXnb|fc_038f28d3e93bbfd4016a59d2dafdd88196876c67ccb02fb770","toolName":"read","content":[{"type":"text","text":"    pub fn mark_seen(&self, notif_id: &str, endpoint: &str) -> rusqlite::Result<bool> {\n…\n        let res = (|| -> rusqlite::Result<bool> {\n            let Some(mut row) = self.read_row(notif_id)? else {\n…\n            };\n            if !row.seen.insert(endpoint.to_string()) {\n                return Ok(false);\n            }\n            self.conn.execute(\n                \"UPDATE notifs SET seen = ?2 WHERE notif_id = ?1\",\n                params![notif_id, encode_seen(&row.seen)],\n            )?;\n            Ok(true)\n        })();\n        match &res {\n            Ok(_) => self.conn.execute_batch(\"COMMIT\")?,\n            Err(_) => self.conn.execute_batch(\"ROLLBACK\")?,\n        }\n        res\n    }\n\n    /// Advance `last_surfaced_ms` to `now_ms` if greater (max-merge — a lagging\n    /// clock never rewinds the suppression window). Returns whether it moved.\n    // [impl->REQ-NOTIF-1]\n    pub fn touch_surfaced(&self, notif_id: &str, now_ms: u64) -> rusqlite::Result<bool> {\n        let n = self.conn.execute(\n            \"UPDATE notifs SET last_surfaced_ms = ?2 WHERE notif_id = ?1 AND last_surfaced_ms < ?2\",\n            params![notif_id, now_ms as i64],\n        )?;\n        Ok(n > 0)\n    }\n\n    /// One row by id.\n    pub fn get(&self, notif_id: &str) -> rusqlite::Result<Option<NotifRow>> {\n        self.read_row(notif_id)\n    }\n\n    /// Every row in `subnet`, oldest produce first (created_ms, id tiebreak).\n    pub fn list(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\n        self.select_rows(\n            \"subnet = ?1 ORDER BY created_ms ASC, notif_id ASC\",\n            params![subnet],\n        )\n    }\n\n    /// The undismissed rows in `subnet` — the resurface engine's working set.\n    // [impl->REQ-NOTIF-1]\n    pub fn undismissed(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\n        self.select_rows(\n            \"subnet = ?1 AND dismissed = 0 ORDER BY created_ms ASC, notif_id ASC\",\n            params![subnet],\n        )\n    }\n\n    fn insert_row(&self, row: &NotifRow) -> rusqlite::Result<()> {\n        self.conn.execute(\n            \"INSERT INTO notifs (notif_id, subnet, kind, from_id, body, created_ms,\n                                 dismissed, seen, last_surfaced_ms)\n             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)\",\n            params![\n                row.notif_id,\n                row.subnet,\n                row.kind,\n                row.from_id,\n…\n            ],\n        )?;\n…\n    }\n\n[Showing lines 219-278 of 566. Use :279 to continue]"}],"details":{"truncation":{"content":"            };\r\n            if !row.seen.insert(endpoint.to_string()) {\r\n                return Ok(false);\r\n            }\r\n            self.conn.execute(\r\n                \"UPDATE notifs SET seen = ?2 WHERE notif_id = ?1\",\r\n                params![notif_id, encode_seen(&row.seen)],\r\n            )?;\r\n            Ok(true)\r\n        })();\r\n        match &res {\r\n            Ok(_) => self.conn.execute_batch(\"COMMIT\")?,\r\n            Err(_) => self.conn.execute_batch(\"ROLLBACK\")?,\r\n        }\r\n        res\r\n    }\r\n\r\n    /// Advance `last_surfaced_ms` to `now_ms` if greater (max-merge — a lagging\r\n    /// clock never rewinds the suppression window). Returns whether it moved.\r\n    // [impl->REQ-NOTIF-1]\r\n    pub fn touch_surfaced(&self, notif_id: &str, now_ms: u64) -> rusqlite::Result<bool> {\r\n        let n = self.conn.execute(\r\n            \"UPDATE notifs SET last_surfaced_ms = ?2 WHERE notif_id = ?1 AND last_surfaced_ms < ?2\",\r\n            params![notif_id, now_ms as i64],\r\n        )?;\r\n        Ok(n > 0)\r\n    }\r\n\r\n    /// One row by id.\r\n    pub fn get(&self, notif_id: &str) -> rusqlite::Result<Option<NotifRow>> {\r\n        self.read_row(notif_id)\r\n    }\r\n\r\n    /// Every row in `subnet`, oldest produce first (created_ms, id tiebreak).\r\n    pub fn list(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\r\n        self.select_rows(\r\n            \"subnet = ?1 ORDER BY created_ms ASC, notif_id ASC\",\r\n            params![subnet],\r\n        )\r\n    }\r\n\r\n    /// The undismissed rows in `subnet` — the resurface engine's working set.\r\n    // [impl->REQ-NOTIF-1]\r\n    pub fn undismissed(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\r\n        self.select_rows(\r\n            \"subnet = ?1 AND dismissed = 0 ORDER BY created_ms ASC, notif_id ASC\",\r\n            params![subnet],\r\n        )\r\n    }\r\n\r\n    fn insert_row(&self, row: &NotifRow) -> rusqlite::Result<()> {\r\n        self.conn.execute(\r\n            \"INSERT INTO notifs (notif_id, subnet, kind, from_id, body, created_ms,\r\n                                 dismissed, seen, last_surfaced_ms)\r\n             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)\",\r\n            params![\r\n                row.notif_id,\r\n                row.subnet,\r\n                row.kind,\r\n                row.from_id,\r","truncated":true,"truncatedBy":"lines","totalLines":348,"totalBytes":2264,"outputLines":60,"outputBytes":2264,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn mark_seen(&self, notif_id: &str, endpoint: &str) -> rusqlite::Result<bool> {\n…\n        let res = (|| -> rusqlite::Result<bool> {\n            let Some(mut row) = self.read_row(notif_id)? else {\n…\n            };\r\n            if !row.seen.insert(endpoint.to_string()) {\r\n                return Ok(false);\r\n            }\r\n            self.conn.execute(\r\n                \"UPDATE notifs SET seen = ?2 WHERE notif_id = ?1\",\r\n                params![notif_id, encode_seen(&row.seen)],\r\n            )?;\r\n            Ok(true)\r\n        })();\r\n        match &res {\r\n            Ok(_) => self.conn.execute_batch(\"COMMIT\")?,\r\n            Err(_) => self.conn.execute_batch(\"ROLLBACK\")?,\r\n        }\r\n        res\r\n    }\r\n\r\n    /// Advance `last_surfaced_ms` to `now_ms` if greater (max-merge — a lagging\r\n    /// clock never rewinds the suppression window). Returns whether it moved.\r\n    // [impl->REQ-NOTIF-1]\r\n    pub fn touch_surfaced(&self, notif_id: &str, now_ms: u64) -> rusqlite::Result<bool> {\r\n        let n = self.conn.execute(\r\n            \"UPDATE notifs SET last_surfaced_ms = ?2 WHERE notif_id = ?1 AND last_surfaced_ms < ?2\",\r\n            params![notif_id, now_ms as i64],\r\n        )?;\r\n        Ok(n > 0)\r\n    }\r\n\r\n    /// One row by id.\r\n    pub fn get(&self, notif_id: &str) -> rusqlite::Result<Option<NotifRow>> {\r\n        self.read_row(notif_id)\r\n    }\r\n\r\n    /// Every row in `subnet`, oldest produce first (created_ms, id tiebreak).\r\n    pub fn list(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\r\n        self.select_rows(\r\n            \"subnet = ?1 ORDER BY created_ms ASC, notif_id ASC\",\r\n            params![subnet],\r\n        )\r\n    }\r\n\r\n    /// The undismissed rows in `subnet` — the resurface engine's working set.\r\n    // [impl->REQ-NOTIF-1]\r\n    pub fn undismissed(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {\r\n        self.select_rows(\r\n            \"subnet = ?1 AND dismissed = 0 ORDER BY created_ms ASC, notif_id ASC\",\r\n            params![subnet],\r\n        )\r\n    }\r\n\r\n    fn insert_row(&self, row: &NotifRow) -> rusqlite::Result<()> {\r\n        self.conn.execute(\r\n            \"INSERT INTO notifs (notif_id, subnet, kind, from_id, body, created_ms,\r\n                                 dismissed, seen, last_surfaced_ms)\r\n             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)\",\r\n            params![\r\n                row.notif_id,\r\n                row.subnet,\r\n                row.kind,\r\n                row.from_id,\r\n…\n            ],\n        )?;\n…\n    }","startLine":214,"lineNumbers":[214,null,216,217,null,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,null,284,285,null,287]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\notif.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":566,"totalBytes":2264,"outputLines":60,"outputBytes":2264,"shownRange":{"start":219,"end":278},"nextOffset":279}}},"isError":false,"timestamp":1784271580017}}
{"type":"message","id":"7eb4eb34","parentId":"cf800893","timestamp":"2026-07-17T06:59:40.027Z","message":{"role":"toolResult","toolCallId":"call_HLKmXxRFi6ffFk8nV76RkYRE|fc_038f28d3e93bbfd4016a59d2dafdcc8196acbd422ffaa10e4e","toolName":"read","content":[{"type":"text","text":"    pub fn read_at(&self, commitish: &str, file: &str) -> std::io::Result<Option<Vec<u8>>> {\n        let out = run_git(\n            &[\n…\n                &self.dir_arg(),\n                \"show\",\n                &format!(\"{commitish}:{file}\"),\n            ],\n            None,\n            None,\n            GIT_TIMEOUT,\n        )?;\n        Ok(if out.success() {\n            Some(out.stdout)\n        } else {\n            None\n        })\n    }\n\n    /// Every tracked file path (worktree-relative) in the tree at `commitish`.\n    // [impl->REQ-NET-3]\n    pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {\n        let out = run_git_ok(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"ls-tree\",\n                \"-r\",\n                \"--name-only\",\n                commitish,\n            ],\n            None,\n            None,\n        )?;\n        Ok(String::from_utf8_lossy(&out.stdout)\n            .lines()\n            .map(str::to_string)\n            .filter(|l| !l.is_empty())\n            .collect())\n    }\n\n    /// Whether `commitish` names a commit this repo holds.\n    pub fn has_commit(&self, commitish: &str) -> std::io::Result<bool> {\n        let out = run_git(\n            &[\n                \"-C\",\n                &self.dir_arg(),\n                \"cat-file\",\n                \"-e\",\n…\n            ],\n…\n        )?;\n…\n    }\n\n[Showing lines 329-373 of 959. Use :374 to continue]"}],"details":{"truncation":{"content":"                &self.dir_arg(),\r\n                \"show\",\r\n                &format!(\"{commitish}:{file}\"),\r\n            ],\r\n            None,\r\n            None,\r\n            GIT_TIMEOUT,\r\n        )?;\r\n        Ok(if out.success() {\r\n            Some(out.stdout)\r\n        } else {\r\n            None\r\n        })\r\n    }\r\n\r\n    /// Every tracked file path (worktree-relative) in the tree at `commitish`.\r\n    // [impl->REQ-NET-3]\r\n    pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {\r\n        let out = run_git_ok(\r\n            &[\r\n                \"-C\",\r\n                &self.dir_arg(),\r\n                \"ls-tree\",\r\n                \"-r\",\r\n                \"--name-only\",\r\n                commitish,\r\n            ],\r\n            None,\r\n            None,\r\n        )?;\r\n        Ok(String::from_utf8_lossy(&out.stdout)\r\n            .lines()\r\n            .map(str::to_string)\r\n            .filter(|l| !l.is_empty())\r\n            .collect())\r\n    }\r\n\r\n    /// Whether `commitish` names a commit this repo holds.\r\n    pub fn has_commit(&self, commitish: &str) -> std::io::Result<bool> {\r\n        let out = run_git(\r\n            &[\r\n                \"-C\",\r\n                &self.dir_arg(),\r\n                \"cat-file\",\r\n                \"-e\",\r","truncated":true,"truncatedBy":"lines","totalLines":631,"totalBytes":1256,"outputLines":45,"outputBytes":1256,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn read_at(&self, commitish: &str, file: &str) -> std::io::Result<Option<Vec<u8>>> {\n        let out = run_git(\n            &[\n…\n                &self.dir_arg(),\r\n                \"show\",\r\n                &format!(\"{commitish}:{file}\"),\r\n            ],\r\n            None,\r\n            None,\r\n            GIT_TIMEOUT,\r\n        )?;\r\n        Ok(if out.success() {\r\n            Some(out.stdout)\r\n        } else {\r\n            None\r\n        })\r\n    }\r\n\r\n    /// Every tracked file path (worktree-relative) in the tree at `commitish`.\r\n    // [impl->REQ-NET-3]\r\n    pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {\r\n        let out = run_git_ok(\r\n            &[\r\n                \"-C\",\r\n                &self.dir_arg(),\r\n                \"ls-tree\",\r\n                \"-r\",\r\n                \"--name-only\",\r\n                commitish,\r\n            ],\r\n            None,\r\n            None,\r\n        )?;\r\n        Ok(String::from_utf8_lossy(&out.stdout)\r\n            .lines()\r\n            .map(str::to_string)\r\n            .filter(|l| !l.is_empty())\r\n            .collect())\r\n    }\r\n\r\n    /// Whether `commitish` names a commit this repo holds.\r\n    pub fn has_commit(&self, commitish: &str) -> std::io::Result<bool> {\r\n        let out = run_git(\r\n            &[\r\n                \"-C\",\r\n                &self.dir_arg(),\r\n                \"cat-file\",\r\n                \"-e\",\r\n…\n            ],\n…\n        )?;\n…\n    }","startLine":325,"lineNumbers":[325,326,327,null,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,null,375,null,379,null,381]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\branchstore.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":959,"totalBytes":1256,"outputLines":45,"outputBytes":1256,"shownRange":{"start":329,"end":373},"nextOffset":374}}},"isError":false,"timestamp":1784271580027}}
{"type":"message","id":"ed2db59a","parentId":"7eb4eb34","timestamp":"2026-07-17T06:59:40.039Z","message":{"role":"toolResult","toolCallId":"call_XKlajKMPcCWnM1XBMWnEipXF|fc_038f28d3e93bbfd4016a59d2dafdd081969398bb1f2adbef74","toolName":"read","content":[{"type":"text","text":"pub fn deregister(adapters_dir: &Path, name: &str) -> Result<Option<String>, RegistryError> {\n    let mut record = load_record(adapters_dir, name)?;\n    record.active = false;\n    save_record(adapters_dir, &record)?;\n    let uninstall = load_manifest(adapters_dir, &record)\n        .ok()\n        .and_then(|m| m.update.and_then(|u| u.uninstall));\n    Ok(uninstall)\n}\n\n/// One adapter's record by name.\npub fn load_record(adapters_dir: &Path, name: &str) -> Result<AdapterRecord, RegistryError> {\n    let path = record_file(adapters_dir, name);\n    if !path.exists() {\n        return Err(RegistryError::NotRegistered(name.to_string()));\n    }\n    let s = std::fs::read_to_string(&path)?;\n    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))\n}\n\nfn save_record(adapters_dir: &Path, record: &AdapterRecord) -> Result<(), RegistryError> {\n    let s = toml::to_string_pretty(record)\n        .map_err(|e| RegistryError::BadRecord(format!(\"{}: {e}\", record.name)))?;\n    atomic_write_string(&record_file(adapters_dir, &record.name), &s)?;\n    Ok(())\n}\n\n/// The record's manifest: the held copy in copy mode, the live source tree in\n/// pointer mode (a delegated adapter's self-updates are always honored).\n// [impl->REQ-INSTALL-4]\npub fn load_manifest(\n    adapters_dir: &Path,\n    record: &AdapterRecord,\n) -> Result<Manifest, RegistryError> {\n    let path = match record.mode {\n        RecordMode::Copy => manifest_copy_file(adapters_dir, &record.name),\n        RecordMode::Pointer => source_manifest_file(Path::new(&record.source_dir)),\n    };\n    let s = std::fs::read_to_string(&path).map_err(|e| {\n        // A POINTER-mode adapter whose source manifest isn't there yet is the\n        // common deferred-install case (the binary/manifest is fetched/extracted\n        // out of band — delegated / GhReleaseManaged). Surface it as an\n        // actionable DeferredManifest, never a raw `io: … (os error 2)`\n        // (REQ-HAZARD-DEFERRED-MANIFEST). A Copy-mode miss is genuine corruption\n        // of the held copy → keep the raw Io.\n        // [impl->REQ-HAZARD-DEFERRED-MANIFEST]\n        if record.mode == RecordMode::Pointer && e.kind() == std::io::ErrorKind::NotFound {\n            RegistryError::DeferredManifest {\n                adapter: record.name.clone(),\n                dir: record.source_dir.clone(),\n            }\n        } else {\n            RegistryError::Io(e)\n        }\n    })?;\n    Manifest::from_toml_str(&s).map_err(RegistryError::Manifest)\n}\n\n/// Every record under `adapters/` (active and not — callers filter), sorted\n/// by name for a stable order. Tolerant: an unreadable entry is skipped, the\n/// rest still list (the stale-registry degrade stance).\npub fn all_records(adapters_dir: &Path) -> Vec<AdapterRecord> {\n    let Ok(entries) = std::fs::read_dir(adapters_dir) else {\n        return Vec::new();\n    };\n    let mut out: Vec<AdapterRecord> = entries\n        .filter_map(|e| e.ok())\n        .filter_map(|e| load_record(adapters_dir, &e.file_name().to_string_lossy()).ok())\n        .collect();\n    out.sort_by(|a, b| a.name.cmp(&b.name));\n    out\n}\n\n/// The **active** registered set with manifests resolved — the single source\n/// for creation-time adapter selection, shell discovery, and the R-UPD-5\n/// ripple. A pointer-mode record whose source tree has vanished is skipped\n/// (tolerant), not fatal.\n// [impl->REQ-INSTALL-4]\npub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {\n    all_records(adapters_dir)\n        .into_iter()\n        .filter(|r| r.active)\n        .filter_map(|r| match load_manifest(adapters_dir, &r) {\n            Ok(m) => Some((r, m)),\n            // Skip-diagnostics, never a silent drop (REQ-HAZARD-DEFERRED-MANIFEST):\n            // a pointer-mode adapter whose manifest isn't extracted yet (or any\n            // unreadable record) is omitted from the active set, but we say WHICH\n            // and WHY on stderr so it never vanishes without a trace (mirrors the\n            // [digest]/[strings] skip stance).\n            // [impl->REQ-HAZARD-DEFERRED-MANIFEST]\n            Err(e) => {\n                eprintln!(\"ADAPTER_SKIP:{} — {e}\", r.name);\n                None\n            }\n        })\n        .collect()\n}\n\n// ───────────────────────── Local profiles (M9 / REQ-MANIFEST-2) ─────────────\n//\n// A **local** profile is a node-local, user-authored sparse overlay registered\n\n[Showing lines 419-518 of 1505. Use :519 to continue]"}],"details":{"truncation":{"content":"    let mut record = load_record(adapters_dir, name)?;\r\n    record.active = false;\r\n    save_record(adapters_dir, &record)?;\r\n    let uninstall = load_manifest(adapters_dir, &record)\r\n        .ok()\r\n        .and_then(|m| m.update.and_then(|u| u.uninstall));\r\n    Ok(uninstall)\r\n}\r\n\r\n/// One adapter's record by name.\r\npub fn load_record(adapters_dir: &Path, name: &str) -> Result<AdapterRecord, RegistryError> {\r\n    let path = record_file(adapters_dir, name);\r\n    if !path.exists() {\r\n        return Err(RegistryError::NotRegistered(name.to_string()));\r\n    }\r\n    let s = std::fs::read_to_string(&path)?;\r\n    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))\r\n}\r\n\r\nfn save_record(adapters_dir: &Path, record: &AdapterRecord) -> Result<(), RegistryError> {\r\n    let s = toml::to_string_pretty(record)\r\n        .map_err(|e| RegistryError::BadRecord(format!(\"{}: {e}\", record.name)))?;\r\n    atomic_write_string(&record_file(adapters_dir, &record.name), &s)?;\r\n    Ok(())\r\n}\r\n\r\n/// The record's manifest: the held copy in copy mode, the live source tree in\r\n/// pointer mode (a delegated adapter's self-updates are always honored).\r\n// [impl->REQ-INSTALL-4]\r\npub fn load_manifest(\r\n    adapters_dir: &Path,\r\n    record: &AdapterRecord,\r\n) -> Result<Manifest, RegistryError> {\r\n    let path = match record.mode {\r\n        RecordMode::Copy => manifest_copy_file(adapters_dir, &record.name),\r\n        RecordMode::Pointer => source_manifest_file(Path::new(&record.source_dir)),\r\n    };\r\n    let s = std::fs::read_to_string(&path).map_err(|e| {\r\n        // A POINTER-mode adapter whose source manifest isn't there yet is the\r\n        // common deferred-install case (the binary/manifest is fetched/extracted\r\n        // out of band — delegated / GhReleaseManaged). Surface it as an\r\n        // actionable DeferredManifest, never a raw `io: … (os error 2)`\r\n        // (REQ-HAZARD-DEFERRED-MANIFEST). A Copy-mode miss is genuine corruption\r\n        // of the held copy → keep the raw Io.\r\n        // [impl->REQ-HAZARD-DEFERRED-MANIFEST]\r\n        if record.mode == RecordMode::Pointer && e.kind() == std::io::ErrorKind::NotFound {\r\n            RegistryError::DeferredManifest {\r\n                adapter: record.name.clone(),\r\n                dir: record.source_dir.clone(),\r\n            }\r\n        } else {\r\n            RegistryError::Io(e)\r\n        }\r\n    })?;\r\n    Manifest::from_toml_str(&s).map_err(RegistryError::Manifest)\r\n}\r\n\r\n/// Every record under `adapters/` (active and not — callers filter), sorted\r\n/// by name for a stable order. Tolerant: an unreadable entry is skipped, the\r\n/// rest still list (the stale-registry degrade stance).\r\npub fn all_records(adapters_dir: &Path) -> Vec<AdapterRecord> {\r\n    let Ok(entries) = std::fs::read_dir(adapters_dir) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out: Vec<AdapterRecord> = entries\r\n        .filter_map(|e| e.ok())\r\n        .filter_map(|e| load_record(adapters_dir, &e.file_name().to_string_lossy()).ok())\r\n        .collect();\r\n    out.sort_by(|a, b| a.name.cmp(&b.name));\r\n    out\r\n}\r\n\r\n/// The **active** registered set with manifests resolved — the single source\r\n/// for creation-time adapter selection, shell discovery, and the R-UPD-5\r\n/// ripple. A pointer-mode record whose source tree has vanished is skipped\r\n/// (tolerant), not fatal.\r\n// [impl->REQ-INSTALL-4]\r\npub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {\r\n    all_records(adapters_dir)\r\n        .into_iter()\r\n        .filter(|r| r.active)\r\n        .filter_map(|r| match load_manifest(adapters_dir, &r) {\r\n            Ok(m) => Some((r, m)),\r\n            // Skip-diagnostics, never a silent drop (REQ-HAZARD-DEFERRED-MANIFEST):\r\n            // a pointer-mode adapter whose manifest isn't extracted yet (or any\r\n            // unreadable record) is omitted from the active set, but we say WHICH\r\n            // and WHY on stderr so it never vanishes without a trace (mirrors the\r\n            // [digest]/[strings] skip stance).\r\n            // [impl->REQ-HAZARD-DEFERRED-MANIFEST]\r\n            Err(e) => {\r\n                eprintln!(\"ADAPTER_SKIP:{} — {e}\", r.name);\r\n                None\r\n            }\r\n        })\r\n        .collect()\r\n}\r\n\r\n// ───────────────────────── Local profiles (M9 / REQ-MANIFEST-2) ─────────────\r\n//\r\n// A **local** profile is a node-local, user-authored sparse overlay registered\r","truncated":true,"truncatedBy":"lines","totalLines":1087,"totalBytes":4479,"outputLines":100,"outputBytes":4479,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn deregister(adapters_dir: &Path, name: &str) -> Result<Option<String>, RegistryError> {\n    let mut record = load_record(adapters_dir, name)?;\r\n    record.active = false;\r\n    save_record(adapters_dir, &record)?;\r\n    let uninstall = load_manifest(adapters_dir, &record)\r\n        .ok()\r\n        .and_then(|m| m.update.and_then(|u| u.uninstall));\r\n    Ok(uninstall)\r\n}\r\n\r\n/// One adapter's record by name.\r\npub fn load_record(adapters_dir: &Path, name: &str) -> Result<AdapterRecord, RegistryError> {\r\n    let path = record_file(adapters_dir, name);\r\n    if !path.exists() {\r\n        return Err(RegistryError::NotRegistered(name.to_string()));\r\n    }\r\n    let s = std::fs::read_to_string(&path)?;\r\n    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))\r\n}\r\n\r\nfn save_record(adapters_dir: &Path, record: &AdapterRecord) -> Result<(), RegistryError> {\r\n    let s = toml::to_string_pretty(record)\r\n        .map_err(|e| RegistryError::BadRecord(format!(\"{}: {e}\", record.name)))?;\r\n    atomic_write_string(&record_file(adapters_dir, &record.name), &s)?;\r\n    Ok(())\r\n}\r\n\r\n/// The record's manifest: the held copy in copy mode, the live source tree in\r\n/// pointer mode (a delegated adapter's self-updates are always honored).\r\n// [impl->REQ-INSTALL-4]\r\npub fn load_manifest(\r\n    adapters_dir: &Path,\r\n    record: &AdapterRecord,\r\n) -> Result<Manifest, RegistryError> {\r\n    let path = match record.mode {\r\n        RecordMode::Copy => manifest_copy_file(adapters_dir, &record.name),\r\n        RecordMode::Pointer => source_manifest_file(Path::new(&record.source_dir)),\r\n    };\r\n    let s = std::fs::read_to_string(&path).map_err(|e| {\r\n        // A POINTER-mode adapter whose source manifest isn't there yet is the\r\n        // common deferred-install case (the binary/manifest is fetched/extracted\r\n        // out of band — delegated / GhReleaseManaged). Surface it as an\r\n        // actionable DeferredManifest, never a raw `io: … (os error 2)`\r\n        // (REQ-HAZARD-DEFERRED-MANIFEST). A Copy-mode miss is genuine corruption\r\n        // of the held copy → keep the raw Io.\r\n        // [impl->REQ-HAZARD-DEFERRED-MANIFEST]\r\n        if record.mode == RecordMode::Pointer && e.kind() == std::io::ErrorKind::NotFound {\r\n            RegistryError::DeferredManifest {\r\n                adapter: record.name.clone(),\r\n                dir: record.source_dir.clone(),\r\n            }\r\n        } else {\r\n            RegistryError::Io(e)\r\n        }\r\n    })?;\r\n    Manifest::from_toml_str(&s).map_err(RegistryError::Manifest)\r\n}\r\n\r\n/// Every record under `adapters/` (active and not — callers filter), sorted\r\n/// by name for a stable order. Tolerant: an unreadable entry is skipped, the\r\n/// rest still list (the stale-registry degrade stance).\r\npub fn all_records(adapters_dir: &Path) -> Vec<AdapterRecord> {\r\n    let Ok(entries) = std::fs::read_dir(adapters_dir) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out: Vec<AdapterRecord> = entries\r\n        .filter_map(|e| e.ok())\r\n        .filter_map(|e| load_record(adapters_dir, &e.file_name().to_string_lossy()).ok())\r\n        .collect();\r\n    out.sort_by(|a, b| a.name.cmp(&b.name));\r\n    out\r\n}\r\n\r\n/// The **active** registered set with manifests resolved — the single source\r\n/// for creation-time adapter selection, shell discovery, and the R-UPD-5\r\n/// ripple. A pointer-mode record whose source tree has vanished is skipped\r\n/// (tolerant), not fatal.\r\n// [impl->REQ-INSTALL-4]\r\npub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {\r\n    all_records(adapters_dir)\r\n        .into_iter()\r\n        .filter(|r| r.active)\r\n        .filter_map(|r| match load_manifest(adapters_dir, &r) {\r\n            Ok(m) => Some((r, m)),\r\n            // Skip-diagnostics, never a silent drop (REQ-HAZARD-DEFERRED-MANIFEST):\r\n            // a pointer-mode adapter whose manifest isn't extracted yet (or any\r\n            // unreadable record) is omitted from the active set, but we say WHICH\r\n            // and WHY on stderr so it never vanishes without a trace (mirrors the\r\n            // [digest]/[strings] skip stance).\r\n            // [impl->REQ-HAZARD-DEFERRED-MANIFEST]\r\n            Err(e) => {\r\n                eprintln!(\"ADAPTER_SKIP:{} — {e}\", r.name);\r\n                None\r\n            }\r\n        })\r\n        .collect()\r\n}\r\n\r\n// ───────────────────────── Local profiles (M9 / REQ-MANIFEST-2) ─────────────\r\n//\r\n// A **local** profile is a node-local, user-authored sparse overlay registered\r","startLine":418,"lineNumbers":[418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-runtime\\src\\registry.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1505,"totalBytes":4479,"outputLines":100,"outputBytes":4479,"shownRange":{"start":419,"end":518},"nextOffset":519}}},"isError":false,"timestamp":1784271580039}}
{"type":"message","id":"a3e35b2e","parentId":"ed2db59a","timestamp":"2026-07-17T06:59:56.893Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing frequent git process invocations**\n\n**Tracing brain direct reads and dispatch patterns**\n\n**Identifying subsecond file read loops**"},{"type":"thinking","thinking":"**Investigating write_ready exe hashing**"},{"type":"toolCall","id":"call_Z54WYZGf9MjVCIBA3v0nKlsZ|fc_038f28d3e93bbfd4016a59d2ec3cb081968e42b1f17edf3fda","name":"grep","arguments":{"i":"Tracing ready heartbeat reads","pattern":"fn write_ready|pub fn write_ready|current_exe_hash|exe_hash|sha256|std::fs::read","path":"crates/spt-daemon/src/brainproc.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11301,"output":672,"cacheRead":198144,"cacheWrite":0,"totalTokens":210117,"reasoningTokens":590,"cost":{"input":0.05650500000000001,"output":0.02016,"cacheRead":0.099072,"cacheWrite":0,"total":0.175737}},"stopReason":"toolUse","timestamp":1784271580046,"responseId":"resp_038f28d3e93bbfd4016a59d2dd63a8819692b90bb6acf595c7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2df3610819690783b164e8ff678","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdLqDo6cV6jqQdsgeuG6JG8tQtGYuHy89owHClIRE3_wtpOrg6BfEHe5RjeO8YFN9NhZCWdjnqCSK8N16K5bE4jtMDnLaD6XuNG3URfltqnF5nM10Ms9HJnEsXyEARXYVzBglS7QJyuG6GRuOeZlxeZZK7ns7ToU1XqsH3Qst7vmWCynIaXJL91esPoh2Y_UIS4PRsQNfnVPIT_-hnWeblWE5s89h4gSgG7Q-Fo1QCIpDWHynY0dor6rO7QPNoyys5B06vb7gBlzl1voX-RuGOLPLtjHe9ZyWBEJ-Yp5LZ_s4VB94-KDbSJud5XJk_038Q3sbr3VecZEpPWPdf6jVSPx_EG3eF7rKBVL5revcnTqlqHH8_YsdRtYkPuHlBCyywUaU8fqCN-dfaTCP1vwD4wPD4NWnnyiKN20HZBoW6iBqxxO8hiIJ_hBEpGeqafAQ8qjNhdu_2eS2nFYBL1lZ3gUcSNYPTwZMifEVlG8VSRJI-KB440QLTbnqRADIULAK5105C1eAdd6ztI2FZwsfeoNyDfpTgFvD6xTmrMD00pCP1aV9rZfgSIR0jIe8TqoG9N0Q898qDoLtapfrKZu6V9NNTwY70KUuiwx1OQcAeAmGJOgqG7CW-LHM_fCTnZRth50VpbeDNtruGqoEm3ZQkQ7OwRoHThYWh1AuL9Bp2J6f9BX1tK86Vx0cbDIRN-MCOLZNpmqL7xHnFUlClDeqdGHRffx6LohgvGxCX1-wo_VPaW7Yf049RPBiT0yRftpBF3KDWNO4_ZvRcrCJLFKOQvnyz24E08qxo2mRk3Pk56FPf0JUQSbsjQDSzwW7YYTl1skbwVqgcke_oANFpxO3mVgnl4ZbZ-Ba-ZbuS3JI23LJhNz3hERmBY2bTtmGwp4xlzoltAZga93fXarQ8FUDNJ41ckKqppjn0trwwZ5CkL_t0uWoNXJ3TnxfSPQicy_HSP1JXCY6XVr3D9zldCRYetjqjjpsT2EHnG8gWG4diKVQyN-1eAYqLjo93hNSCC22yU5eBjE_iCqiK2Iqmdm5zbNXTgIeU87iStcBQYcJ8VKcl19BJyg8S7vY3Ieh3PVDdxc3GYsyfzlmnmeOvWyFJ6kMJdevUBaKJQlbVvZlvg7NfVkxMsjejY0opnX6hAd_jneSFo41liQE-wh9sftg2vrtvWcSshbs3cDdLZQwJamzhKCAbdtKCYkTkK6cBeU-K2ERO0YllxdnYlSmSNnaA1aTxqEB1ZrGMdjPk4rpnY2o9n2NKvTujkEohSR00zRZz9gHEYOj5MiScfy0gWDk5zhcs4PKs8TLbXlUjgmkAywDLnKl9RPnlpPWHRkvXsDpBN_JVe6V3O063DkU7aSFP_H4n9xY4rTbJLFoGObx29IhYIP5lb6ljYI2_zGDDLO2sePzyscocBGplEFF1OrmjYpjP_WE8fd_ur3OHRAmBYnhnlPj1ag8s27Roy14DEuTsgagoRcKHVnjon2__lNbX3Q6s__oeIsqn9WXf4lSsMAs71XxS1RI_38bOj3CpoZ8eFjYF97jx9vey5rm8FxjdBlAD2LDC4fh8szzW0ZajBlGNDyesGz8Pgd7D3GAW5O7zRW6-iQaRWffvksnNItutSofYR9JJbHUnr_iiXjWbW_ZCK_MDP6L84DcYc_13Fa97YIbVSxGUaXJmYX9CUqT9tIhXho4YQmjDWcREmw_yKyaaRbyG4yw0UZCBuny8xM2MC6JBdyIab4K7wVp6_nfcSH7KjaLpXbQQBaFK3ma40cZcv-x8UG_TEOsdDkqyjzvDnUXK7bUaSh-y_9YacpzzkYKPL2l6090OrRAso-snF8f8ReUq4zRubF47_y14pSqDqoOJeOyGop_RqKlq7tvjomIs5oUWakysz5knCvyYep43vGf3aUnTKLB1NyEIhHxDSasCH8PeLz3jQ-vdtmOQ5q_oJ6Sf-TVyxWCZMgJR-oA97VOcso65IR7RaFPAfEcQCCC67RLRbHcbfinNvPkDeOmwmHJyxpx903dj-DHjV4IOL_Rg3XwBj41k2B_m3ztkS4pepXn26Qjmj6czWzaGa2hQ5a8avhi5sjSj4ZCTokTmHkwYh6Hu5X5zg922yYhEaD_yda7cHBW8D1IquFT2406qARgeXcOwp0FPYHnUf56-ECm0j82y2HUZFC30tv9DXYlCdeULZb8Usmq86O8umIgTXRTVSTSlu-A4EyCqcPtGrx1Sp5Iww5RK0NpzISXBP1qZW_oXSo6nyrCWHE7n_4ZS2Fy0li_X-7S39h8VNyEMnOsT6-kb4qiGW2HbWq6VQQ8bjw8kH9_3THX49SwmbZMcdiLHC-6JBJUYYPpO--41Dw6nabj5NPbAP9xJoJ_Ce_AO3Jb2DeWLCqYXoSzHSFBi-tiHHs7VIAwL1MJ6zu4P644RqIXUA6YESuZGR7I1Mzlb4iLXjcpFRsvRJdzdXEF2VvtiAi5_VWCAwW0szQ1mCqun9W0CwzUsvDXr_Nadf98GyzbdLWX2RJMVep2_4Z91FR-zJvXh2MhcOjhy6tgiPqu7BBi0IR3znxdFTn7EFeAByGlgvtso2dsSxykvWBk2ibpaA-wNVBJzGuJJMEnmC-Xf9Reja2Fq80eAMTTZYb4AcYBbZlqwhF21WPhFswTzPAXshz_rSGRR5lhTms84kSsIjGlhb4_dTgCaTqh-vWGPn3wGN9CZUJyx3Ft-iVvju6VXhs0wVLmJG6XF9LeFUNRzEE8CRwgnIxJBCmBJyoSkUFx7pRm5w1vv7b0E9Fyiy5jeAyVe9Ub3S5Z5_oLDm93OmcCs_7PkPoCKB1aO3zHb5n8Sf8s1W4rZ0hc0IeevyFEbWYmAQwq3tEW9-P9nsUOom_KvECAPmFQU0AYzy0LO8bqX-R9GS1ej9yDqsQbIwLOSrnn5AuTN9RPL7pdJV4A15XMVOGnCzzAQqiHLvrgCmsK90SN7cEdrHd2GWQ9fwI45rUeCgD2FJ1BqmCrERXe4-yBFTnOu-6wHF8SmiJagfwZJg76yqMudngOvOhjET9oMRvEgrNTMuY5zqPOTNw7SKthl35jHY59OSPwNxqdPuM_kClEh1qzS9rmHQn5f7syafKnbceCStrRyhz3W0sl_omgaPzBTdQe7GoFhxPVVh8A40EhGoSZk8TbV0kl-YkAM8C5XAiseEUS7ceDCDnxwA7k7vmj3jhbl_xnwwVVZVOz6hHvMjnpFfs1lI8gJ8Cn_uZynXYR12vKbe9fArkWaPxS0_cCnAxlfLgkOZRVUY-hkiMGxdbZkxFFpZtJuPW7tCG8uAn8RMmzJtJlzHpjE2jW80G3IEkXv2zXYG73fmn4nZJs-ztR-wHC3saoL6w82WvV7VqKX576AT85z1sFF5_YRPNo5PDhc7dK2wGGZ5nXhOGk4rv5LsojbjoPsWgZJnZWPQ0DRG67VYmWUoAAWctaAn8caLIYCqv9DtXW-wmb_4p7zClfpcQvKy3Mu1YtIt4PvMUzRor_S9HQuyy7r0J6307MLs94a2sLFVIbcQuVK8ZZIHUoV5Mix-voG7tfLj6qoxAQZDD4Jq8vxxkKGfr89WUjIoDz9jZ_708blNjPvQkaM0o7cfzL1McBL5ku4GWoM7rUC2XaDe19Q8RnN4y99Cn233QzRNmb3___oNmqF-I3JrRypcIZae3IxdzIMusrdnzHxHFyojzZ8UvOikgOfRPRq8F9cAAu81iPFJg0URGHk00PbPFKBTfZ_AqT3ao5ERQXo0QXyEh0i8_DSab4sF_8Wb5jxtTO-E6SC1MXFqOdWF2E-tXL3zhLKWIgM60a8j8hEAGnTVZjGOB29LjfIDGUVsNf_bP5pi19WjNfEmXu-pxLUhZbXVRJLEwFX_8FeUbWbE3Sw-HOF-kU4QTUrZ11tBlSnsRWpFDDC5Y8XOpR9ozO5MPWH-EA-ylUYiiPtsmOJvph2hMWV5IS75wS8EhHLkfsppjYzR2uq6QVwdEnq8xZ1fMNgwmyvWx7kNVmf2c2VKwaN1Lcf39iRrSZs5ZCLVPr5EMiqVteodk6vNvoJppQ6-QuWXZZQ==","summary":[{"type":"summary_text","text":"**Analyzing frequent git process invocations**"},{"type":"summary_text","text":"**Tracing brain direct reads and dispatch patterns**"},{"type":"summary_text","text":"**Identifying subsecond file read loops**"}]},{"id":"rs_038f28d3e93bbfd4016a59d2ead2988196a901fd50af292262","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdLs0-bWyEcDd9OEafCP30F6ecN0uBPvj0y3vcqTZ4GulPbKvuMuZ-nBsVN0qy9rYsp0why7b_ebrzIQ_xifEmXm_rH0sEA_amagOCHqzFPhQpeQIvVxLNBim_VK_lMx59u5CWVixPcCc42_JF9Aqn2p3qAWuEFG388WNBXpGSCqpHEn-UcoQH9hbdtX95kiiI2rokuHU9B7PFleL20WPOzTpe6sgL2krS3e2VUq0aB-m-dMAlczkc9mC4Ae9a_ZEEACLSuslDIyuVWIY2LNOUDpwmyPnR-FPMRFKSe5u0IztkIJEO6Wfpg1xuMmKWn90DIFIuSb-Y6MeCeL-OlBg6LNQaIze7LFLaGK41MiGvWKR7-0YAFZdHWNKW9VjHY-P85Hu6L_P60Uh0tZisynT77gHNPF_mUb0INvD9TmhbuoxKgWlCHm8vJTw3kzH5euo2gJCQFC-C-SbCkWGbeoErIPS60VMuqIP35QWi85bpmuIDG1GX0bee6hNc4hgG_epwcwExAPL_sbgk9FaQqcdOyFo7oCbpI8YPV4pQ8xFK3slnO2I5xP5QHpX8aeJOG6YLK_Ob2QlMVTUwPbWnDqoNIAL3uz2VyskKcNlApAO0m_ZTWdgjU14OPjxL56GHz2TmCtOiU8b3AF-NJIzVrdQ671phlJSQNnKSQYpc5C5kehvBSKGCtMI1krcvyXRan8USWTfD_oTZcT4zm9KhA50mSfMuQ9848hvQ6rvb5wLI2WTILgEh-azD7cfFRE6TnhX8F_GN-Jx8go5geOLZxCR15BxRqzwspoAQfxwTNilKRg0ZcfpV9dPLrpj3cvB0QXcOPP17qkK280mT-5TKjFcmqVwo-mykWt3zid_7dXJSMKQ-WSmraI1QGDZGX1vogN68bb3e7wX96pxMoTx2wR0TZi0m_37Zi-sejVSdxkVgCAkWyXm9VWb0gA6pAub4FJ8egwSug2ZxN0oBBIc0aFRmK1w2kK6hBr1EVWC794uBw_pTa3qeaRnWHnbOq4E9zUiuILPnY0oGwlKb4mA4kQ3FMMu2pZuU1tqxhVCEz0LS5fsYhp3Cm7yJUmbwzLQ0h5TFGZtoXR0t5y0yR4R4yyWXXp35rM0XICnni6DDxS_S5TO1kukMmXLLcuynYa9cy4oxN_AySwI7WoUMeLf4jPF99sJ50fy6VIDZIU3xNo8LhzQV2iMZof2v7v1I3jUl9aWWZJN77ycX4Kj_oZKvFg0oP0yOWZ27REflkarbbxzrWK0wZ1ujJ2Ze0w-7WYb4r9UPlccsk37Poh7X7ewQP0c_zij155xLWFbEh400_SKwJMs7s=","summary":[{"type":"summary_text","text":"**Investigating write_ready exe hashing**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2ec3cb081968e42b1f17edf3fda","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing ready heartbeat reads\",\"pattern\":\"fn write_ready|pub fn write_ready|current_exe_hash|exe_hash|sha256|std::fs::read\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Z54WYZGf9MjVCIBA3v0nKlsZ","name":"grep"}]},"duration":16824.33449999988,"ttft":3241.7293999940157,"contextSnapshot":{"promptTokens":209445,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Z54WYZGf9MjVCIBA3v0nKlsZ|fc_038f28d3e93bbfd4016a59d2ec3cb081968e42b1f17edf3fda","toolName":"grep","startedAt":"2026-07-17T06:59:56.893Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Tracing ready heartbeat reads"},"id":"4917a7ec","parentId":"a3e35b2e","timestamp":"2026-07-17T06:59:56.893Z"}
{"type":"message","id":"08ee0bd0","parentId":"4917a7ec","timestamp":"2026-07-17T06:59:56.900Z","message":{"role":"toolResult","toolCallId":"call_Z54WYZGf9MjVCIBA3v0nKlsZ|fc_038f28d3e93bbfd4016a59d2ec3cb081968e42b1f17edf3fda","toolName":"grep","content":[{"type":"text","text":" 350|/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\n*351|/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\n 352|/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\n 353|/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\n 354|/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\n 355|/// so a stale stamp from a prior generation cannot falsely promote a brain that\n*356|/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\n 357|/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\n 358|/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\n 359|/// flips across a swap; the D7-4 field run asserts it matches the released\n...\n 364|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*365|fn write_ready(generation: u64) {\n 366|    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n*367|    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n 368|    // tolerates its absence). Never fail the ready write over a diagnostic field.\n*369|    if let Some(hash) = current_exe_hash() {\n*370|        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n 371|    }\n 372|    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n 373|}\n...\n 376|/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n*377|/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n 378|/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n 379|/// read + hash per brain start — negligible, and it pays rent far beyond the\n 380|/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\n*381|fn current_exe_hash() -> Option<String> {\n 382|    let exe = std::env::current_exe().ok()?;\n*383|    let bytes = std::fs::read(exe).ok()?;\n*384|    Some(crate::release::sha256_hex(&bytes))\n 385|}\n 386|\n 387|/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\n...\n 398|\n*399|/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\n 400|/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\n 401|/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\n 402|/// this to prove a respawn ran the swapped binary's bytes.\n 403|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*404|pub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n 405|    serde_json::from_str::<serde_json::Value>(body)\n 406|        .ok()?\n*407|        .get(\"exe_hash\")?\n 408|        .as_str()\n 409|        .map(str::to_string)\n 410|}\n...\n 418|pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n*419|    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n 420|}\n 421|\n 422|/// The supervisor's next backoff after a brain death: a healthy run (≥\n...\n 464|    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);\n*465|    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is\n 466|    /// actually running), or `None` if absent/garbled — degrades the promotion\n 467|    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\n*468|    fn ready_exe_hash(&self) -> Option<String>;\n 469|    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\n 470|    /// the candidate SHOULD be running), or `None` if no staged metadata /\n 471|    /// version mismatch / platform absent — degrades the gate to readiness-only.\n...\n 499|    fn record_rolled_back(&self, _q: u64, _r: u64, _bin: &str) {}\n*500|    fn ready_exe_hash(&self) -> Option<String> {\n 501|        None\n 502|    }\n 503|    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n...\n 562|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n*563|    fn ready_exe_hash(&self) -> Option<String> {\n*564|        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n*565|        parse_ready_exe_hash(&body)\n 566|    }\n 567|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n 568|    fn staged_artifact_hash(&self, version: u64) -> Option<String> {\n...\n 972|\n*973|/// Compare the just-ready candidate's stamped `exe_hash` against the staged\n 974|/// artifact hash for `version` on this platform (KH 6.11). Readiness alone is\n 975|/// NOT proof the new bytes run — on Linux a brain respawned onto the renamed\n 976|/// old binary still signals ready (the v0.4.1 kitsubito false-success). On\n...\n 980|fn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n*981|    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n 982|        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n 983|        (Some(_), Some(_)) => BytesGate::Mismatch,\n 984|        (expected, _) => {\n...\n 1396|\n*1397|    /// D7-1: the `exe_hash` breadcrumb reader extracts the resident-bytes digest\n 1398|    /// and is N-1-readable — a legacy D6-era `{pid, generation}` stamp (no\n*1399|    /// `exe_hash`) reads `None`, never an error.\n 1400|    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1401|    #[test]\n*1402|    fn ready_exe_hash_parses_and_is_n1_readable() {\n 1403|        // Present → Some(hash).\n 1404|        assert_eq!(\n*1405|            parse_ready_exe_hash(\n*1406|                &serde_json::json!({\"pid\": 1, \"generation\": 3, \"exe_hash\": \"deadbeef\"})\n 1407|                    .to_string()\n 1408|            ),\n 1409|            Some(\"deadbeef\".to_string())\n...\n 1412|        assert_eq!(\n*1413|            parse_ready_exe_hash(&serde_json::json!({\"pid\": 1, \"generation\": 3}).to_string()),\n 1414|            None,\n*1415|            \"a D6-era stamp without exe_hash reads None (additive / N-1-readable)\"\n 1416|        );\n*1417|        assert_eq!(parse_ready_exe_hash(\"{ not json\"), None, \"garbage → None\");\n 1418|    }\n 1419|\n 1420|    /// D7-1: the fixture-B swap mechanism — appending padding bytes to a binary\n...\n 1422|    /// how the D7-1 survival E2E builds fixture B from fixture A (PE + ELF both\n*1423|    /// tolerate trailing data, so behavior is unchanged but `exe_hash` flips).\n 1424|    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1425|    #[test]\n*1426|    fn appended_padding_flips_exe_hash() {\n 1427|        let a = b\"the-fixture-A-bytes\".to_vec();\n 1428|        let mut b = a.clone();\n 1429|        b.extend_from_slice(&[0u8; 16]); // fixture B = A + appended padding\n 1430|        assert_ne!(\n*1431|            crate::release::sha256_hex(&a),\n*1432|            crate::release::sha256_hex(&b),\n*1433|            \"appending padding must flip the exe_hash (the fixture-B swap proof)\"\n 1434|        );\n 1435|    }\n 1436|\n...\n 1536|        }\n*1537|        fn ready_exe_hash(&self) -> Option<String> {\n 1538|            self.ready_hash.lock().unwrap().clone()\n 1539|        }\n 1540|        fn staged_artifact_hash(&self, _version: u64) -> Option<String> {"}],"details":{"scopePath":"crates/spt-daemon/src/brainproc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":36,"fileCount":1,"files":["crates/spt-daemon/src/brainproc.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":36}],"truncated":false,"displayContent":"  350│/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\n *351│/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\n  352│/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\n  353│/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\n  354│/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\n  355│/// so a stale stamp from a prior generation cannot falsely promote a brain that\n *356│/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\n  357│/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\n  358│/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\n  359│/// flips across a swap; the D7-4 field run asserts it matches the released\n     │...\n  364│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n *365│fn write_ready(generation: u64) {\n  366│    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n *367│    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n  368│    // tolerates its absence). Never fail the ready write over a diagnostic field.\n *369│    if let Some(hash) = current_exe_hash() {\n *370│        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n  371│    }\n  372│    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n  373│}\n     │...\n  376│/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n *377│/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n  378│/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n  379│/// read + hash per brain start — negligible, and it pays rent far beyond the\n  380│/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\n *381│fn current_exe_hash() -> Option<String> {\n  382│    let exe = std::env::current_exe().ok()?;\n *383│    let bytes = std::fs::read(exe).ok()?;\n *384│    Some(crate::release::sha256_hex(&bytes))\n  385│}\n  386│\n  387│/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\n     │...\n  398│\n *399│/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\n  400│/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\n  401│/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\n  402│/// this to prove a respawn ran the swapped binary's bytes.\n  403│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n *404│pub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n  405│    serde_json::from_str::<serde_json::Value>(body)\n  406│        .ok()?\n *407│        .get(\"exe_hash\")?\n  408│        .as_str()\n  409│        .map(str::to_string)\n  410│}\n     │...\n  418│pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n *419│    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n  420│}\n  421│\n  422│/// The supervisor's next backoff after a brain death: a healthy run (≥\n     │...\n  464│    fn record_rolled_back(&self, quarantine_version: u64, running_version: u64, rollback_binary: &str);\n *465│    /// The `exe_hash` the candidate stamped in `brain.ready` (the bytes it is\n  466│    /// actually running), or `None` if absent/garbled — degrades the promotion\n  467│    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\n *468│    fn ready_exe_hash(&self) -> Option<String>;\n  469│    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\n  470│    /// the candidate SHOULD be running), or `None` if no staged metadata /\n  471│    /// version mismatch / platform absent — degrades the gate to readiness-only.\n     │...\n  499│    fn record_rolled_back(&self, _q: u64, _r: u64, _bin: &str) {}\n *500│    fn ready_exe_hash(&self) -> Option<String> {\n  501│        None\n  502│    }\n  503│    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n     │...\n  562│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n *563│    fn ready_exe_hash(&self) -> Option<String> {\n *564│        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n *565│        parse_ready_exe_hash(&body)\n  566│    }\n  567│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n  568│    fn staged_artifact_hash(&self, version: u64) -> Option<String> {\n     │...\n  972│\n *973│/// Compare the just-ready candidate's stamped `exe_hash` against the staged\n  974│/// artifact hash for `version` on this platform (KH 6.11). Readiness alone is\n  975│/// NOT proof the new bytes run — on Linux a brain respawned onto the renamed\n  976│/// old binary still signals ready (the v0.4.1 kitsubito false-success). On\n     │...\n  980│fn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n *981│    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n  982│        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n  983│        (Some(_), Some(_)) => BytesGate::Mismatch,\n  984│        (expected, _) => {\n     │...\n 1396│\n*1397│    /// D7-1: the `exe_hash` breadcrumb reader extracts the resident-bytes digest\n 1398│    /// and is N-1-readable — a legacy D6-era `{pid, generation}` stamp (no\n*1399│    /// `exe_hash`) reads `None`, never an error.\n 1400│    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1401│    #[test]\n*1402│    fn ready_exe_hash_parses_and_is_n1_readable() {\n 1403│        // Present → Some(hash).\n 1404│        assert_eq!(\n*1405│            parse_ready_exe_hash(\n*1406│                &serde_json::json!({\"pid\": 1, \"generation\": 3, \"exe_hash\": \"deadbeef\"})\n 1407│                    .to_string()\n 1408│            ),\n 1409│            Some(\"deadbeef\".to_string())\n     │...\n 1412│        assert_eq!(\n*1413│            parse_ready_exe_hash(&serde_json::json!({\"pid\": 1, \"generation\": 3}).to_string()),\n 1414│            None,\n*1415│            \"a D6-era stamp without exe_hash reads None (additive / N-1-readable)\"\n 1416│        );\n*1417│        assert_eq!(parse_ready_exe_hash(\"{ not json\"), None, \"garbage → None\");\n 1418│    }\n 1419│\n 1420│    /// D7-1: the fixture-B swap mechanism — appending padding bytes to a binary\n     │...\n 1422│    /// how the D7-1 survival E2E builds fixture B from fixture A (PE + ELF both\n*1423│    /// tolerate trailing data, so behavior is unchanged but `exe_hash` flips).\n 1424│    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 1425│    #[test]\n*1426│    fn appended_padding_flips_exe_hash() {\n 1427│        let a = b\"the-fixture-A-bytes\".to_vec();\n 1428│        let mut b = a.clone();\n 1429│        b.extend_from_slice(&[0u8; 16]); // fixture B = A + appended padding\n 1430│        assert_ne!(\n*1431│            crate::release::sha256_hex(&a),\n*1432│            crate::release::sha256_hex(&b),\n*1433│            \"appending padding must flip the exe_hash (the fixture-B swap proof)\"\n 1434│        );\n 1435│    }\n 1436│\n     │...\n 1536│        }\n*1537│        fn ready_exe_hash(&self) -> Option<String> {\n 1538│            self.ready_hash.lock().unwrap().clone()\n 1539│        }\n 1540│        fn staged_artifact_hash(&self, _version: u64) -> Option<String> {"},"isError":false,"timestamp":1784271596900}}
{"type":"message","id":"7454dd9a","parentId":"08ee0bd0","timestamp":"2026-07-17T06:59:56.901Z","message":{"role":"user","content":"Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.\n\nParent IRC message:\n\nFresh v0.36 evidence: repeated Registry-family stream subscriber poison. Examples stream 524/1161/1347/3053/3810: CONN_WRITE_POISONED after ~15s, DISPATCH_EV family=Registry endpoint=- elapsed 15.5-16.4s. Current broker ~550 threads; brain reads ~64 MiB/s. Investigate Registry stream lifecycle/replay specifically.","attribution":"agent","timestamp":1784271582387,"steering":true}}
{"type":"message","id":"086b44c3","parentId":"7454dd9a","timestamp":"2026-07-17T07:00:06.760Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying frequent binary reads and planning registry investigation**"},{"type":"toolCall","id":"call_GWorweI4NRfQgde3I7MWke4u|fc_038f28d3e93bbfd4016a59d2f3e29881968e029e0f83f11188","name":"irc","arguments":{"i":"Reporting brain I/O smoking gun","op":"send","to":"Main","message":"Brain 64 MiB/s is almost certainly self-hash loop: brainproc.rs:63 BRAIN_HEARTBEAT=500ms; run_brain loop :259-260 then write_ready(generation) :297 each heartbeat; write_ready :365-372 calls current_exe_hash every time; current_exe_hash :381-384 std::fs::read(current_exe) + SHA256. Thus read rate = 2 × spt.exe size/sec; a ~31.96 MiB binary predicts 63.9 MiB/s exactly. Source comment :379 wrongly says one exe read per brain start. Continuing Registry poison lifecycle.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4060,"output":259,"cacheRead":208384,"cacheWrite":0,"totalTokens":212703,"reasoningTokens":69,"cost":{"input":0.020300000000000002,"output":0.00777,"cacheRead":0.10419199999999999,"cacheWrite":0,"total":0.132262}},"stopReason":"toolUse","timestamp":1784271596908,"responseId":"resp_038f28d3e93bbfd4016a59d2ed26248196a45eaff2f65d0f4e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2f12a74819692d190436e814adb","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdLzJ3SsemOXWGs06kzJVwrLROd_-BHC9kJVvzvjcm1UCODeEnTqQNzbO8hNi-ML_En6s1I8F3p6pyVpHE-bGGquVQnInO3LD17GI8WE2uDaTGl5jmTLH-ndKYETqEUeaMTj2YYbEF9HzT2nsKY0VjCwIFO7Tn3VYoj1eqRU9LoDhmmRAYeoFfqY4gCoZnp9Nd8B7htxVKI9kdiczU7qNgD4DRbAhdject4r9YMNPQabM7jt2XbDxT5wWKstDZknmSa96IH73Bu-wpXvCz2dFXL5B0jlTUVJ7hsjMjAldVMXNodgIyAuWHqmasqoFDeHm2TBrDlZXejMnPPYz08kl5Ab-fH29dIMpV9pFDVZSgIB6bHJcXjx0K76Npex2hPoG-TgNnVtvIxI1ADUomKBvlVdEqerX_6kNG7uFaZDbBGcR0ENjpQdStrqGjwCdcSJvV4K2ybgAJVw6YHJXQGdyMjZicRAtSmzvtOP0aWesi5HpOhiAxyi4RyVJO0roIoy9PSpi0DFGgBr5Vd-eD61f1kKoud6oOesyWEI_kRqBTGnFxGkIYnjgUJAPoEJ1UE6plXCr-Tj8PLFPlTTfuWqRj_LxIh_Z79zfBf6lrFqxtaQvfzwY-09yzC7j91sq4F72q5TCgIXAXPJqGtnijslk7En9PuKEm3ktZ3oPHFpXDsZw2kG8ovquSXNDHVg30gyaThAQhjgNt3FKFRFtizIdydRWzwAYqpA6uiIOQtQDjvlK6I9Tfc3ewdz40AFJZ1dQWob76YWKICr8husfrnyxv3YKcvuLf-JPwO17X7zeFPTwKYHrHuULgkZXxjQSf5Vm9vb0kXyxBL-2ct3i_odH4Bhwxz45myFNMes6Btejdq7Hxi3J0ixd2jilJnbU5kr0V8awRspfsb_ZgcNlrH8oK_IxbqjNoz2R3NrfToo7U_xph2qvi8TLfHXQUY7RhQ7TYNxhE5KKghAuZOaftxcND3NWZnQRUJix_oDqEgAbAJf8ehYl4vB9tkt3qJzOK_58dYTFR5LVohKFlH8OS-Z6__Rnq-Y0y0s_1gzCJZz9V1zOluQ9iKQRmt_vDp8yJtShxhcJf-riHigIB6nqEwu5Pd9_-qDth2kpcrpPvlX6BRDkibZ64NZYDCHPRmPkniReCXgKP57x7oMc7NWedAYeQkqouHGsQm3EKWGTob_TW0BYv84A6Z2r0Anwu5ZvjZVEneTxQ7HBsoqKLBwKz82pCY2_dwqtuIWBCa5XxiRjM4JPjQJnTsnAnbc9Q8FYnxVdjkP2Hj1DVdOECzFqHHPP2rPMyCoD6X-T1m6Z6P_PXIgZnI=","summary":[{"type":"summary_text","text":"**Identifying frequent binary reads and planning registry investigation**"}]},{"id":"fc_038f28d3e93bbfd4016a59d2f3e29881968e029e0f83f11188","type":"function_call","status":"completed","arguments":"{\"i\":\"Reporting brain I/O smoking gun\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Brain 64 MiB/s is almost certainly self-hash loop: brainproc.rs:63 BRAIN_HEARTBEAT=500ms; run_brain loop :259-260 then write_ready(generation) :297 each heartbeat; write_ready :365-372 calls current_exe_hash every time; current_exe_hash :381-384 std::fs::read(current_exe) + SHA256. Thus read rate = 2 × spt.exe size/sec; a ~31.96 MiB binary predicts 63.9 MiB/s exactly. Source comment :379 wrongly says one exe read per brain start. Continuing Registry poison lifecycle.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_GWorweI4NRfQgde3I7MWke4u","name":"irc"}]},"duration":9825.265799999237,"ttft":4379.507699996233,"contextSnapshot":{"promptTokens":212444,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GWorweI4NRfQgde3I7MWke4u|fc_038f28d3e93bbfd4016a59d2f3e29881968e029e0f83f11188","toolName":"irc","startedAt":"2026-07-17T07:00:06.760Z","intent":"Reporting brain I/O smoking gun"},"id":"b4843dd5","parentId":"086b44c3","timestamp":"2026-07-17T07:00:06.760Z"}
{"type":"message","id":"41f1c70d","parentId":"b4843dd5","timestamp":"2026-07-17T07:00:06.761Z","message":{"role":"toolResult","toolCallId":"call_GWorweI4NRfQgde3I7MWke4u|fc_038f28d3e93bbfd4016a59d2f3e29881968e029e0f83f11188","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"BrainCpuRca","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784271606760}}
{"type":"message","id":"b0b949e4","parentId":"41f1c70d","timestamp":"2026-07-17T07:00:16.796Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating Registry subscriber poison**"},{"type":"toolCall","id":"call_jiyaDucAhrO18YLZjrpjuSXF|fc_038f28d3e93bbfd4016a59d300b718819682cf21fb6f021ee8","name":"grep","arguments":{"i":"Locating registry serve worker","pattern":"serve_registry|RegistryDecoder|StreamFamily::Registry|net_stream_subscribe|RegistryApply","path":"crates/spt-daemon/src/dispatch.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Wl6eZA6zZoZoqCdP8MFVHFdB|fc_038f28d3e93bbfd4016a59d300b72c8196bbe2b1e8f7bdbcd7","name":"read","arguments":{"i":"Reading dispatcher worker lifecycle","path":"crates/spt-daemon/src/dispatch.rs","selector":"590-950"}},{"type":"toolCall","id":"call_UXkogNBydWMryMy5C2Qb4PT3|fc_038f28d3e93bbfd4016a59d300b738819685f27ab1dcfe24cd","name":"grep","arguments":{"i":"Tracing stream subscription handler","pattern":"KIND_NET_STREAM_SUBSCRIBE|NetStreamSubscribeReq|subscribe_stream|SubscriberSeat::install|subscriber busy|replay halted","path":"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Qq5DxN5Dl4wMzYZDA6bVfeaJ|fc_038f28d3e93bbfd4016a59d300b7408196abd993b7a9fcec84","name":"grep","arguments":{"i":"Tracing stream finish delivery","pattern":"fn finish|finish\\(|NetStreamEof|eof_envelope|finished","path":"crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_RBgPiYKNWRgIWCR9Uve0diM9|fc_038f28d3e93bbfd4016a59d300b748819686c6ee7e479923fe","name":"grep","arguments":{"i":"Tracing registry feed sender","pattern":"push_feed|net_stream_send|finish|peer_step","path":"crates/spt-daemon/src/pump/registry.rs; crates/spt-daemon/src/pump/mod.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":1268,"output":384,"cacheRead":211456,"cacheWrite":0,"totalTokens":213108,"reasoningTokens":29,"cost":{"input":0.00634,"output":0.01152,"cacheRead":0.10572799999999999,"cacheWrite":0,"total":0.12358799999999999}},"stopReason":"toolUse","timestamp":1784271606776,"responseId":"resp_038f28d3e93bbfd4016a59d2f7191c819696a5e53c12316cae","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d2f9c7c88196aadac30feacc99f5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdMAO5ycnBolIdgbcYXACRkVWcJry0haqXdyrqmLVcWhSJ9sVrTdC14_z9u46rnldHbXSLKVoe-Xi_sXzpW4jyj6PXCHljMKGfvwlveY5ICMBzyl_8l0_jmC634vAh-NyEqRWorhAI4q-1LFkEC6tKV__CoRY9KY7ypBYNjcHncGftdfZTJJRXO5UluoQVPhaYvpONDwxg25e3IMtVMnYM5jV1XgiPLO3-n7tiuLK56ABUVSudderCiCR7STnxzcbPH8lJBaKsyFdudFMGA982WkKQ7-PfKL_FcIIagWetkBEtZPCZAEsDejf-FXqX35LrUCNjXKC2FphADpsZJB74S_7HKumSGmcKVaOvdeLeXwwzwyM81EbS4836FJEVCtYYyGwiZnuQCQdctULHjoilWB5nZkGYr9VRlOe57ETXLuMIyRlwZNW97XfKjxraEpmmZPmnW85laVu3qASDBwhw2AR51TjSlGPAt1-YBqS4Uta1EozaXAX1GK-ra9ENb2wjLL8Ggv93bblvUtW-GrPwIjhRA-wS4GaAaYQgQpKaN-K9TAIPhJtLpkhhkeaTrrG0UbJuotW-6dao_0uEyKHelJIGSV3_6fJM49IsZtB18qa3k64Cf6N-INAQAoHwas6GomFPaXtIqU-TCQTyvnwWgagByPxWlcLaZeCnJovj23ktRF91Z16fGYpJM0JDf-It6goJuEKONpTpc68PcwBgeVFk4FsO6y1p6P7EnNozqo9YYDmeCO4k0x2ka4STZ2UCKS3Uuvno1qGjLigKyMsrP4NK02RbLdOJg4GT-Bt5m_khwwXAg0fO71pVIigOir6N_D2Maijr7H0H2mZ55qjMeWMaD1oGhwF_sZoUGcCXHnrtL5SnMLQzIr5sikAMENppzwtWK7eXw3H1TLlCgRKvd-yvs4xqiMAs0h7p62f-judThEouVlFXbQs4InuWfHVwpmkN6LG9PrOxRUQow7U2H7hXr7ya5S8axHTQUZvZ7fHvrcGH0MMlNlXESC2HAbHAcNucflScQrFuO6nvoGWb68tM-hh8r5jFYoGliuQLJ8VrXWZvGHB85kda9oP3eSAMQnjnKy_j0ZK2K11NcDafW6AnQoIdGZj76nx35Mg4snYz1JKKY1gCBLftSvlaGn3h92","summary":[{"type":"summary_text","text":"**Investigating Registry subscriber poison**"}]},{"id":"fc_038f28d3e93bbfd4016a59d300b718819682cf21fb6f021ee8","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating registry serve worker\",\"pattern\":\"serve_registry|RegistryDecoder|StreamFamily::Registry|net_stream_subscribe|RegistryApply\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_jiyaDucAhrO18YLZjrpjuSXF","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d300b72c8196bbe2b1e8f7bdbcd7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher worker lifecycle\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"590-950\"}","call_id":"call_Wl6eZA6zZoZoqCdP8MFVHFdB","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d300b738819685f27ab1dcfe24cd","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing stream subscription handler\",\"pattern\":\"KIND_NET_STREAM_SUBSCRIBE|NetStreamSubscribeReq|subscribe_stream|SubscriberSeat::install|subscriber busy|replay halted\",\"path\":\"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_UXkogNBydWMryMy5C2Qb4PT3","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d300b7408196abd993b7a9fcec84","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing stream finish delivery\",\"pattern\":\"fn finish|finish\\\\(|NetStreamEof|eof_envelope|finished\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Qq5DxN5Dl4wMzYZDA6bVfeaJ","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d300b748819686c6ee7e479923fe","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing registry feed sender\",\"pattern\":\"push_feed|net_stream_send|finish|peer_step\",\"path\":\"crates/spt-daemon/src/pump/registry.rs; crates/spt-daemon/src/pump/mod.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_RBgPiYKNWRgIWCR9Uve0diM9","name":"grep"}]},"duration":10018.661100000143,"ttft":3022.5183999985456,"contextSnapshot":{"promptTokens":212724,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jiyaDucAhrO18YLZjrpjuSXF|fc_038f28d3e93bbfd4016a59d300b718819682cf21fb6f021ee8","toolName":"grep","startedAt":"2026-07-17T07:00:16.796Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Locating registry serve worker"},"id":"da45517f","parentId":"b0b949e4","timestamp":"2026-07-17T07:00:16.796Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Wl6eZA6zZoZoqCdP8MFVHFdB|fc_038f28d3e93bbfd4016a59d300b72c8196bbe2b1e8f7bdbcd7","toolName":"read","startedAt":"2026-07-17T07:00:16.796Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher worker lifecycle"},"id":"4de3c661","parentId":"da45517f","timestamp":"2026-07-17T07:00:16.796Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UXkogNBydWMryMy5C2Qb4PT3|fc_038f28d3e93bbfd4016a59d300b738819685f27ab1dcfe24cd","toolName":"grep","startedAt":"2026-07-17T07:00:16.796Z","args":{"path":"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs"},"intent":"Tracing stream subscription handler"},"id":"e45640cd","parentId":"4de3c661","timestamp":"2026-07-17T07:00:16.796Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Qq5DxN5Dl4wMzYZDA6bVfeaJ|fc_038f28d3e93bbfd4016a59d300b7408196abd993b7a9fcec84","toolName":"grep","startedAt":"2026-07-17T07:00:16.797Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Tracing stream finish delivery"},"id":"e28a7c90","parentId":"e45640cd","timestamp":"2026-07-17T07:00:16.797Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RBgPiYKNWRgIWCR9Uve0diM9|fc_038f28d3e93bbfd4016a59d300b748819686c6ee7e479923fe","toolName":"grep","startedAt":"2026-07-17T07:00:16.797Z","args":{"path":"crates/spt-daemon/src/pump/registry.rs; crates/spt-daemon/src/pump/mod.rs"},"intent":"Tracing registry feed sender"},"id":"f28c1a13","parentId":"e28a7c90","timestamp":"2026-07-17T07:00:16.797Z"}
{"type":"message","id":"76664a53","parentId":"f28c1a13","timestamp":"2026-07-17T07:00:16.799Z","message":{"role":"toolResult","toolCallId":"call_jiyaDucAhrO18YLZjrpjuSXF|fc_038f28d3e93bbfd4016a59d300b718819682cf21fb6f021ee8","toolName":"grep","content":[{"type":"text","text":" 66|use spt_net::net::replicate::{\n*67|    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n 68|};\n 69|use spt_net::net::wanmsg::{WanDecoder, WanReply};\n 70|use spt_store::contextstore::ContextStore;\n...\n 141|            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\n*142|                StreamFamily::Registry\n 143|            } else if line.get(\"subnet\").is_some()\n 144|                && line.get(\"node\").is_some()\n 145|                && line.get(\"label\").is_some()\n...\n 149|                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n*150|                StreamFamily::Registry\n 151|            } else {\n 152|                StreamFamily::Unknown\n 153|            }\n...\n 663|    let mut brain = connect(broker_name)?;\n*664|    brain.net_stream_subscribe(stream_id, floor_seq)?;\n 665|    let mut buf: Vec<u8> = Vec::new();\n 666|    loop {\n 667|        match brain.read_event()? {\n...\n 830|        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n*831|        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n 832|        StreamFamily::Attach => {\n 833|            // The gate subject is the session's OWNING endpoint, resolved\n 834|            // from the broker's table (the spawn's label) — never from wire\n...\n 983|// [impl->REQ-INST-7]\n*984|fn serve_registry_feed(\n 985|    brain: &mut Brain,\n 986|    stream_id: u64,\n 987|    origin: &str,\n...\n 990|    let policy = RegistryGatePolicy::load();\n*991|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 992|        return DispatchOutcome::Failed(e.to_string());\n 993|    }\n*994|    let mut decoder = RegistryDecoder::new();\n 995|    let mut merged = 0usize;\n 996|    loop {\n 997|        match brain.read_event() {\n...\n 1075|        .unwrap_or_default();\n*1076|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 1077|        return DispatchOutcome::Failed(e.to_string());\n 1078|    }\n 1079|    let mut decoder = NotifDecoder::new();\n...\n 1148|) -> DispatchOutcome {\n*1149|    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 1150|        return DispatchOutcome::Failed(e.to_string());\n 1151|    }\n 1152|    let mut decoder = WanDecoder::new();\n...\n 1248|            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n*1249|        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 1250|    }\n 1251|\n 1252|    // [unit->REQ-NET-1] unclassifiable shapes are Unknown — dropped loud,\n...\n 1512|            (StreamFamily::Sync, json!({\"endpoint_id\": \"x\", \"endpoint\": \"x\"}), None),\n*1513|            (StreamFamily::Registry, json!({\"target\": \"x\"}), None),\n 1514|            // The right family with a blank/missing field -> None, not \"\".\n 1515|            (StreamFamily::Attach, json!({\"endpoint_id\": \"\"}), None),\n 1516|            (StreamFamily::Attach, json!({\"session_id\": 3}), None),\n...\n 1543|            StreamFamily::WanMsg,\n*1544|            StreamFamily::Registry,\n 1545|            StreamFamily::Xfer,\n 1546|            StreamFamily::Rest,\n 1547|            StreamFamily::ShellLink,"}],"details":{"scopePath":"crates/spt-daemon/src/dispatch.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":13,"fileCount":1,"files":["crates/spt-daemon/src/dispatch.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/dispatch.rs","count":13}],"truncated":false,"displayContent":"   66│use spt_net::net::replicate::{\n  *67│    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n   68│};\n   69│use spt_net::net::wanmsg::{WanDecoder, WanReply};\n   70│use spt_store::contextstore::ContextStore;\n     │...\n  141│            } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {\n *142│                StreamFamily::Registry\n  143│            } else if line.get(\"subnet\").is_some()\n  144│                && line.get(\"node\").is_some()\n  145│                && line.get(\"label\").is_some()\n     │...\n  149│                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n *150│                StreamFamily::Registry\n  151│            } else {\n  152│                StreamFamily::Unknown\n  153│            }\n     │...\n  663│    let mut brain = connect(broker_name)?;\n *664│    brain.net_stream_subscribe(stream_id, floor_seq)?;\n  665│    let mut buf: Vec<u8> = Vec::new();\n  666│    loop {\n  667│        match brain.read_event()? {\n     │...\n  830│        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n *831│        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n  832│        StreamFamily::Attach => {\n  833│            // The gate subject is the session's OWNING endpoint, resolved\n  834│            // from the broker's table (the spawn's label) — never from wire\n     │...\n  983│// [impl->REQ-INST-7]\n *984│fn serve_registry_feed(\n  985│    brain: &mut Brain,\n  986│    stream_id: u64,\n  987│    origin: &str,\n     │...\n  990│    let policy = RegistryGatePolicy::load();\n *991│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n  992│        return DispatchOutcome::Failed(e.to_string());\n  993│    }\n *994│    let mut decoder = RegistryDecoder::new();\n  995│    let mut merged = 0usize;\n  996│    loop {\n  997│        match brain.read_event() {\n     │...\n 1075│        .unwrap_or_default();\n*1076│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 1077│        return DispatchOutcome::Failed(e.to_string());\n 1078│    }\n 1079│    let mut decoder = NotifDecoder::new();\n     │...\n 1148│) -> DispatchOutcome {\n*1149│    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n 1150│        return DispatchOutcome::Failed(e.to_string());\n 1151│    }\n 1152│    let mut decoder = WanDecoder::new();\n     │...\n 1248│            \"instance\": {\"node\": \"n1\", \"status\": \"Active\", \"epoch\": 4}});\n*1249│        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);\n 1250│    }\n 1251│\n 1252│    // [unit->REQ-NET-1] unclassifiable shapes are Unknown — dropped loud,\n     │...\n 1512│            (StreamFamily::Sync, json!({\"endpoint_id\": \"x\", \"endpoint\": \"x\"}), None),\n*1513│            (StreamFamily::Registry, json!({\"target\": \"x\"}), None),\n 1514│            // The right family with a blank/missing field -> None, not \"\".\n 1515│            (StreamFamily::Attach, json!({\"endpoint_id\": \"\"}), None),\n 1516│            (StreamFamily::Attach, json!({\"session_id\": 3}), None),\n     │...\n 1543│            StreamFamily::WanMsg,\n*1544│            StreamFamily::Registry,\n 1545│            StreamFamily::Xfer,\n 1546│            StreamFamily::Rest,\n 1547│            StreamFamily::ShellLink,"},"isError":false,"timestamp":1784271616799}}
{"type":"message","id":"656740d9","parentId":"76664a53","timestamp":"2026-07-17T07:00:16.801Z","message":{"role":"toolResult","toolCallId":"call_Qq5DxN5Dl4wMzYZDA6bVfeaJ|fc_038f28d3e93bbfd4016a59d300b7408196abd993b7a9fcec84","toolName":"grep","content":[{"type":"text","text":" 49|use crate::msg::{\n*50|    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,\n 51|    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,\n 52|};\n 53|\n...\n 354|    lease: Arc<StreamLease>,\n*355|    /// Read side finished cleanly (peer called finish / conn ended).\n*356|    finished: bool,\n 357|    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n 358|    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n 359|    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n...\n 382|            lease: Arc::new(StreamLease::default()),\n*383|            finished: false,\n 384|            retentive_cap: 0,\n 385|            opener: OpenerCapture::Buffering(Vec::new()),\n 386|        }\n...\n 478|    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n*479|    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n*480|    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n 481|    /// pending yet.\n 482|    fn drain(&mut self) -> (Vec<u8>, bool) {\n 483|        let mut out = Vec::new();\n...\n 486|        }\n*487|        (out, self.finished)\n 488|    }\n 489|\n*490|    /// Mark the read side finished and tell the subscriber (with the final\n 491|    /// cursor, so it can verify completeness).\n*492|    fn finish(&mut self) {\n*493|        self.finished = true;\n 494|        if let Some(seat) = &self.subscriber {\n*495|            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n 496|            // Enqueue only — same discipline as append (Amendment fix 2:\n 497|            // ALL producer sites halt-and-remove, finish included).\n 498|            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n...\n 505|    /// Install `sub` as the subscriber with the replay snapshot (retained\n*506|    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\n 507|    /// its seat's WRITER THREAD, which drains it before any live frame — the\n 508|    /// replay-then-live seq order is structural (see\n 509|    /// [`SubscriberSeat::install`]), while every conn write still happens\n...\n 550|            .collect();\n*551|        if self.finished {\n*552|            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\n 553|        }\n 554|        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\n 555|        // on the fix PR): each subscriber generation gets its OWN Arc, and a\n...\n 756|    /// `finish` / duplex `shutdown`).\n*757|    async fn finish(&mut self) -> io::Result<()> {\n 758|        match self {\n*759|            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),\n 760|            SendHalf::Loopback(w) => w.shutdown().await,\n 761|        }\n 762|    }\n...\n 795|    /// fire-and-forget, so a retire that dropped this entry could overtake the\n*796|    /// still-queued reply inside the broker and reset the never-finished QUIC\n 797|    /// send half (requester sees torn-EOF, no reply). The reply-flush ordering\n 798|    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n 799|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n...\n 1042|                    Ok(None) | Err(_) => {\n*1043|                        log.lock().unwrap().finish();\n 1044|                        break;\n 1045|                    }\n 1046|                }\n...\n 1053|                        Ok(0) | Err(_) => {\n*1054|                            log.lock().unwrap().finish();\n 1055|                            break;\n 1056|                        }\n 1057|                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n...\n 1784|            if finish {\n*1785|                send.finish().await?;\n 1786|            }\n 1787|            Ok(())\n 1788|        })\n...\n 1816|                    next_seq: log.next_seq,\n*1817|                    finished: log.finished,\n 1818|                    initiated_locally: e.initiated_locally,\n 1819|                    floor_seq: log.floor_seq(),\n 1820|                }\n...\n 1888|    /// may have been lagging the sender at the backpressure cap). Returns\n*1889|    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n*1890|    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.\n 1891|    // [impl->REQ-SHELL-4]\n 1892|    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n 1893|        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n...\n 2100|        // The BYTES are retained + drained in arrival order, framing-agnostic.\n*2101|        let (bytes, _finished) = log.drain();\n 2102|        assert_eq!(\n 2103|            bytes,\n 2104|            b\"xa-much-larger-coalesced-read-carrying-many-logical-sender-writes\".to_vec()\n...\n 2218|        }\n*2219|        let (bytes, finished) = ret.drain();\n 2220|        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), \"retentive loses nothing, in order\");\n*2221|        assert!(!finished);\n 2222|        // …and a second drain is empty (the cursor consumed them exactly once).\n 2223|        assert_eq!(ret.drain().0, Vec::<u8>::new());\n 2224|\n...\n 2290|        let mut got: Vec<u8> = Vec::new();\n*2291|        let mut finished = false;\n 2292|        for _ in 0..2000 {\n 2293|            if let Some((mut bytes, fin)) = host.drain_stream(shell) {\n 2294|                got.append(&mut bytes);\n 2295|                if fin {\n*2296|                    finished = true;\n 2297|                    break;\n 2298|                }\n 2299|            }\n...\n 2302|        writer.join().expect(\"writer joins\");\n*2303|        assert!(finished, \"the tunnel saw EOF after the owner finished\");\n 2304|        let expected: Vec<u8> = frames.concat();\n 2305|        assert_eq!(got.len(), expected.len(), \"no byte lost or duplicated\");\n 2306|        assert_eq!(got, expected, \"byte-exact, in order, under backpressure\");\n...\n 2430|    // processed FIRST. Pre-fix, retire removed the entry: the late send hit\n*2431|    // no-such-stream (or the drop reset the never-finished send half) and the\n 2432|    // requester got torn-EOF instead of its reply (wansend NoReply, CI red\n 2433|    // @2a110ee, 30/150 under contention vs 0/150 with retire disabled).\n 2434|    #[test]\n...\n 2438|        // The requester's one-shot request: write + FIN (the rest/sync shape —\n*2439|        // the row is born finished on the serving side).\n 2440|        host.send_stream(\n 2441|            owner,\n 2442|            b\"{\\\"kind\\\":\\\"request\\\",\\\"endpoint\\\":\\\"e\\\",\\\"event\\\":\\\"suspend\\\"}\\n\",\n...\n 2453|        let mut got = Vec::new();\n*2454|        let mut finished = false;\n 2455|        for _ in 0..400 {\n 2456|            let (bytes, fin) = host.drain_stream(owner).expect(\"owner row exists\");\n 2457|            got.extend_from_slice(&bytes);\n 2458|            if fin {\n*2459|                finished = true;\n 2460|                break;\n 2461|            }\n 2462|            std::thread::sleep(Duration::from_millis(5));\n...\n 2467|        );\n*2468|        assert!(finished, \"the reply's FIN reached the requester side (clean end, not torn)\");\n 2469|    }\n 2470|\n 2471|    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] a dead CONNECTION retires its\n...\n 2603|    // attach only fails): eof delivery through a poisoned seat removes it\n*2604|    // and cancels the lease, and the log still records finished.\n 2605|    #[test]\n*2606|    fn finish_halts_and_removes_a_poisoned_seat() {\n 2607|        let (sub, client, _recv) = seat_socket_pair(Duration::from_secs(5));\n 2608|        let mut log = StreamLog::new(8, 64);\n 2609|        log.begin_attach(Arc::clone(&sub), 0).expect(\"attach\");\n...\n 2619|\n*2620|        log.finish();\n*2621|        assert!(log.finished, \"the read side still records its clean end\");\n 2622|        assert!(log.subscriber.is_none(), \"halt-and-remove at the finish site\");\n 2623|        assert!(lease.is_canceled(), \"poison at finish cancels the lease too\");\n 2624|    }"}],"details":{"scopePath":"crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":38,"fileCount":1,"files":["crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/nethost.rs","count":38}],"truncated":false,"displayContent":"   49│use crate::msg::{\n  *50│    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,\n   51│    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,\n   52│};\n   53│\n     │...\n  354│    lease: Arc<StreamLease>,\n *355│    /// Read side finished cleanly (peer called finish / conn ended).\n *356│    finished: bool,\n  357│    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n  358│    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n  359│    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n     │...\n  382│            lease: Arc::new(StreamLease::default()),\n *383│            finished: false,\n  384│            retentive_cap: 0,\n  385│            opener: OpenerCapture::Buffering(Vec::new()),\n  386│        }\n     │...\n  478│    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n *479│    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n *480│    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n  481│    /// pending yet.\n  482│    fn drain(&mut self) -> (Vec<u8>, bool) {\n  483│        let mut out = Vec::new();\n     │...\n  486│        }\n *487│        (out, self.finished)\n  488│    }\n  489│\n *490│    /// Mark the read side finished and tell the subscriber (with the final\n  491│    /// cursor, so it can verify completeness).\n *492│    fn finish(&mut self) {\n *493│        self.finished = true;\n  494│        if let Some(seat) = &self.subscriber {\n *495│            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n  496│            // Enqueue only — same discipline as append (Amendment fix 2:\n  497│            // ALL producer sites halt-and-remove, finish included).\n  498│            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n     │...\n  505│    /// Install `sub` as the subscriber with the replay snapshot (retained\n *506│    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\n  507│    /// its seat's WRITER THREAD, which drains it before any live frame — the\n  508│    /// replay-then-live seq order is structural (see\n  509│    /// [`SubscriberSeat::install`]), while every conn write still happens\n     │...\n  550│            .collect();\n *551│        if self.finished {\n *552│            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\n  553│        }\n  554│        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\n  555│        // on the fix PR): each subscriber generation gets its OWN Arc, and a\n     │...\n  756│    /// `finish` / duplex `shutdown`).\n *757│    async fn finish(&mut self) -> io::Result<()> {\n  758│        match self {\n *759│            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),\n  760│            SendHalf::Loopback(w) => w.shutdown().await,\n  761│        }\n  762│    }\n     │...\n  795│    /// fire-and-forget, so a retire that dropped this entry could overtake the\n *796│    /// still-queued reply inside the broker and reset the never-finished QUIC\n  797│    /// send half (requester sees torn-EOF, no reply). The reply-flush ordering\n  798│    /// is load-bearing; physical teardown belongs to the conn-close sweep only.\n  799│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n     │...\n 1042│                    Ok(None) | Err(_) => {\n*1043│                        log.lock().unwrap().finish();\n 1044│                        break;\n 1045│                    }\n 1046│                }\n     │...\n 1053│                        Ok(0) | Err(_) => {\n*1054│                            log.lock().unwrap().finish();\n 1055│                            break;\n 1056│                        }\n 1057│                        Ok(n) => log.lock().unwrap().append(&buf[..n]),\n     │...\n 1784│            if finish {\n*1785│                send.finish().await?;\n 1786│            }\n 1787│            Ok(())\n 1788│        })\n     │...\n 1816│                    next_seq: log.next_seq,\n*1817│                    finished: log.finished,\n 1818│                    initiated_locally: e.initiated_locally,\n 1819│                    floor_seq: log.floor_seq(),\n 1820│                }\n     │...\n 1888│    /// may have been lagging the sender at the backpressure cap). Returns\n*1889│    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).\n*1890│    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.\n 1891│    // [impl->REQ-SHELL-4]\n 1892│    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n 1893│        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n     │...\n 2100│        // The BYTES are retained + drained in arrival order, framing-agnostic.\n*2101│        let (bytes, _finished) = log.drain();\n 2102│        assert_eq!(\n 2103│            bytes,\n 2104│            b\"xa-much-larger-coalesced-read-carrying-many-logical-sender-writes\".to_vec()\n     │...\n 2218│        }\n*2219│        let (bytes, finished) = ret.drain();\n 2220│        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), \"retentive loses nothing, in order\");\n*2221│        assert!(!finished);\n 2222│        // …and a second drain is empty (the cursor consumed them exactly once).\n 2223│        assert_eq!(ret.drain().0, Vec::<u8>::new());\n 2224│\n     │...\n 2290│        let mut got: Vec<u8> = Vec::new();\n*2291│        let mut finished = false;\n 2292│        for _ in 0..2000 {\n 2293│            if let Some((mut bytes, fin)) = host.drain_stream(shell) {\n 2294│                got.append(&mut bytes);\n 2295│                if fin {\n*2296│                    finished = true;\n 2297│                    break;\n 2298│                }\n 2299│            }\n     │...\n 2302│        writer.join().expect(\"writer joins\");\n*2303│        assert!(finished, \"the tunnel saw EOF after the owner finished\");\n 2304│        let expected: Vec<u8> = frames.concat();\n 2305│        assert_eq!(got.len(), expected.len(), \"no byte lost or duplicated\");\n 2306│        assert_eq!(got, expected, \"byte-exact, in order, under backpressure\");\n     │...\n 2430│    // processed FIRST. Pre-fix, retire removed the entry: the late send hit\n*2431│    // no-such-stream (or the drop reset the never-finished send half) and the\n 2432│    // requester got torn-EOF instead of its reply (wansend NoReply, CI red\n 2433│    // @2a110ee, 30/150 under contention vs 0/150 with retire disabled).\n 2434│    #[test]\n     │...\n 2438│        // The requester's one-shot request: write + FIN (the rest/sync shape —\n*2439│        // the row is born finished on the serving side).\n 2440│        host.send_stream(\n 2441│            owner,\n 2442│            b\"{\\\"kind\\\":\\\"request\\\",\\\"endpoint\\\":\\\"e\\\",\\\"event\\\":\\\"suspend\\\"}\\n\",\n     │...\n 2453│        let mut got = Vec::new();\n*2454│        let mut finished = false;\n 2455│        for _ in 0..400 {\n 2456│            let (bytes, fin) = host.drain_stream(owner).expect(\"owner row exists\");\n 2457│            got.extend_from_slice(&bytes);\n 2458│            if fin {\n*2459│                finished = true;\n 2460│                break;\n 2461│            }\n 2462│            std::thread::sleep(Duration::from_millis(5));\n     │...\n 2467│        );\n*2468│        assert!(finished, \"the reply's FIN reached the requester side (clean end, not torn)\");\n 2469│    }\n 2470│\n 2471│    // [unit->REQ-REDISPATCH-FINISHED-RETIRE] a dead CONNECTION retires its\n     │...\n 2603│    // attach only fails): eof delivery through a poisoned seat removes it\n*2604│    // and cancels the lease, and the log still records finished.\n 2605│    #[test]\n*2606│    fn finish_halts_and_removes_a_poisoned_seat() {\n 2607│        let (sub, client, _recv) = seat_socket_pair(Duration::from_secs(5));\n 2608│        let mut log = StreamLog::new(8, 64);\n 2609│        log.begin_attach(Arc::clone(&sub), 0).expect(\"attach\");\n     │...\n 2619│\n*2620│        log.finish();\n*2621│        assert!(log.finished, \"the read side still records its clean end\");\n 2622│        assert!(log.subscriber.is_none(), \"halt-and-remove at the finish site\");\n 2623│        assert!(lease.is_canceled(), \"poison at finish cancels the lease too\");\n 2624│    }"},"isError":false,"timestamp":1784271616801}}
{"type":"message","id":"4bdc6bcc","parentId":"656740d9","timestamp":"2026-07-17T07:00:16.807Z","message":{"role":"toolResult","toolCallId":"call_RBgPiYKNWRgIWCR9Uve0diM9|fc_038f28d3e93bbfd4016a59d300b748819686c6ee7e479923fe","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/pump/\n## registry.rs\n 7|//! rotations → re-advertise) ONCE per due round and holds the produced adverts\n*8|//! as worker state; `peer_step` pushes this node's own rows for the given\n 9|//! subnet over the peer's feed stream. Construction-captured deps: the shared\n 10|//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the\n 11|//! two registry cadence knobs.\n...\n 82|    evict_after: Duration,\n*83|    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\n 84|    adverts: Vec<RegistryFeedRecord>,\n 85|}\n 86|\n...\n 123|\n*124|    /// The node-local sweep, run ONCE per due round before any `peer_step`\n 125|    /// (§Frozen-7, order preserved verbatim):\n 126|    fn pre_round(&mut self, _ctx: &RoundCtx) {\n 127|        // Consume the re-pair eviction marker FIRST (M8 decision 13,\n...\n 153|        // Re-advertise this node's own rows; held as worker state for the\n*154|        // per-subnet `peer_step` filter below.\n 155|        self.adverts = self.registry.advertise_local(&self.owlery);\n 156|    }\n 157|\n...\n 160|    /// stream per feed.\n*161|    fn peer_step(\n 162|        &mut self,\n 163|        io: &mut PeerIo,\n 164|        subnet: &str,\n...\n 174|        if !lines.is_empty() {\n*175|            io.push_feed(&lines)?;\n 176|        }\n 177|        Ok(())\n 178|    }\n## mod.rs\n 7|//! whom* (every roster member of every non-detached member subnet); each worker\n*8|//! owns *what* (its node-local `pre_round` and its per-peer `peer_step`), a thin\n 9|//! call over an already-shipped, idempotent driver. The four legs, in fan-out\n 10|//! order, each in its own module:\n 11|//!\n...\n 34|//! choreography table below): [`compute_due`] (prime-all / wake-forces-one),\n*35|//! [`pre_rounds`] (once per due round, before any peer_step), [`run_due`]\n 36|//! (abort-on-first-fail), [`mark_ran`] (the §V4 stagger), [`fan_targets`]\n 37|//! (detached-skip). LLM-bearing work is never a [`PumpWorker`] (KNOWN-HAZARDS\n 38|//! 7.4) — see the trait's contract.\n...\n 49|//! Every pump write is fire-and-forget (`op_id: None`) — a journaled\n*50|//! `net_stream_send` blocks on its ack and DISCARDS interleaved events\n 51|//! (gotcha #1). Stream opens for push feeds carry no op either: the feeds\n 52|//! are idempotent by construction. The sync pull's `open_op` IS durable\n 53|//! (exactly-once open identity), minted from a dedicated [`EpochSource`]\n...\n 140|/// dial-eligible when `now >= next_due`. A `DIAL_FAILED` (or a mid-round\n*141|/// `peer_step` failure — Q3) doubles `backoff` (capped [`PEER_BACKOFF_CAP`]) and\n 142|/// pushes `next_due` out; a successful connect drops the entry (reset). Kept in a\n 143|/// `HashMap<peer_hex, PeerSched>` on the pump loop — RAM-only, re-primed on a\n 144|/// supervised restart like `WorkerLasts`.\n...\n 363|/// (per-subnet × per-roster-member). The pump owns *when* and *toward whom*;\n*364|/// the worker owns *what* — its node-local `pre_round` and its `peer_step`.\n 365|///\n 366|/// **KH 7.4 — never an LLM-bearing worker.** A pump worker is cadenced,\n 367|/// fire-and-forget peer IO. LLM-bearing work (the bounded Psyche reconcile\n...\n 382|\n*383|    /// Node-local work run ONCE per due round, before any `peer_step`.\n 384|    /// Default: nothing.\n 385|    fn pre_round(&mut self, _ctx: &RoundCtx) {}\n 386|\n...\n 389|    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\n*390|    fn peer_step(\n 391|        &mut self,\n 392|        io: &mut PeerIo,\n 393|        subnet: &str,\n...\n 460|    /// A minimal empty round context for worker unit tests whose `pre_round` /\n*461|    /// `peer_step` ignore it (the registry leg keys off its own state, not the\n 462|    /// shared loads).\n 463|    fn for_test() -> RoundCtx {\n 464|        RoundCtx {\n...\n 493|    /// Open a fresh stream and write one whole feed fire-and-forget (the\n*494|    /// push-feed discipline — `op_id: None`, finished on send).\n*495|    fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {\n*496|        push_feed(self.brain, self.conn_id, feed)\n 497|    }\n 498|\n 499|    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n...\n 514|/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\n*515|/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\n*516|/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\n 517|/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\n 518|/// logic lives here — the shell owns only *when* and *toward whom*.\n 519|pub fn run_peer_pump(\n...\n 540|    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\n*541|    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\n 542|    // whose reply-reads consume every event kind off their carrier; sharing one\n 543|    // carrier would make a pull-read swallow a presence frame (advancing the\n 544|    // exactly-once cursor while dropping the outcome — the D4c break). Two\n...\n 620|        // Each due worker's node-local pre_round, once per round before ANY\n*621|        // peer_step (only the registry leg does work here — its sweep +\n 622|        // advertise; the rest default to a no-op).\n 623|        pre_rounds(&mut workers, &due_flags, &ctx);\n 624|\n...\n 754|/// Run each due worker's node-local `pre_round` ONCE for the round, before any\n*755|/// peer_step (the registry sweep + advertise; the other legs default no-op).\n 756|fn pre_rounds(workers: &mut [&mut dyn PumpWorker], due_flags: &[bool], ctx: &RoundCtx) {\n 757|    for i in 0..workers.len() {\n 758|        if due_flags[i] {\n...\n 781|/// `TimedOut` by `Brain::read_peer_reply_until`) is a PER-PEER failure: drop THAT\n*782|/// conn + back the peer off (Q3 — a mid-round `peer_step` fail isolates exactly\n 783|/// like a `DIAL_FAILED`, never round-wide), the round continues, the heartbeat\n 784|/// advances. A genuine broker-IPC-CARRIER desync — a raw `TimedOut` (the carrier\n 785|/// ops `net_open_stream` / `subscribe` / `send` keep it) — STILL bubbles `Err` to\n...\n 809|\n*810|/// Run every due worker's `peer_step` for a CONNECTED peer across all subnets it\n 811|/// targets this round (one conn serves them all), applying [`peer_leg_outcome`]\n 812|/// after each — an ordinary failure drops the conn + backs the peer off and stops\n 813|/// this peer's remaining legs; a carrier desync bubbles to a supervised restart.\n...\n 836|            };\n*837|            workers[i].peer_step(&mut io, sub_name, peer_hex, ctx)\n 838|        });\n 839|        peer_leg_outcome(outcome, peer_hex, conns, sched, now)?;\n 840|        if !conns.contains_key(peer_hex) {\n...\n 1116|/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,\n*1117|/// and finish it (the receiver's dispatcher classifies + applies). One\n 1118|/// stream per feed: streams are single-purpose by the demux contract."}],"details":{"scopePath":"crates/spt-daemon/src/pump/registry.rs, crates/spt-daemon/src/pump/mod.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":26,"fileCount":2,"files":["crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/pump/mod.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/pump/registry.rs","count":6},{"path":"crates/spt-daemon/src/pump/mod.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/pump/\n## registry.rs\n   7│//! rotations → re-advertise) ONCE per due round and holds the produced adverts\n  *8│//! as worker state; `peer_step` pushes this node's own rows for the given\n   9│//! subnet over the peer's feed stream. Construction-captured deps: the shared\n  10│//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the\n  11│//! two registry cadence knobs.\n    │...\n  82│    evict_after: Duration,\n *83│    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\n  84│    adverts: Vec<RegistryFeedRecord>,\n  85│}\n  86│\n    │...\n 123│\n*124│    /// The node-local sweep, run ONCE per due round before any `peer_step`\n 125│    /// (§Frozen-7, order preserved verbatim):\n 126│    fn pre_round(&mut self, _ctx: &RoundCtx) {\n 127│        // Consume the re-pair eviction marker FIRST (M8 decision 13,\n    │...\n 153│        // Re-advertise this node's own rows; held as worker state for the\n*154│        // per-subnet `peer_step` filter below.\n 155│        self.adverts = self.registry.advertise_local(&self.owlery);\n 156│    }\n 157│\n    │...\n 160│    /// stream per feed.\n*161│    fn peer_step(\n 162│        &mut self,\n 163│        io: &mut PeerIo,\n 164│        subnet: &str,\n    │...\n 174│        if !lines.is_empty() {\n*175│            io.push_feed(&lines)?;\n 176│        }\n 177│        Ok(())\n 178│    }\n## mod.rs\n    7│//! whom* (every roster member of every non-detached member subnet); each worker\n   *8│//! owns *what* (its node-local `pre_round` and its per-peer `peer_step`), a thin\n    9│//! call over an already-shipped, idempotent driver. The four legs, in fan-out\n   10│//! order, each in its own module:\n   11│//!\n     │...\n   34│//! choreography table below): [`compute_due`] (prime-all / wake-forces-one),\n  *35│//! [`pre_rounds`] (once per due round, before any peer_step), [`run_due`]\n   36│//! (abort-on-first-fail), [`mark_ran`] (the §V4 stagger), [`fan_targets`]\n   37│//! (detached-skip). LLM-bearing work is never a [`PumpWorker`] (KNOWN-HAZARDS\n   38│//! 7.4) — see the trait's contract.\n     │...\n   49│//! Every pump write is fire-and-forget (`op_id: None`) — a journaled\n  *50│//! `net_stream_send` blocks on its ack and DISCARDS interleaved events\n   51│//! (gotcha #1). Stream opens for push feeds carry no op either: the feeds\n   52│//! are idempotent by construction. The sync pull's `open_op` IS durable\n   53│//! (exactly-once open identity), minted from a dedicated [`EpochSource`]\n     │...\n  140│/// dial-eligible when `now >= next_due`. A `DIAL_FAILED` (or a mid-round\n *141│/// `peer_step` failure — Q3) doubles `backoff` (capped [`PEER_BACKOFF_CAP`]) and\n  142│/// pushes `next_due` out; a successful connect drops the entry (reset). Kept in a\n  143│/// `HashMap<peer_hex, PeerSched>` on the pump loop — RAM-only, re-primed on a\n  144│/// supervised restart like `WorkerLasts`.\n     │...\n  363│/// (per-subnet × per-roster-member). The pump owns *when* and *toward whom*;\n *364│/// the worker owns *what* — its node-local `pre_round` and its `peer_step`.\n  365│///\n  366│/// **KH 7.4 — never an LLM-bearing worker.** A pump worker is cadenced,\n  367│/// fire-and-forget peer IO. LLM-bearing work (the bounded Psyche reconcile\n     │...\n  382│\n *383│    /// Node-local work run ONCE per due round, before any `peer_step`.\n  384│    /// Default: nothing.\n  385│    fn pre_round(&mut self, _ctx: &RoundCtx) {}\n  386│\n     │...\n  389│    /// conn (redial next tick) — abort-on-first-fail, current behaviour.\n *390│    fn peer_step(\n  391│        &mut self,\n  392│        io: &mut PeerIo,\n  393│        subnet: &str,\n     │...\n  460│    /// A minimal empty round context for worker unit tests whose `pre_round` /\n *461│    /// `peer_step` ignore it (the registry leg keys off its own state, not the\n  462│    /// shared loads).\n  463│    fn for_test() -> RoundCtx {\n  464│        RoundCtx {\n     │...\n  493│    /// Open a fresh stream and write one whole feed fire-and-forget (the\n *494│    /// push-feed discipline — `op_id: None`, finished on send).\n *495│    fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {\n *496│        push_feed(self.brain, self.conn_id, feed)\n  497│    }\n  498│\n  499│    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n     │...\n  514│/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\n *515│/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\n *516│/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\n  517│/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\n  518│/// logic lives here — the shell owns only *when* and *toward whom*.\n  519│pub fn run_peer_pump(\n     │...\n  540│    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\n *541│    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\n  542│    // whose reply-reads consume every event kind off their carrier; sharing one\n  543│    // carrier would make a pull-read swallow a presence frame (advancing the\n  544│    // exactly-once cursor while dropping the outcome — the D4c break). Two\n     │...\n  620│        // Each due worker's node-local pre_round, once per round before ANY\n *621│        // peer_step (only the registry leg does work here — its sweep +\n  622│        // advertise; the rest default to a no-op).\n  623│        pre_rounds(&mut workers, &due_flags, &ctx);\n  624│\n     │...\n  754│/// Run each due worker's node-local `pre_round` ONCE for the round, before any\n *755│/// peer_step (the registry sweep + advertise; the other legs default no-op).\n  756│fn pre_rounds(workers: &mut [&mut dyn PumpWorker], due_flags: &[bool], ctx: &RoundCtx) {\n  757│    for i in 0..workers.len() {\n  758│        if due_flags[i] {\n     │...\n  781│/// `TimedOut` by `Brain::read_peer_reply_until`) is a PER-PEER failure: drop THAT\n *782│/// conn + back the peer off (Q3 — a mid-round `peer_step` fail isolates exactly\n  783│/// like a `DIAL_FAILED`, never round-wide), the round continues, the heartbeat\n  784│/// advances. A genuine broker-IPC-CARRIER desync — a raw `TimedOut` (the carrier\n  785│/// ops `net_open_stream` / `subscribe` / `send` keep it) — STILL bubbles `Err` to\n     │...\n  809│\n *810│/// Run every due worker's `peer_step` for a CONNECTED peer across all subnets it\n  811│/// targets this round (one conn serves them all), applying [`peer_leg_outcome`]\n  812│/// after each — an ordinary failure drops the conn + backs the peer off and stops\n  813│/// this peer's remaining legs; a carrier desync bubbles to a supervised restart.\n     │...\n  836│            };\n *837│            workers[i].peer_step(&mut io, sub_name, peer_hex, ctx)\n  838│        });\n  839│        peer_leg_outcome(outcome, peer_hex, conns, sched, now)?;\n  840│        if !conns.contains_key(peer_hex) {\n     │...\n 1116│/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,\n*1117│/// and finish it (the receiver's dispatcher classifies + applies). One\n 1118│/// stream per feed: streams are single-purpose by the demux contract."},"isError":false,"timestamp":1784271616807}}
{"type":"message","id":"29fa0710","parentId":"4bdc6bcc","timestamp":"2026-07-17T07:00:16.808Z","message":{"role":"toolResult","toolCallId":"call_UXkogNBydWMryMy5C2Qb4PT3|fc_038f28d3e93bbfd4016a59d300b738819685f27ab1dcfe24cd","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 57|    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n*58|    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n 59|    NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n 60|    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n 61|    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n...\n 66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n...\n 3244|                }\n*3245|                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n 3246|                    Ok(id) => my_stream_subs.push(id),\n 3247|                    Err(msg) => send_error(&send, &msg),\n 3248|                },\n...\n 4430|    ) -> Result<u64, String> {\n*4431|        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)\n 4432|            .map_err(|e| format!(\"bad net-stream-subscribe payload: {e}\"))?;\n 4433|        let host = self.net.get().ok_or(\"net disabled\")?;\n*4434|        host.subscribe_stream(req.stream_id, Arc::clone(send), req.from_seq)\n 4435|            .map_err(|e| e.to_string())?;\n 4436|        Ok(req.stream_id)\n 4437|    }\n## nethost.rs\n 508|    /// replay-then-live seq order is structural (see\n*509|    /// [`SubscriberSeat::install`]), while every conn write still happens\n 510|    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\n 511|    /// under the lock with discarded write errors, the exact serial-poison\n 512|    /// engine of the v0.34 field stalls). Returns the replay frame count.\n...\n 528|                    format!(\n*529|                        \"stream {} subscriber busy: prior subscriber still draining\",\n 530|                        self.stream_id\n 531|                    ),\n 532|                ));\n...\n 569|        let replay_len = replay.len();\n*570|        self.subscriber = Some(SubscriberSeat::install(\n 571|            sub,\n 572|            replay,\n 573|            Some(Arc::clone(&self.lease)),\n...\n 698|                    io::ErrorKind::WouldBlock,\n*699|                    \"presence subscriber busy: prior subscriber still draining\",\n 700|                ));\n 701|            }\n 702|            self.subscriber = None;\n...\n 716|        // No lease on the presence channel (it has no paired serve leg).\n*717|        self.subscriber = Some(SubscriberSeat::install(sub, replay, None));\n 718|        Ok(replay_len)\n 719|    }\n 720|\n...\n 1949|    // [impl->REQ-DAEMON-2]\n*1950|    pub(crate) fn subscribe_stream(\n 1951|        &self,\n 1952|        stream_id: u64,\n 1953|        sub: SharedSend,\n...\n 1994|    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n*1995|        // Same atomic install-with-replay discipline as subscribe_stream\n 1996|        // (the PresenceLog mirror): the seat's writer drains the replay\n 1997|        // before any live event, conn I/O outside the mutex. A replay write\n 1998|        // failure poisons the seat; the next push halt-and-removes it — the\n...\n 2628|    // prior seat whose writer is still draining refuses a replacement\n*2629|    // (`WouldBlock` \"subscriber busy\" — the string the dispatcher classifies\n 2630|    // SeatBlocked), a HEALTHY prior is DISPLACED as before (the\n 2631|    // newer-brain-takes-over contract a blanket no-reinstall broke — it\n 2632|    // TIMEOUTed the applyhost brain-swap tests), and a fully-GONE prior\n...\n 2653|        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);\n*2654|        assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");\n 2655|\n 2656|        // Fully gone → the replacement installs.\n 2657|        log.subscriber.as_ref().unwrap().done.store(true, Ordering::Release);\n...\n 2840|        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);\n*2841|        assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");\n 2842|    }\n 2843|\n 2844|    // [unit->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] the lease gate at the send"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":15,"fileCount":2,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":5},{"path":"crates/spt-daemon/src/nethost.rs","count":10}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n   57│    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,\n  *58│    BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,\n   59│    NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,\n   60│    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,\n   61│    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,\n     │...\n   66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  *67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n   69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n   70│};\n     │...\n 3244│                }\n*3245│                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n 3246│                    Ok(id) => my_stream_subs.push(id),\n 3247│                    Err(msg) => send_error(&send, &msg),\n 3248│                },\n     │...\n 4430│    ) -> Result<u64, String> {\n*4431│        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)\n 4432│            .map_err(|e| format!(\"bad net-stream-subscribe payload: {e}\"))?;\n 4433│        let host = self.net.get().ok_or(\"net disabled\")?;\n*4434│        host.subscribe_stream(req.stream_id, Arc::clone(send), req.from_seq)\n 4435│            .map_err(|e| e.to_string())?;\n 4436│        Ok(req.stream_id)\n 4437│    }\n## nethost.rs\n  508│    /// replay-then-live seq order is structural (see\n *509│    /// [`SubscriberSeat::install`]), while every conn write still happens\n  510│    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\n  511│    /// under the lock with discarded write errors, the exact serial-poison\n  512│    /// engine of the v0.34 field stalls). Returns the replay frame count.\n     │...\n  528│                    format!(\n *529│                        \"stream {} subscriber busy: prior subscriber still draining\",\n  530│                        self.stream_id\n  531│                    ),\n  532│                ));\n     │...\n  569│        let replay_len = replay.len();\n *570│        self.subscriber = Some(SubscriberSeat::install(\n  571│            sub,\n  572│            replay,\n  573│            Some(Arc::clone(&self.lease)),\n     │...\n  698│                    io::ErrorKind::WouldBlock,\n *699│                    \"presence subscriber busy: prior subscriber still draining\",\n  700│                ));\n  701│            }\n  702│            self.subscriber = None;\n     │...\n  716│        // No lease on the presence channel (it has no paired serve leg).\n *717│        self.subscriber = Some(SubscriberSeat::install(sub, replay, None));\n  718│        Ok(replay_len)\n  719│    }\n  720│\n     │...\n 1949│    // [impl->REQ-DAEMON-2]\n*1950│    pub(crate) fn subscribe_stream(\n 1951│        &self,\n 1952│        stream_id: u64,\n 1953│        sub: SharedSend,\n     │...\n 1994│    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n*1995│        // Same atomic install-with-replay discipline as subscribe_stream\n 1996│        // (the PresenceLog mirror): the seat's writer drains the replay\n 1997│        // before any live event, conn I/O outside the mutex. A replay write\n 1998│        // failure poisons the seat; the next push halt-and-removes it — the\n     │...\n 2628│    // prior seat whose writer is still draining refuses a replacement\n*2629│    // (`WouldBlock` \"subscriber busy\" — the string the dispatcher classifies\n 2630│    // SeatBlocked), a HEALTHY prior is DISPLACED as before (the\n 2631│    // newer-brain-takes-over contract a blanket no-reinstall broke — it\n 2632│    // TIMEOUTed the applyhost brain-swap tests), and a fully-GONE prior\n     │...\n 2653│        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);\n*2654│        assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");\n 2655│\n 2656│        // Fully gone → the replacement installs.\n 2657│        log.subscriber.as_ref().unwrap().done.store(true, Ordering::Release);\n     │...\n 2840│        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);\n*2841│        assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");\n 2842│    }\n 2843│\n 2844│    // [unit->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] the lease gate at the send"},"isError":false,"timestamp":1784271616808}}
{"type":"message","id":"0ac7747b","parentId":"29fa0710","timestamp":"2026-07-17T07:00:16.820Z","message":{"role":"toolResult","toolCallId":"call_Wl6eZA6zZoZoqCdP8MFVHFdB|fc_038f28d3e93bbfd4016a59d300b72c8196bbe2b1e8f7bdbcd7","toolName":"read","content":[{"type":"text","text":"/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn pool_has_room(active: usize) -> bool {\n    active < MAX_DISPATCH_WORKERS\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n/// it survives any ring roll — and fall back to the ring peek only when the\n/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n/// (terminal); `Err` = transient (the claim requeues bounded).\nfn first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n        // Pinned: classify from the durable fact. A pinned line that does not\n        // parse is terminally unclassifiable (same contract as the peek path).\n        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\n        // The broker holds the stream but no opener is pinned yet (first\n        // newline still in flight, or the capture gave up bounded). The\n        // next_seq guard proved bytes exist, so treat as transient — the\n        // bounded claim retry re-reads it shortly, and a stream that never\n        // pins goes terminal when the budget is spent (no hot loop).\n        Ok(None) => {\n            return Err(io::Error::other(format!(\n                \"stream {stream_id}: opener not pinned yet\"\n            )))\n        }\n        // ONLY the old broker's explicit unknown-verb answer falls through\n        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n        // retry then manufactured a throwaway replay subscriber against a\n        // possibly-wedged conn (the v0.34 stall engine's first gear).\n        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n        Err(e) if is_unsupported_verb(&e) => {}\n        // Everything else is a transient: Failed -> classified bounded\n        // requeue, never a second replay subscriber.\n        Err(e) => return Err(e),\n    }\n    peek_first_line(broker_name, stream_id, floor_seq)\n}\n\n/// Peek the stream's first retained NDJSON line on a throwaway brain\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\n/// gap check.\nfn peek_first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n/// The endpoint identity a stream's opener names, where the family carries\n/// one (Amendment fix 9 — hertz's live catch on stream 7058 could not bind\n/// the stalled stream to an endpoint; this key is what makes the next catch\n/// a bound stream→endpoint proof). Families without an endpoint concept\n/// answer `None` honestly.\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn line_endpoint(family: StreamFamily, line: &serde_json::Value) -> Option<String> {\n    let key = match family {\n        StreamFamily::Attach => \"endpoint_id\",\n        StreamFamily::Rest | StreamFamily::Xfer => \"endpoint\",\n        StreamFamily::ShellLink => \"owner\",\n        StreamFamily::WanMsg => \"target\",\n        _ => return None,\n    };\n    line.get(key)\n        .and_then(|v| v.as_str())\n        .filter(|s| !s.is_empty())\n        .map(str::to_string)\n}\n\n/// A worker's report: the outcome plus the telemetry keys it resolved\n/// (family + endpoint, once the opener classified) — Amendment fix 9.\nstruct WorkerReport {\n    outcome: DispatchOutcome,\n    family: Option<StreamFamily>,\n    endpoint: Option<String>,\n}\n\nimpl WorkerReport {\n    fn bare(outcome: DispatchOutcome) -> WorkerReport {\n        WorkerReport {\n            outcome,\n            family: None,\n            endpoint: None,\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> WorkerReport {\n    if take_injected_transient() {\n        return WorkerReport::bare(DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        ));\n    }\n    if take_injected_seatblocked() {\n        return WorkerReport::bare(DispatchOutcome::Failed(\n            \"injected subscriber busy failure (test valve)\".into(),\n        ));\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return WorkerReport::bare(DispatchOutcome::Unknown),\n        // Pre-serve terminal exclusion (Amendment fix 6 rider): a row that\n        // vanished between enumeration and this query (retired by a sibling\n        // worker's Served report, or conn-swept) is TERMINAL — report Served\n        // so the claim closes now, instead of burning three transient\n        // retries against a stream that no longer exists.\n        // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\n        Err(e) if e.to_string().contains(\"no such stream\") => {\n            return WorkerReport::bare(DispatchOutcome::Served(\n                \"terminal pre-serve: row already gone\".into(),\n            ))\n        }\n        Err(e) => return WorkerReport::bare(DispatchOutcome::Failed(e.to_string())),\n    };\n    let family = classify_first_line(&line);\n    let endpoint = line_endpoint(family, &line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return WorkerReport {\n            outcome: DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into()),\n            family: Some(family),\n            endpoint,\n        };\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => {\n            return WorkerReport {\n                outcome: DispatchOutcome::Failed(e.to_string()),\n                family: Some(family),\n                endpoint,\n            }\n        }\n    };\n\n    let outcome = match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => {\n                    return WorkerReport {\n                        outcome: DispatchOutcome::Failed(e.to_string()),\n                        family: Some(family),\n                        endpoint,\n                    }\n                }\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                // Seed the serve loop with the DURABLE opener Request\n                // (ADR-0038 B): after a ring roll the wire replay no longer\n                // carries the Request record, so the serve would idle forever\n                // waiting for one. The pinned opener IS that record — hand it\n                // over so reconstruction resumes the same operator stream.\n                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n                // gone, and a below-floor cursor trips the brain's gap check.\n                // [impl->REQ-STREAM-OPENER-DURABLE]\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    floor_seq,\n                    remote_session,\n                    serde_json::from_value(line.clone()).ok(),\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n                Some(endpoint) => {\n                    let root = spt_store::xfer::transfers_dir();\n                    let progress = paths.scratch.join(\"xfer-progress\");\n                    match serve_xfer(\n                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\n                    ) {\n                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n                    }\n                }\n                None => {\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n                }\n            }\n        }\n        StreamFamily::Rest => {\n            // The wire names whose whitelist gates (the xfer posture); the\n            // origin stays handshake-proven. The serve applies the event\n            // through the LOCAL transition host — echo gate, wake effects,\n            // and shell cascade run on this node, where the instance lives.\n            // [impl->REQ-INST-6]\n            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ShellLink => {\n            // The wire names the owner whose whitelist gates AND whose shell\n            // tree resolves (owner exclusivity = resolution scope); the\n            // action runs against the LOCAL owlery — perch, manifest, spool,\n            // and binary all live here, the shell's node.\n            // [impl->REQ-SHELL-2]\n            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ServeProbe => {\n            // No gate (serveprobe module docs): the answer is public\n            // serve-state for a named subnet, read fresh from this node's\n…\n        }\n…\n    };\n…\n}\n\n[Showing lines 589-953 of 1558. Use :954 to continue]"}],"details":{"truncation":{"content":"/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn pool_has_room(active: usize) -> bool {\n    active < MAX_DISPATCH_WORKERS\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n/// it survives any ring roll — and fall back to the ring peek only when the\n/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n/// (terminal); `Err` = transient (the claim requeues bounded).\nfn first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n        // Pinned: classify from the durable fact. A pinned line that does not\n        // parse is terminally unclassifiable (same contract as the peek path).\n        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\n        // The broker holds the stream but no opener is pinned yet (first\n        // newline still in flight, or the capture gave up bounded). The\n        // next_seq guard proved bytes exist, so treat as transient — the\n        // bounded claim retry re-reads it shortly, and a stream that never\n        // pins goes terminal when the budget is spent (no hot loop).\n        Ok(None) => {\n            return Err(io::Error::other(format!(\n                \"stream {stream_id}: opener not pinned yet\"\n            )))\n        }\n        // ONLY the old broker's explicit unknown-verb answer falls through\n        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n        // retry then manufactured a throwaway replay subscriber against a\n        // possibly-wedged conn (the v0.34 stall engine's first gear).\n        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n        Err(e) if is_unsupported_verb(&e) => {}\n        // Everything else is a transient: Failed -> classified bounded\n        // requeue, never a second replay subscriber.\n        Err(e) => return Err(e),\n    }\n    peek_first_line(broker_name, stream_id, floor_seq)\n}\n\n/// Peek the stream's first retained NDJSON line on a throwaway brain\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\n/// gap check.\nfn peek_first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n/// The endpoint identity a stream's opener names, where the family carries\n/// one (Amendment fix 9 — hertz's live catch on stream 7058 could not bind\n/// the stalled stream to an endpoint; this key is what makes the next catch\n/// a bound stream→endpoint proof). Families without an endpoint concept\n/// answer `None` honestly.\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn line_endpoint(family: StreamFamily, line: &serde_json::Value) -> Option<String> {\n    let key = match family {\n        StreamFamily::Attach => \"endpoint_id\",\n        StreamFamily::Rest | StreamFamily::Xfer => \"endpoint\",\n        StreamFamily::ShellLink => \"owner\",\n        StreamFamily::WanMsg => \"target\",\n        _ => return None,\n    };\n    line.get(key)\n        .and_then(|v| v.as_str())\n        .filter(|s| !s.is_empty())\n        .map(str::to_string)\n}\n\n/// A worker's report: the outcome plus the telemetry keys it resolved\n/// (family + endpoint, once the opener classified) — Amendment fix 9.\nstruct WorkerReport {\n    outcome: DispatchOutcome,\n    family: Option<StreamFamily>,\n    endpoint: Option<String>,\n}\n\nimpl WorkerReport {\n    fn bare(outcome: DispatchOutcome) -> WorkerReport {\n        WorkerReport {\n            outcome,\n            family: None,\n            endpoint: None,\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> WorkerReport {\n    if take_injected_transient() {\n        return WorkerReport::bare(DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        ));\n    }\n    if take_injected_seatblocked() {\n        return WorkerReport::bare(DispatchOutcome::Failed(\n            \"injected subscriber busy failure (test valve)\".into(),\n        ));\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return WorkerReport::bare(DispatchOutcome::Unknown),\n        // Pre-serve terminal exclusion (Amendment fix 6 rider): a row that\n        // vanished between enumeration and this query (retired by a sibling\n        // worker's Served report, or conn-swept) is TERMINAL — report Served\n        // so the claim closes now, instead of burning three transient\n        // retries against a stream that no longer exists.\n        // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\n        Err(e) if e.to_string().contains(\"no such stream\") => {\n            return WorkerReport::bare(DispatchOutcome::Served(\n                \"terminal pre-serve: row already gone\".into(),\n            ))\n        }\n        Err(e) => return WorkerReport::bare(DispatchOutcome::Failed(e.to_string())),\n    };\n    let family = classify_first_line(&line);\n    let endpoint = line_endpoint(family, &line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return WorkerReport {\n            outcome: DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into()),\n            family: Some(family),\n            endpoint,\n        };\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => {\n            return WorkerReport {\n                outcome: DispatchOutcome::Failed(e.to_string()),\n                family: Some(family),\n                endpoint,\n            }\n        }\n    };\n\n    let outcome = match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => {\n                    return WorkerReport {\n                        outcome: DispatchOutcome::Failed(e.to_string()),\n                        family: Some(family),\n                        endpoint,\n                    }\n                }\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                // Seed the serve loop with the DURABLE opener Request\n                // (ADR-0038 B): after a ring roll the wire replay no longer\n                // carries the Request record, so the serve would idle forever\n                // waiting for one. The pinned opener IS that record — hand it\n                // over so reconstruction resumes the same operator stream.\n                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n                // gone, and a below-floor cursor trips the brain's gap check.\n                // [impl->REQ-STREAM-OPENER-DURABLE]\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    floor_seq,\n                    remote_session,\n                    serde_json::from_value(line.clone()).ok(),\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n                Some(endpoint) => {\n                    let root = spt_store::xfer::transfers_dir();\n                    let progress = paths.scratch.join(\"xfer-progress\");\n                    match serve_xfer(\n                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\n                    ) {\n                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n                    }\n                }\n                None => {\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n                }\n            }\n        }\n        StreamFamily::Rest => {\n            // The wire names whose whitelist gates (the xfer posture); the\n            // origin stays handshake-proven. The serve applies the event\n            // through the LOCAL transition host — echo gate, wake effects,\n            // and shell cascade run on this node, where the instance lives.\n            // [impl->REQ-INST-6]\n            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ShellLink => {\n            // The wire names the owner whose whitelist gates AND whose shell\n            // tree resolves (owner exclusivity = resolution scope); the\n            // action runs against the LOCAL owlery — perch, manifest, spool,\n            // and binary all live here, the shell's node.\n            // [impl->REQ-SHELL-2]\n            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ServeProbe => {\n            // No gate (serveprobe module docs): the answer is public\n            // serve-state for a named subnet, read fresh from this node's","truncated":true,"truncatedBy":"lines","totalLines":970,"totalBytes":16382,"outputLines":365,"outputBytes":16382,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn pool_has_room(active: usize) -> bool {\n    active < MAX_DISPATCH_WORKERS\n}\n\n/// Connect a fresh brain IPC client (retrying through broker startup).\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start(broker_name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// The stream's first NDJSON line, restart-durable (ADR-0038 B,\n/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first —\n/// it survives any ring roll — and fall back to the ring peek only when the\n/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable\n/// (terminal); `Err` = transient (the claim requeues bounded).\nfn first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n        // Pinned: classify from the durable fact. A pinned line that does not\n        // parse is terminally unclassifiable (same contract as the peek path).\n        Ok(Some(bytes)) => return Ok(serde_json::from_slice(&bytes).ok()),\n        // The broker holds the stream but no opener is pinned yet (first\n        // newline still in flight, or the capture gave up bounded). The\n        // next_seq guard proved bytes exist, so treat as transient — the\n        // bounded claim retry re-reads it shortly, and a stream that never\n        // pins goes terminal when the budget is spent (no hot loop).\n        Ok(None) => {\n            return Err(io::Error::other(format!(\n                \"stream {stream_id}: opener not pinned yet\"\n            )))\n        }\n        // ONLY the old broker's explicit unknown-verb answer falls through\n        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n        // retry then manufactured a throwaway replay subscriber against a\n        // possibly-wedged conn (the v0.34 stall engine's first gear).\n        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n        Err(e) if is_unsupported_verb(&e) => {}\n        // Everything else is a transient: Failed -> classified bounded\n        // requeue, never a second replay subscriber.\n        Err(e) => return Err(e),\n    }\n    peek_first_line(broker_name, stream_id, floor_seq)\n}\n\n/// Peek the stream's first retained NDJSON line on a throwaway brain\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\n/// gap check.\nfn peek_first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n/// The endpoint identity a stream's opener names, where the family carries\n/// one (Amendment fix 9 — hertz's live catch on stream 7058 could not bind\n/// the stalled stream to an endpoint; this key is what makes the next catch\n/// a bound stream→endpoint proof). Families without an endpoint concept\n/// answer `None` honestly.\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn line_endpoint(family: StreamFamily, line: &serde_json::Value) -> Option<String> {\n    let key = match family {\n        StreamFamily::Attach => \"endpoint_id\",\n        StreamFamily::Rest | StreamFamily::Xfer => \"endpoint\",\n        StreamFamily::ShellLink => \"owner\",\n        StreamFamily::WanMsg => \"target\",\n        _ => return None,\n    };\n    line.get(key)\n        .and_then(|v| v.as_str())\n        .filter(|s| !s.is_empty())\n        .map(str::to_string)\n}\n\n/// A worker's report: the outcome plus the telemetry keys it resolved\n/// (family + endpoint, once the opener classified) — Amendment fix 9.\nstruct WorkerReport {\n    outcome: DispatchOutcome,\n    family: Option<StreamFamily>,\n    endpoint: Option<String>,\n}\n\nimpl WorkerReport {\n    fn bare(outcome: DispatchOutcome) -> WorkerReport {\n        WorkerReport {\n            outcome,\n            family: None,\n            endpoint: None,\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn worker(\n    broker_name: &str,\n    stream_id: u64,\n    origin: &str,\n    floor_seq: u64,\n    finished: bool,\n    paths: &DispatchPaths,\n    registry: &RegistryHost,\n) -> WorkerReport {\n    if take_injected_transient() {\n        return WorkerReport::bare(DispatchOutcome::Failed(\n            \"injected transient worker-start failure (test valve)\".into(),\n        ));\n    }\n    if take_injected_seatblocked() {\n        return WorkerReport::bare(DispatchOutcome::Failed(\n            \"injected subscriber busy failure (test valve)\".into(),\n        ));\n    }\n    let line = match first_line(broker_name, stream_id, floor_seq) {\n        Ok(Some(v)) => v,\n        Ok(None) => return WorkerReport::bare(DispatchOutcome::Unknown),\n        // Pre-serve terminal exclusion (Amendment fix 6 rider): a row that\n        // vanished between enumeration and this query (retired by a sibling\n        // worker's Served report, or conn-swept) is TERMINAL — report Served\n        // so the claim closes now, instead of burning three transient\n        // retries against a stream that no longer exists.\n        // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\n        Err(e) if e.to_string().contains(\"no such stream\") => {\n            return WorkerReport::bare(DispatchOutcome::Served(\n                \"terminal pre-serve: row already gone\".into(),\n            ))\n        }\n        Err(e) => return WorkerReport::bare(DispatchOutcome::Failed(e.to_string())),\n    };\n    let family = classify_first_line(&line);\n    let endpoint = line_endpoint(family, &line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return WorkerReport {\n            outcome: DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into()),\n            family: Some(family),\n            endpoint,\n        };\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => {\n            return WorkerReport {\n                outcome: DispatchOutcome::Failed(e.to_string()),\n                family: Some(family),\n                endpoint,\n            }\n        }\n    };\n\n    let outcome = match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => {\n                    return WorkerReport {\n                        outcome: DispatchOutcome::Failed(e.to_string()),\n                        family: Some(family),\n                        endpoint,\n                    }\n                }\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n            let (endpoint, remote_session) = match line\n                .get(\"endpoint_id\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n            {\n                Some(ep) => match crate::attach::resolve_local_session(&mut brain, ep) {\n                    Some(sid) => (Some(ep.to_string()), Some(sid)),\n                    None => (None, None),\n                },\n                None => {\n                    let session_id = line.get(\"session_id\").and_then(|v| v.as_u64());\n                    let endpoint = session_id.and_then(|sid| {\n                        brain.sessions().ok().and_then(|reply| {\n                            reply\n                                .sessions\n                                .iter()\n                                .find(|s| s.session_id == sid && !s.endpoint.is_empty())\n                                .map(|s| s.endpoint.clone())\n                        })\n                    });\n                    (endpoint, None)\n                }\n            };\n            match endpoint {\n                // Seed the serve loop with the DURABLE opener Request\n                // (ADR-0038 B): after a ring roll the wire replay no longer\n                // carries the Request record, so the serve would idle forever\n                // waiting for one. The pinned opener IS that record — hand it\n                // over so reconstruction resumes the same operator stream.\n                // Subscribe from the ring FLOOR: a rolled ring's chunk 0 is\n                // gone, and a below-floor cursor trips the brain's gap check.\n                // [impl->REQ-STREAM-OPENER-DURABLE]\n                Some(endpoint) => match serve_attach(\n                    &mut brain,\n                    broker_name,\n                    stream_id,\n                    &endpoint,\n                    origin,\n                    floor_seq,\n                    remote_session,\n                    serde_json::from_value(line.clone()).ok(),\n                ) {\n                    Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                    Err(e) => DispatchOutcome::Failed(e.to_string()),\n                },\n                None => {\n                    // Refuse politely: finish our send side, claim stays.\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"attach refused: unlabeled/unknown session\".into())\n                }\n            }\n        }\n        StreamFamily::Xfer => {\n            // The wire names whose whitelist gates (the rule set); the origin\n            // it is checked against stays handshake-proven. Blank refuses.\n            let endpoint = line\n                .get(\"endpoint\")\n                .and_then(|v| v.as_str())\n                .filter(|e| !e.is_empty())\n                .map(str::to_string);\n            match endpoint {\n                Some(endpoint) => {\n                    let root = spt_store::xfer::transfers_dir();\n                    let progress = paths.scratch.join(\"xfer-progress\");\n                    match serve_xfer(\n                        &mut brain, stream_id, &endpoint, origin, 0, &root, &progress,\n                    ) {\n                        Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                        Err(e) => DispatchOutcome::Failed(e.to_string()),\n                    }\n                }\n                None => {\n                    let _ = brain.net_stream_send(stream_id, &[], None, true);\n                    DispatchOutcome::Served(\"xfer refused: no endpoint named\".into())\n                }\n            }\n        }\n        StreamFamily::Rest => {\n            // The wire names whose whitelist gates (the xfer posture); the\n            // origin stays handshake-proven. The serve applies the event\n            // through the LOCAL transition host — echo gate, wake effects,\n            // and shell cascade run on this node, where the instance lives.\n            // [impl->REQ-INST-6]\n            let knob = crate::config::DaemonConfig::load().auto_suspend_after_ms;\n            match crate::resthost::serve_rest(&mut brain, stream_id, origin, knob) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ShellLink => {\n            // The wire names the owner whose whitelist gates AND whose shell\n            // tree resolves (owner exclusivity = resolution scope); the\n            // action runs against the LOCAL owlery — perch, manifest, spool,\n            // and binary all live here, the shell's node.\n            // [impl->REQ-SHELL-2]\n            match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::ServeProbe => {\n            // No gate (serveprobe module docs): the answer is public\n            // serve-state for a named subnet, read fresh from this node's\n…\n        }\n…\n    };\n…\n}","startLine":589,"lineNumbers":[589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,null,960,null,962,null,968]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1558,"totalBytes":16382,"outputLines":365,"outputBytes":16382,"shownRange":{"start":589,"end":953},"nextOffset":954}}},"isError":false,"timestamp":1784271616820}}
{"type":"message","id":"38e5a8e9","parentId":"0ac7747b","timestamp":"2026-07-17T07:00:24.880Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Requesting additional registry functions**"},{"type":"toolCall","id":"call_jx4aDgPhMLOMEO3zU736WvFk|fc_038f28d3e93bbfd4016a59d308d094819692e4f27d0f17ffc4","name":"read","arguments":{"i":"Reading registry receive lifecycle","path":"crates/spt-daemon/src/dispatch.rs","selector":"970-1045"}},{"type":"toolCall","id":"call_IK5c2WyD4YNWLGXdcDyNjrtz|fc_038f28d3e93bbfd4016a59d308d0ac8196811e4c414f528ffa","name":"read","arguments":{"i":"Reading stream log subscribe","path":"crates/spt-daemon/src/nethost.rs","selector":"340-580,1935-1975"}},{"type":"toolCall","id":"call_dIylKwfC8oKMYuwzMEWfOI7B|fc_038f28d3e93bbfd4016a59d308d0d48196966d1beff47b5aeb","name":"read","arguments":{"i":"Reading feed stream completion","path":"crates/spt-daemon/src/pump/mod.rs","selector":"480-510,1105-1145"}},{"type":"toolCall","id":"call_opVnVgpqj7YOM21QRc5d8x0H|fc_038f28d3e93bbfd4016a59d308d0dc8196b56189e5e036c333","name":"read","arguments":{"i":"Reading broker unsubscribe cleanup","path":"crates/spt-daemon/src/broker.rs","selector":"3210-3275"}},{"type":"toolCall","id":"call_I7xHvAVSCtsYymbu2th8fZKg|fc_038f28d3e93bbfd4016a59d308d0e481968d5d1ad36555b7ab","name":"grep","arguments":{"i":"Finding unsubscribe net streams","pattern":"unsubscribe_stream|my_stream_subs|detach_stream|drop_subscriber|KIND_UNSUBSCRIBE","path":"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10743,"output":271,"cacheRead":212480,"cacheWrite":0,"totalTokens":223494,"reasoningTokens":11,"cost":{"input":0.053715000000000006,"output":0.00813,"cacheRead":0.10624,"cacheWrite":0,"total":0.168085}},"stopReason":"toolUse","timestamp":1784271616873,"responseId":"resp_038f28d3e93bbfd4016a59d30172848196834528648bccfe66","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d3040520819681b17e858f28cfcf","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdMIa-nr5mz1Rp4NE2g2z18gIxljogE0J-PBlYXsfgHDQNUPZNlvpOJKNZsH8VLRba06uLUIZupJKHqupx99j64hmwD7bTwb91Hcua5E4YjbteV3W2akLSUBCnJpNgkd0BH1ZlUR7TbSmdgiPkZ87Ux7jwqn_hFTL78jFzc1VhUwsom4Oc6rb74LgLswdV8zx-8HUnrzsuyzGAZyFEwLkXrujZvujGDDsUMEGobBysjM3mtjhkEGf434sqli7lp_HCKjQERwp8_qJ4GzGOL8K9of3An7sJ0GtJRER1yJVQTX718-c8j_Ea6whx-YyVZhK5hxRNQQH2JrE6Ydtd1PvRKrZ7FKo2xKKSC577vHCHEOUQVGtCGRQPtI_TEQ3xu6YxbHHkbKwNh7oYmBosje6Ep-zgJS3RA0EKTXqhI9j7wjpzQHAMN4rMgZPZ7RlnLfzqgpNkEga6jrMDXXe-4Mr85tASC_ccJGv1BkHXWwh_HBZC1pNPJr0V3Qsx3gMkZJlCjZc1H8_mEcE3RtPHq-y8hSbXVWyxJkUmiWHW6jvyY8F5in6c9ZZWhDaKJ2ct5Q3PxyV008FllcH-GF8emnrUIQyrnnJQe75UljeZWkD_9IrbjaDy5mKbw8garK-Yxjdxe44wm8cnDvtjyl94Z4N8ie15b6VfElMoub1aMpZq1FPhex7E63g1e9yJvpDDJJlaWXBvfKfcyRzKjUmRcUFvksagFWTWhmbRLL9grXPcjHGdCismosEgjEfgM-UfPPqb6gAaBFNrY9Fm-QwiTtw-dOi3CiVJ-_IFtjALBE6JWf21nE5LfPywT1pck6Wn5d5KkVpk_fgB1t2CUT2HznMZRxWOmcgXk9-zNh8L0177RUvI8-sG1yvpG-0MedIm3LslXipY4Wz9sPHROC3VWqU9fCSowPyugVxWFZJIXSJyK9_Uz8GLWHl5BWry7Elg12eDmxZD5P9SHfw6hYfAFC7UdAZw==","summary":[{"type":"summary_text","text":"**Requesting additional registry functions**"}]},{"id":"fc_038f28d3e93bbfd4016a59d308d094819692e4f27d0f17ffc4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry receive lifecycle\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"970-1045\"}","call_id":"call_jx4aDgPhMLOMEO3zU736WvFk","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d308d0ac8196811e4c414f528ffa","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream log subscribe\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"340-580,1935-1975\"}","call_id":"call_IK5c2WyD4YNWLGXdcDyNjrtz","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d308d0d48196966d1beff47b5aeb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading feed stream completion\",\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"selector\":\"480-510,1105-1145\"}","call_id":"call_dIylKwfC8oKMYuwzMEWfOI7B","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d308d0dc8196b56189e5e036c333","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker unsubscribe cleanup\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3210-3275\"}","call_id":"call_opVnVgpqj7YOM21QRc5d8x0H","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d308d0e481968d5d1ad36555b7ab","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding unsubscribe net streams\",\"pattern\":\"unsubscribe_stream|my_stream_subs|detach_stream|drop_subscriber|KIND_UNSUBSCRIBE\",\"path\":\"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_I7xHvAVSCtsYymbu2th8fZKg","name":"grep"}]},"duration":8004.464600011706,"ttft":3161.1740000098944,"contextSnapshot":{"promptTokens":223223,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jx4aDgPhMLOMEO3zU736WvFk|fc_038f28d3e93bbfd4016a59d308d094819692e4f27d0f17ffc4","toolName":"read","startedAt":"2026-07-17T07:00:24.880Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading registry receive lifecycle"},"id":"a0cca859","parentId":"38e5a8e9","timestamp":"2026-07-17T07:00:24.880Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IK5c2WyD4YNWLGXdcDyNjrtz|fc_038f28d3e93bbfd4016a59d308d0ac8196811e4c414f528ffa","toolName":"read","startedAt":"2026-07-17T07:00:24.880Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream log subscribe"},"id":"485736c1","parentId":"a0cca859","timestamp":"2026-07-17T07:00:24.880Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dIylKwfC8oKMYuwzMEWfOI7B|fc_038f28d3e93bbfd4016a59d308d0d48196966d1beff47b5aeb","toolName":"read","startedAt":"2026-07-17T07:00:24.880Z","args":{"path":"crates/spt-daemon/src/pump/mod.rs"},"intent":"Reading feed stream completion"},"id":"b377c516","parentId":"485736c1","timestamp":"2026-07-17T07:00:24.880Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_opVnVgpqj7YOM21QRc5d8x0H|fc_038f28d3e93bbfd4016a59d308d0dc8196b56189e5e036c333","toolName":"read","startedAt":"2026-07-17T07:00:24.880Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker unsubscribe cleanup"},"id":"1e088a75","parentId":"b377c516","timestamp":"2026-07-17T07:00:24.880Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_I7xHvAVSCtsYymbu2th8fZKg|fc_038f28d3e93bbfd4016a59d308d0e481968d5d1ad36555b7ab","toolName":"grep","startedAt":"2026-07-17T07:00:24.880Z","args":{"path":"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs"},"intent":"Finding unsubscribe net streams"},"id":"13abeaa5","parentId":"1e088a75","timestamp":"2026-07-17T07:00:24.880Z"}
{"type":"message","id":"ecb6618e","parentId":"13abeaa5","timestamp":"2026-07-17T07:00:24.902Z","message":{"role":"toolResult","toolCallId":"call_IK5c2WyD4YNWLGXdcDyNjrtz|fc_038f28d3e93bbfd4016a59d308d0ac8196811e4c414f528ffa","toolName":"read","content":[{"type":"text","text":"struct StreamLog {\n…\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    /// The installed subscriber, behind its bounded writer queue (ADR-0038\n    /// Amendment fixes 2+3): producers ENQUEUE here; conn I/O never runs\n    /// under this log's mutex.\n    subscriber: Option<SubscriberSeat>,\n    /// The stream's serve lease (Amendment fix 4): canceled on subscriber\n    /// POISON so the paired serve/wire worker's sends refuse.\n    lease: Arc<StreamLease>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n    /// The pinned first NDJSON line (ADR-0038 B) — one bounded side fact per\n    /// stream, independent of the ring's eviction. Fed by [`append`](Self::append).\n    opener: OpenerCapture,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            lease: Arc::new(StreamLease::default()),\n            finished: false,\n            retentive_cap: 0,\n            opener: OpenerCapture::Buffering(Vec::new()),\n        }\n    }\n\n    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\n    /// `None` while still buffering or when capture gave up (`Oversize`).\n    fn opener_line(&self) -> Option<Vec<u8>> {\n        match &self.opener {\n            OpenerCapture::Pinned(line) => Some(line.clone()),\n            _ => None,\n        }\n    }\n\n    /// The oldest retained ring seq — the safe subscribe cursor after the ring\n    /// rolled (a `from_seq` below this floor makes the brain's gap check\n    /// reject the resume). `next_seq` when the ring is empty.\n    fn floor_seq(&self) -> u64 {\n        self.ring.front().map(|(s, _)| *s).unwrap_or(self.next_seq)\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\n        // `Oversize` are terminal states.\n        // [impl->REQ-STREAM-OPENER-DURABLE]\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n            match bytes.iter().position(|&b| b == b'\\n') {\n                Some(nl) => {\n                    buf.extend_from_slice(&bytes[..nl]);\n                    let line = std::mem::take(buf);\n                    self.opener = if line.len() > OPENER_PIN_MAX {\n                        OpenerCapture::Oversize\n                    } else {\n                        OpenerCapture::Pinned(line)\n                    };\n                }\n                None => {\n                    buf.extend_from_slice(bytes);\n                    if buf.len() > OPENER_PIN_MAX {\n                        self.opener = OpenerCapture::Oversize;\n                    }\n                }\n            }\n        }\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(seat) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // ENQUEUE only (ADR-0038 Amendment fix 3): the conn I/O runs on\n            // the seat's writer thread, outside this mutex — a wedged\n            // subscriber can no longer park this producer OR the lock.\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n            if let Err(fail) = seat.enqueue(frame) {\n                self.drop_seat(fail);\n            }\n        }\n    }\n\n    /// Remove the current seat after an enqueue failure (halt-at-first-\n    /// failure, all producer sites): a POISON also cancels the stream's\n    /// lease so the paired serve/wire worker terminates (Amendment fix 4);\n    /// an OVERFLOW just detaches — the brain resumes from its cursor.\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn drop_seat(&mut self, fail: SeatFail) {\n        if let Some(seat) = self.subscriber.take() {\n            seat.sub.lifecycle_event(\n                \"stream-sub-halt\",\n                &format!(\"stream={} reason={fail:?}\", self.stream_id),\n            );\n            if fail == SeatFail::Poisoned {\n                self.lease.cancel();\n            }\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(seat) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Enqueue only — same discipline as append (Amendment fix 2:\n            // ALL producer sites halt-and-remove, finish included).\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n            if let Err(fail) = seat.enqueue(frame) {\n                self.drop_seat(fail);\n            }\n        }\n    }\n\n    /// Install `sub` as the subscriber with the replay snapshot (retained\n    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\n    /// its seat's WRITER THREAD, which drains it before any live frame — the\n    /// replay-then-live seq order is structural (see\n    /// [`SubscriberSeat::install`]), while every conn write still happens\n    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\n    /// under the lock with discarded write errors, the exact serial-poison\n    /// engine of the v0.34 field stalls). Returns the replay frame count.\n    ///\n    /// Refuses while a prior seat writer is still alive (`WouldBlock`):\n    /// a replacement subscriber is never installed until the prior one is\n    /// fully gone (REQ-DISPATCH-FALLBACK-CIRCUIT no-reinstall rule).\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\n        if let Some(prior) = &self.subscriber {\n            // No-reinstall-until-gone applies to the POISON class only\n            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\n            // 15s bounded write — installing a replacement now would hand the\n            // retry machinery a fresh replay victim. A HEALTHY prior seat is\n            // DISPLACED as before (the newer-brain-takes-over contract).\n            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\n                return Err(io::Error::new(\n                    io::ErrorKind::WouldBlock,\n                    format!(\n                        \"stream {} subscriber busy: prior subscriber still draining\",\n                        self.stream_id\n                    ),\n                ));\n            }\n            // Healthy displacement, prior fully gone, or same sub re-attach.\n            self.subscriber = None;\n        }\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        let mut replay: Vec<Envelope> = self\n            .ring\n            .iter()\n            .filter(|(s, _)| *s >= from_seq)\n            .map(|(seq, bytes)| net_stream_data_envelope(self.stream_id, *seq, bytes))\n            .collect();\n        if self.finished {\n            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\n        }\n        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\n        // on the fix PR): each subscriber generation gets its OWN Arc, and a\n        // prior writer — poisoned OR healthily displaced — holds only its own\n        // generation's handle. Renewal-on-canceled alone was WRONG: a healthy\n        // brain-swap displacement left the displaced writer and the successor\n        // SHARING one Arc, so the displaced writer's eventual queued-frame\n        // write failure (its conn dying — every daemon refresh) canceled the\n        // SUCCESSOR's lease: a spurious serve-kill + breaker trip per\n        // displaced stream on exactly the post-refresh path this wave calms.\n        // One poison never dead-ends the stream (T5 recovery): the next\n        // generation's mint IS the renewal; `send_stream` reads this current\n        // Arc, old workers' handles stay theirs (C2 consumes via\n        // [`NetHost::stream_lease`]).\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n        self.lease = Arc::new(StreamLease::default());\n        let replay_len = replay.len();\n        self.subscriber = Some(SubscriberSeat::install(\n            sub,\n            replay,\n            Some(Arc::clone(&self.lease)),\n        ));\n        Ok(replay_len)\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n…\n    }\n}\n…\n    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {\n…\n        let (conn_id, _) = self.dial_loopback();\n        let before = self.inbound_rows_on(conn_id);\n        let owner_stream = self.open_stream(conn_id)?;\n        let shell_stream = self\n            .inbound_rows_on(conn_id)\n            .difference(&before)\n            .copied()\n            .next()\n            .ok_or_else(|| io::Error::other(\"loopback peer row not registered\"))?;\n        Ok((owner_stream, shell_stream))\n    }\n\n    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n    // [impl->REQ-DAEMON-2]\n    pub(crate) fn subscribe_stream(\n        &self,\n        stream_id: u64,\n        sub: SharedSend,\n        from_seq: u64,\n    ) -> io::Result<()> {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            let e = streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?;\n            Arc::clone(&e.log)\n        };\n        // Install the seat WITH its replay snapshot in one atomic critical\n        // section (ADR-0038 Amendment fix 3, gate follow-up): the snapshot\n        // handoff is a memory move; the seat's writer thread performs every\n        // conn write outside the mutex, replay first, live after — so a\n        // producer appending concurrently can never slot a live frame into\n        // the middle of the replay (the \"got seq 134 want 6\" re-serve red).\n        // A replay write failure poisons the seat writer-side; the next\n        // producer enqueue halt-and-removes it (the T1 discipline).\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n        log.lock().unwrap().begin_attach(Arc::clone(&sub), from_seq)?;\n…\n    }"}],"details":{"displayContent":{"text":"struct StreamLog {\n…\n    /// `read_chunk` (or split one), so `next_seq` can be < the number of logical\n    /// chunks the sender issued for the SAME bytes. Correctness (byte-exact +\n    /// exactly-once) holds via these transport seqs + the recv cursor regardless;\n    /// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows\n    /// coalescing flake) and must gate on BYTES instead.\n    next_seq: u64,\n    ring: VecDeque<(u64, Vec<u8>)>,\n    cap_chunks: usize,\n    /// The installed subscriber, behind its bounded writer queue (ADR-0038\n    /// Amendment fixes 2+3): producers ENQUEUE here; conn I/O never runs\n    /// under this log's mutex.\n    subscriber: Option<SubscriberSeat>,\n    /// The stream's serve lease (Amendment fix 4): canceled on subscriber\n    /// POISON so the paired serve/wire worker's sends refuse.\n    lease: Arc<StreamLease>,\n    /// Read side finished cleanly (peer called finish / conn ended).\n    finished: bool,\n    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not\n    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must\n    /// NOT drop bytes — CONTEXT:262 \"tunneled protocols cannot drop frames, so\n    /// congestion surfaces as lag, never loss.\" So a retentive log never cap-evicts\n    /// on append (no loss), and the read pump REFUSES to pull the next chunk once\n    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the\n    /// transport's native flow-control then lags the *sender*. A chunk leaves the\n    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is\n    /// woken to resume via the entry's `room` notifier. Ordinary streams keep\n    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an\n    /// unbounded ring) and are drained by subscribe, never the sync cursor.\n    retentive_cap: usize,\n    /// The pinned first NDJSON line (ADR-0038 B) — one bounded side fact per\n    /// stream, independent of the ring's eviction. Fed by [`append`](Self::append).\n    opener: OpenerCapture,\n}\n\nimpl StreamLog {\n    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n            stream_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            lease: Arc::new(StreamLease::default()),\n            finished: false,\n            retentive_cap: 0,\n            opener: OpenerCapture::Buffering(Vec::new()),\n        }\n    }\n\n    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\n    /// `None` while still buffering or when capture gave up (`Oversize`).\n    fn opener_line(&self) -> Option<Vec<u8>> {\n        match &self.opener {\n            OpenerCapture::Pinned(line) => Some(line.clone()),\n            _ => None,\n        }\n    }\n\n    /// The oldest retained ring seq — the safe subscribe cursor after the ring\n    /// rolled (a `from_seq` below this floor makes the brain's gap check\n    /// reject the resume). `next_seq` when the ring is empty.\n    fn floor_seq(&self) -> u64 {\n        self.ring.front().map(|(s, _)| *s).unwrap_or(self.next_seq)\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\n        // `Oversize` are terminal states.\n        // [impl->REQ-STREAM-OPENER-DURABLE]\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n            match bytes.iter().position(|&b| b == b'\\n') {\n                Some(nl) => {\n                    buf.extend_from_slice(&bytes[..nl]);\n                    let line = std::mem::take(buf);\n                    self.opener = if line.len() > OPENER_PIN_MAX {\n                        OpenerCapture::Oversize\n                    } else {\n                        OpenerCapture::Pinned(line)\n                    };\n                }\n                None => {\n                    buf.extend_from_slice(bytes);\n                    if buf.len() > OPENER_PIN_MAX {\n                        self.opener = OpenerCapture::Oversize;\n                    }\n                }\n            }\n        }\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(seat) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // ENQUEUE only (ADR-0038 Amendment fix 3): the conn I/O runs on\n            // the seat's writer thread, outside this mutex — a wedged\n            // subscriber can no longer park this producer OR the lock.\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n            if let Err(fail) = seat.enqueue(frame) {\n                self.drop_seat(fail);\n            }\n        }\n    }\n\n    /// Remove the current seat after an enqueue failure (halt-at-first-\n    /// failure, all producer sites): a POISON also cancels the stream's\n    /// lease so the paired serve/wire worker terminates (Amendment fix 4);\n    /// an OVERFLOW just detaches — the brain resumes from its cursor.\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn drop_seat(&mut self, fail: SeatFail) {\n        if let Some(seat) = self.subscriber.take() {\n            seat.sub.lifecycle_event(\n                \"stream-sub-halt\",\n                &format!(\"stream={} reason={fail:?}\", self.stream_id),\n            );\n            if fail == SeatFail::Poisoned {\n                self.lease.cancel();\n            }\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(seat) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Enqueue only — same discipline as append (Amendment fix 2:\n            // ALL producer sites halt-and-remove, finish included).\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n            if let Err(fail) = seat.enqueue(frame) {\n                self.drop_seat(fail);\n            }\n        }\n    }\n\n    /// Install `sub` as the subscriber with the replay snapshot (retained\n    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\n    /// its seat's WRITER THREAD, which drains it before any live frame — the\n    /// replay-then-live seq order is structural (see\n    /// [`SubscriberSeat::install`]), while every conn write still happens\n    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\n    /// under the lock with discarded write errors, the exact serial-poison\n    /// engine of the v0.34 field stalls). Returns the replay frame count.\n    ///\n    /// Refuses while a prior seat writer is still alive (`WouldBlock`):\n    /// a replacement subscriber is never installed until the prior one is\n    /// fully gone (REQ-DISPATCH-FALLBACK-CIRCUIT no-reinstall rule).\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\n        if let Some(prior) = &self.subscriber {\n            // No-reinstall-until-gone applies to the POISON class only\n            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\n            // 15s bounded write — installing a replacement now would hand the\n            // retry machinery a fresh replay victim. A HEALTHY prior seat is\n            // DISPLACED as before (the newer-brain-takes-over contract).\n            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\n                return Err(io::Error::new(\n                    io::ErrorKind::WouldBlock,\n                    format!(\n                        \"stream {} subscriber busy: prior subscriber still draining\",\n                        self.stream_id\n                    ),\n                ));\n            }\n            // Healthy displacement, prior fully gone, or same sub re-attach.\n            self.subscriber = None;\n        }\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        let mut replay: Vec<Envelope> = self\n            .ring\n            .iter()\n            .filter(|(s, _)| *s >= from_seq)\n            .map(|(seq, bytes)| net_stream_data_envelope(self.stream_id, *seq, bytes))\n            .collect();\n        if self.finished {\n            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\n        }\n        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\n        // on the fix PR): each subscriber generation gets its OWN Arc, and a\n        // prior writer — poisoned OR healthily displaced — holds only its own\n        // generation's handle. Renewal-on-canceled alone was WRONG: a healthy\n        // brain-swap displacement left the displaced writer and the successor\n        // SHARING one Arc, so the displaced writer's eventual queued-frame\n        // write failure (its conn dying — every daemon refresh) canceled the\n        // SUCCESSOR's lease: a spurious serve-kill + breaker trip per\n        // displaced stream on exactly the post-refresh path this wave calms.\n        // One poison never dead-ends the stream (T5 recovery): the next\n        // generation's mint IS the renewal; `send_stream` reads this current\n        // Arc, old workers' handles stay theirs (C2 consumes via\n        // [`NetHost::stream_lease`]).\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n        self.lease = Arc::new(StreamLease::default());\n        let replay_len = replay.len();\n        self.subscriber = Some(SubscriberSeat::install(\n            sub,\n            replay,\n            Some(Arc::clone(&self.lease)),\n        ));\n        Ok(replay_len)\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n…\n    }\n}\n…\n    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {\n…\n        let (conn_id, _) = self.dial_loopback();\n        let before = self.inbound_rows_on(conn_id);\n        let owner_stream = self.open_stream(conn_id)?;\n        let shell_stream = self\n            .inbound_rows_on(conn_id)\n            .difference(&before)\n            .copied()\n            .next()\n            .ok_or_else(|| io::Error::other(\"loopback peer row not registered\"))?;\n        Ok((owner_stream, shell_stream))\n    }\n\n    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`\n    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).\n    // [impl->REQ-DAEMON-2]\n    pub(crate) fn subscribe_stream(\n        &self,\n        stream_id: u64,\n        sub: SharedSend,\n        from_seq: u64,\n    ) -> io::Result<()> {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            let e = streams.get(&stream_id).ok_or_else(|| {\n                io::Error::new(\n                    io::ErrorKind::NotFound,\n                    format!(\"no such stream {stream_id}\"),\n                )\n            })?;\n            Arc::clone(&e.log)\n        };\n        // Install the seat WITH its replay snapshot in one atomic critical\n        // section (ADR-0038 Amendment fix 3, gate follow-up): the snapshot\n        // handoff is a memory move; the seat's writer thread performs every\n        // conn write outside the mutex, replay first, live after — so a\n        // producer appending concurrently can never slot a live frame into\n        // the middle of the replay (the \"got seq 134 want 6\" re-serve red).\n        // A replay write failure poisons the seat writer-side; the next\n        // producer enqueue halt-and-removes it (the T1 discipline).\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n        log.lock().unwrap().begin_attach(Arc::clone(&sub), from_seq)?;\n…\n    }","startLine":335,"lineNumbers":[335,null,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,null,586,587,null,1933,null,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,null,1977]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784271624901}}
{"type":"message","id":"d0c71d53","parentId":"ecb6618e","timestamp":"2026-07-17T07:00:24.903Z","message":{"role":"toolResult","toolCallId":"call_I7xHvAVSCtsYymbu2th8fZKg|fc_038f28d3e93bbfd4016a59d308d0e481968d5d1ad36555b7ab","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n*69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n 71|use crate::nethost::{NetHost, NET_EFFECT_SESSION};\n 72|use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\n...\n 3051|        let mut my_subs: Vec<u64> = Vec::new();\n*3052|        let mut my_stream_subs: Vec<u64> = Vec::new();\n 3053|        let mut my_presence_sub = false;\n 3054|\n 3055|        loop {\n...\n 3069|                },\n*3070|                KIND_UNSUBSCRIBE => {\n 3071|                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n 3072|                        send_error(&send, &msg);\n 3073|                    }\n...\n 3245|                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n*3246|                    Ok(id) => my_stream_subs.push(id),\n 3247|                    Err(msg) => send_error(&send, &msg),\n 3248|                },\n 3249|                KIND_NET_PRESENCE_SUBSCRIBE => {\n...\n 3279|        if let Some(host) = self.net.get() {\n*3280|            for id in &my_stream_subs {\n*3281|                host.detach_stream_sub(*id, &send);\n 3282|            }\n 3283|            // And presence: the liveness log + its ring persist (D4c).\n 3284|            if my_presence_sub {\n## nethost.rs\n 1980|    /// disconnect cleanup; the stream + its ring persist).\n*1981|    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n 1982|        let log = {\n 1983|            let streams = self.shared.streams.lock().unwrap();\n 1984|            streams.get(&stream_id).map(|e| Arc::clone(&e.log))"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":7,"fileCount":2,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":6},{"path":"crates/spt-daemon/src/nethost.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n  *69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n   70│};\n   71│use crate::nethost::{NetHost, NET_EFFECT_SESSION};\n   72│use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\n     │...\n 3051│        let mut my_subs: Vec<u64> = Vec::new();\n*3052│        let mut my_stream_subs: Vec<u64> = Vec::new();\n 3053│        let mut my_presence_sub = false;\n 3054│\n 3055│        loop {\n     │...\n 3069│                },\n*3070│                KIND_UNSUBSCRIBE => {\n 3071│                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n 3072│                        send_error(&send, &msg);\n 3073│                    }\n     │...\n 3245│                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n*3246│                    Ok(id) => my_stream_subs.push(id),\n 3247│                    Err(msg) => send_error(&send, &msg),\n 3248│                },\n 3249│                KIND_NET_PRESENCE_SUBSCRIBE => {\n     │...\n 3279│        if let Some(host) = self.net.get() {\n*3280│            for id in &my_stream_subs {\n*3281│                host.detach_stream_sub(*id, &send);\n 3282│            }\n 3283│            // And presence: the liveness log + its ring persist (D4c).\n 3284│            if my_presence_sub {\n## nethost.rs\n 1980│    /// disconnect cleanup; the stream + its ring persist).\n*1981│    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n 1982│        let log = {\n 1983│            let streams = self.shared.streams.lock().unwrap();\n 1984│            streams.get(&stream_id).map(|e| Arc::clone(&e.log))"},"isError":false,"timestamp":1784271624902}}
{"type":"message","id":"3c270395","parentId":"d0c71d53","timestamp":"2026-07-17T07:00:24.917Z","message":{"role":"toolResult","toolCallId":"call_dIylKwfC8oKMYuwzMEWfOI7B|fc_038f28d3e93bbfd4016a59d308d0d48196966d1beff47b5aeb","toolName":"read","content":[{"type":"text","text":"/// epoch-lease double-mint and is forbidden.\nstruct PeerIo<'a> {\n    /// The one brain IPC handle.\n    brain: &'a mut Brain,\n    /// The established peer conn id for this step.\n    conn_id: u64,\n    /// The pump's sole durable open-op counter. Workers mint open-ops only\n    /// through [`PeerIo::open_op`] (the sync + update legs); a worker-owned\n    /// `EpochSource` over this counter would be the KH epoch-lease double-mint.\n    ops: &'a mut EpochSource,\n}\n\nimpl PeerIo<'_> {\n    /// Open a fresh stream and write one whole feed fire-and-forget (the\n    /// push-feed discipline — `op_id: None`, finished on send).\n    fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {\n        push_feed(self.brain, self.conn_id, feed)\n    }\n\n    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n    /// open identity — a restart can never reuse one), tagged with the `pump`\n    /// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,\n    /// so its ops namespace under `pump` and never collide with cli/rc/shell ops.\n    fn open_op(&mut self) -> io::Result<MintedOp> {\n        Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))\n    }\n}\n\n/// Run the pump until `stop` is set. Owns one brain IPC connection and a\n/// per-peer connection cache (an IO failure against a peer drops its cache\n/// row; the next tick redials — conns are rebuilt freely, nothing leaks\n…\nfn roster_route(\n…\n) -> Option<serde_json::Value> {\n…\n}\n\n/// Monotonic milliseconds since the process's first stamp — the `mono_ms`\n/// half of the stamped failure lines (REQ-PUMP-STAGE-TRUTH): wall time can\n/// step; this cannot, so intra-incident ordering is trustworthy.\nfn mono_ms() -> u64 {\n    use std::sync::OnceLock;\n    static BASE: OnceLock<Instant> = OnceLock::new();\n    BASE.get_or_init(Instant::now).elapsed().as_millis() as u64\n}\n\n/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,\n/// and finish it (the receiver's dispatcher classifies + applies). One\n/// stream per feed: streams are single-purpose by the demux contract.\nfn push_feed(brain: &mut Brain, conn_id: u64, feed: &[u8]) -> io::Result<()> {\n    let opened = brain.net_open_stream(conn_id, None)?;\n    brain.net_stream_send(opened.stream_id, feed, None, true)?;\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client in PUMP mode (retrying through broker\n/// startup): the carrier is split at construction with the per-call deadline\n/// armed ([`Brain::cold_start_pump`]), so the pump's reads are bounded from the\n/// first tick (REQ-HAZARD-PUMP-IPC-DEADLINE).\n// [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start_pump(broker_name, now_ms(), PUMP_PEER_IO_TIMEOUT) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}"}],"details":{"displayContent":{"text":"/// epoch-lease double-mint and is forbidden.\nstruct PeerIo<'a> {\n    /// The one brain IPC handle.\n    brain: &'a mut Brain,\n    /// The established peer conn id for this step.\n    conn_id: u64,\n    /// The pump's sole durable open-op counter. Workers mint open-ops only\n    /// through [`PeerIo::open_op`] (the sync + update legs); a worker-owned\n    /// `EpochSource` over this counter would be the KH epoch-lease double-mint.\n    ops: &'a mut EpochSource,\n}\n\nimpl PeerIo<'_> {\n    /// Open a fresh stream and write one whole feed fire-and-forget (the\n    /// push-feed discipline — `op_id: None`, finished on send).\n    fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {\n        push_feed(self.brain, self.conn_id, feed)\n    }\n\n    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n    /// open identity — a restart can never reuse one), tagged with the `pump`\n    /// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,\n    /// so its ops namespace under `pump` and never collide with cli/rc/shell ops.\n    fn open_op(&mut self) -> io::Result<MintedOp> {\n        Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))\n    }\n}\n\n/// Run the pump until `stop` is set. Owns one brain IPC connection and a\n/// per-peer connection cache (an IO failure against a peer drops its cache\n/// row; the next tick redials — conns are rebuilt freely, nothing leaks\n…\nfn roster_route(\n…\n) -> Option<serde_json::Value> {\n…\n}\n\n/// Monotonic milliseconds since the process's first stamp — the `mono_ms`\n/// half of the stamped failure lines (REQ-PUMP-STAGE-TRUTH): wall time can\n/// step; this cannot, so intra-incident ordering is trustworthy.\nfn mono_ms() -> u64 {\n    use std::sync::OnceLock;\n    static BASE: OnceLock<Instant> = OnceLock::new();\n    BASE.get_or_init(Instant::now).elapsed().as_millis() as u64\n}\n\n/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,\n/// and finish it (the receiver's dispatcher classifies + applies). One\n/// stream per feed: streams are single-purpose by the demux contract.\nfn push_feed(brain: &mut Brain, conn_id: u64, feed: &[u8]) -> io::Result<()> {\n    let opened = brain.net_open_stream(conn_id, None)?;\n    brain.net_stream_send(opened.stream_id, feed, None, true)?;\n    Ok(())\n}\n\n/// Connect a fresh brain IPC client in PUMP mode (retrying through broker\n/// startup): the carrier is split at construction with the per-call deadline\n/// armed ([`Brain::cold_start_pump`]), so the pump's reads are bounded from the\n/// first tick (REQ-HAZARD-PUMP-IPC-DEADLINE).\n// [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\nfn connect(broker_name: &str) -> io::Result<Brain> {\n    let mut last = io::Error::other(\"broker never answered\");\n    for _ in 0..300 {\n        match Brain::cold_start_pump(broker_name, now_ms(), PUMP_PEER_IO_TIMEOUT) {\n            Ok(b) => return Ok(b),\n            Err(e) => last = e,\n        }\n        thread::sleep(Duration::from_millis(10));\n    }\n    Err(last)\n}\n\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}","startLine":480,"lineNumbers":[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,null,1094,null,1097,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\mod.rs"}}},"isError":false,"timestamp":1784271624917}}
{"type":"message","id":"5662c7ea","parentId":"3c270395","timestamp":"2026-07-17T07:00:24.933Z","message":{"role":"toolResult","toolCallId":"call_jx4aDgPhMLOMEO3zU736WvFk|fc_038f28d3e93bbfd4016a59d308d094819692e4f27d0f17ffc4","toolName":"read","content":[{"type":"text","text":"\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n                // (a perch with a record); everything else is a no-op here.\n                // [impl->REQ-INST-3]\n                for id in flips {\n                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);\n                    if local.join(\"info.json\").exists() {\n                        if let Err(e) = crate::resting::daemon_rest_event(\n                            &id,\n                            crate::resting::RestEvent::AttentionShift,\n                            None,\n                        ) {\n                            eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");\n                        }\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n[Showing lines 969-1048 of 1558. Use :1049 to continue]"}],"details":{"truncation":{"content":"\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n                // (a perch with a record); everything else is a no-op here.\n                // [impl->REQ-INST-3]\n                for id in flips {\n                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);\n                    if local.join(\"info.json\").exists() {\n                        if let Err(e) = crate::resting::daemon_rest_event(\n                            &id,\n                            crate::resting::RestEvent::AttentionShift,\n                            None,\n                        ) {\n                            eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");\n                        }\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }","truncated":true,"truncatedBy":"lines","totalLines":590,"totalBytes":3666,"outputLines":80,"outputBytes":3666,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n                // (a perch with a record); everything else is a no-op here.\n                // [impl->REQ-INST-3]\n                for id in flips {\n                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);\n                    if local.join(\"info.json\").exists() {\n                        if let Err(e) = crate::resting::daemon_rest_event(\n                            &id,\n                            crate::resting::RestEvent::AttentionShift,\n                            None,\n                        ) {\n                            eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");\n                        }\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}","startLine":969,"lineNumbers":[969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1558,"totalBytes":3666,"outputLines":80,"outputBytes":3666,"shownRange":{"start":969,"end":1048},"nextOffset":1049}}},"isError":false,"timestamp":1784271624933}}
{"type":"message","id":"4a192352","parentId":"5662c7ea","timestamp":"2026-07-17T07:00:24.990Z","message":{"role":"toolResult","toolCallId":"call_opVnVgpqj7YOM21QRc5d8x0H|fc_038f28d3e93bbfd4016a59d308d0dc8196b56189e5e036c333","toolName":"read","content":[{"type":"text","text":"        loop {\n…\n            match env.kind.as_str() {\n…\n                KIND_SESSIONS => {\n…\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        // Clone the log Arcs out from under the sessions lock FIRST (ADR-0038\n        // Amendment fix 8, KH 7.12 kin): `detach_if` re-stamps info.json\n        // (`viewer_count`/`driven_by`) and emits a lifecycle record — file +\n        // conn I/O that must never run under the shared sessions table lock,\n        // or one slow perch write stalls every spawn/attach/kill dispatch.\n        // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n        let logs: Vec<Arc<Mutex<OutputLog>>> = {\n            let sessions = recover(&self.sessions);\n            my_subs\n                .iter()\n                .filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))\n                .collect()\n        };\n        for log in &logs {\n            recover_log(log).detach_if(&send);\n        }\n        // Same for net streams: the streams + their rings persist (D4b).\n\n[Showing lines 3209-3278 of 6298. Use :3279 to continue]"}],"details":{"truncation":{"content":"                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        // Clone the log Arcs out from under the sessions lock FIRST (ADR-0038\n        // Amendment fix 8, KH 7.12 kin): `detach_if` re-stamps info.json\n        // (`viewer_count`/`driven_by`) and emits a lifecycle record — file +\n        // conn I/O that must never run under the shared sessions table lock,\n        // or one slow perch write stalls every spawn/attach/kill dispatch.\n        // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n        let logs: Vec<Arc<Mutex<OutputLog>>> = {\n            let sessions = recover(&self.sessions);\n            my_subs\n                .iter()\n                .filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))\n                .collect()\n        };\n        for log in &logs {\n            recover_log(log).detach_if(&send);\n        }\n        // Same for net streams: the streams + their rings persist (D4b).","truncated":true,"truncatedBy":"lines","totalLines":3090,"totalBytes":3523,"outputLines":70,"outputBytes":3523,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"        loop {\n…\n            match env.kind.as_str() {\n…\n                KIND_SESSIONS => {\n…\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\n                    // each live session's control/viewer stamps to broker truth,\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\n                    // pre-bind); converges within one reconcile-poll window after bind.\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                        );\n                    }\n                    let reply = SessionsReply {\n                        sessions: snaps\n                            .into_iter()\n                            .map(|s| SessionInfo {\n                                session_id: s.id,\n                                endpoint: s.endpoint,\n                                resume_seq: s.resume_seq,\n                                controller_by: s.controller_by,\n                            })\n                            .collect(),\n                    };\n                    let frame = Envelope::new(\n                        KIND_SESSIONS_REPLY,\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\n                    );\n                    send_frame(&send, &frame);\n                }\n                KIND_ENDPOINT_INPUT => {\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\n                    Ok(id) => my_stream_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_PRESENCE_SUBSCRIBE => {\n                    match self.dispatch_net_presence_subscribe(env, &send) {\n                        Ok(()) => my_presence_sub = true,\n                        Err(msg) => send_error(&send, &msg),\n                    }\n                }\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\n                // An unknown kind is tolerated (forward-compat): report, don't die.\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\n            }\n        }\n\n        // Brain gone: detach as subscriber but leave the sessions running.\n        // Clone the log Arcs out from under the sessions lock FIRST (ADR-0038\n        // Amendment fix 8, KH 7.12 kin): `detach_if` re-stamps info.json\n        // (`viewer_count`/`driven_by`) and emits a lifecycle record — file +\n        // conn I/O that must never run under the shared sessions table lock,\n        // or one slow perch write stalls every spawn/attach/kill dispatch.\n        // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n        let logs: Vec<Arc<Mutex<OutputLog>>> = {\n            let sessions = recover(&self.sessions);\n            my_subs\n                .iter()\n                .filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))\n                .collect()\n        };\n        for log in &logs {\n            recover_log(log).detach_if(&send);\n        }\n        // Same for net streams: the streams + their rings persist (D4b).","startLine":3055,"lineNumbers":[3055,null,3061,null,3165,null,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6298,"totalBytes":3523,"outputLines":70,"outputBytes":3523,"shownRange":{"start":3209,"end":3278},"nextOffset":3279}}},"isError":false,"timestamp":1784271624990}}
{"type":"message","id":"dc02ae28","parentId":"4a192352","timestamp":"2026-07-17T07:00:45.097Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing Brain subscription handling and replay flow**\n\n**Investigating potential deadlocks in registry locking**"},{"type":"toolCall","id":"call_mUkbZ6mPR3zuWvqCWwRVCZEk|fc_038f28d3e93bbfd4016a59d31c99b4819693f62ccec2eb3894","name":"grep","arguments":{"i":"Locating registry apply locks","pattern":"fn apply_feed_flips|pub fn apply_feed_flips|fn apply_node_labels|pub fn apply_node_labels|write_snapshots|regs.lock|epoch.lock|heard.lock","path":"crates/spt-daemon/src/registryhost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_aDilQCCfbsulc7Qi7TXjDHa4|fc_038f28d3e93bbfd4016a59d31c99d48196b37099ce2a8a6e0a","name":"read","arguments":{"i":"Reading registry apply locking","path":"crates/spt-daemon/src/registryhost.rs","selector":"480-790"}},{"type":"toolCall","id":"call_HrEqPiBo3ChW0tgJRvCE9ZFT|fc_038f28d3e93bbfd4016a59d31c99d881968766210332b122fa","name":"read","arguments":{"i":"Reading advertisement lock scope","path":"crates/spt-daemon/src/registryhost.rs","selector":"400-620"}},{"type":"toolCall","id":"call_VU5vjjMdZpn07pcuT7NkRkKX|fc_038f28d3e93bbfd4016a59d31c99e081968ace37435fed9d5a","name":"read","arguments":{"i":"Reading stream detach behavior","path":"crates/spt-daemon/src/nethost.rs","selector":"570-590,1975-1995"}},{"type":"toolCall","id":"call_U7dNLMm39oIX0IWotU9Dt3qn|fc_038f28d3e93bbfd4016a59d31c99e481969b802eea27ffc8b4","name":"grep","arguments":{"i":"Tracing brain subscribe call","pattern":"pub fn net_stream_subscribe|fn net_stream_subscribe","path":"crates/spt-daemon/src/brain.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6974,"output":624,"cacheRead":222720,"cacheWrite":0,"totalTokens":230318,"reasoningTokens":338,"cost":{"input":0.034870000000000005,"output":0.01872,"cacheRead":0.11136,"cacheWrite":0,"total":0.16495}},"stopReason":"toolUse","timestamp":1784271625045,"responseId":"resp_038f28d3e93bbfd4016a59d30995208196861c989cd68583b2","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d30d02b881968f5cfb72fe4acdc5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdMcFZ0sTD3MB9NwQFRJ9sWrbd3LPTSODZicKhnfA_JJqs3LNSsZxXTArn3IAzgIdnCeGrQy8NrQCFLu6gzHe-06DNLD4dWu1zMInrTeD_bd9JtmVpretR1gRRtNeI-Fxdd0UuwY3jQSpnn-qgbBB8wEkyd9TFIlC6kKp1AdtAwVMSI9BR20-eke0LEA1US2G2nt-azBJxQ0KQQqSXsE-elnRF1YHXM8J5E7Ji4iheHhTC8eBWg3c_HVUxQiL0F6Z2qahbVrte3nS7ELmvtlOwdj17ewvSwewhCwUFOJhO0xJ3UmWS87MySf5CdlCtWHiv4-BpN-wwGujiAuhHPF-0DzKHjs2tfQZ_s8PisyAD_QuupjXdq-fh86jPZUxrdYic34R2ndBhlgpPjcM0z5mJqcP-SCROwSuZQuyFzZmbyjVzKI4Mqcwt3nwc56k7y2V81NWUNkLCIT3b2zVOyrjDuWf0PAjRmoaVjGZMyrUGFHldJ26fdJpreMt6rK90qt5Ex-SU3b-__QTJuDIoCwPTa6ATYdSddc9Qbqm_EyA-VhHseyM9JW-Dj__Dv1M1DI3KQDLob3rGUTNBVivYNFa-1joTdI2wx6nxutgsOLgbS6eNrCPeD9CfVLscZEaxCFM1_aLTd2-u_rc9wICa6hrz4si0DlE0_bMZDpxLi9dVLeOrq9TJbghUpj4k_dlU9iRxLnrbRhGLq9l2ZcRb_ZQsiEf2Xagh2MnkJIkCjtper-V30zzfZAOjrpBntfm9sPpornWrai6QqOmAwQ1oFPsfV7xBHrObX1CMNVl6J7KWPtcBq1t-plkgFBmvaYufnGijhihNtEjCBwyckf17nzML8F4Fx747nvs5sR1A4LBrsCOq-e_gNemDAqSr_9UKrNWEauyBiCKklK9RoK9p8Mr6ECIDN-yBtU7B_WPkvumsMxCs008ZpveVtQDO-m6SV0SPZOeiM2uOkn9cNR9i8S_M4flaZ3JZ0w6Ef3KL4yxUsI2qx5juJlCx3nEwYmGaYsd3oeT2OZqkah4mrRc-XjTPx-66tbBWL0W-4lo9cIDOHi9IY352UrstG-iNpjcGxPqD22YVo80wYNvjN35yTe-bE-F98kaSFfkeKEAG07DexOXt8C2i3TggcVHgL-cdPRQiMwdi7I02e_mnpVpsBEy3dbha_XwJdwJodidkLZ-qYEqMl6CrfNi6IoTDOZsKK29ZwJFERzFKZ72UcfyPfTjPcSx_3nIYMkcihKsBImURBqKnxRZ5aFMH0mr5Wv6RC6nzV1TeaWLCeYAbJn17g3Xc3QWi_opfssiwlvgj-cWCKcJTZQdW6Be66ysiudV2DE3BEh-23-B6KEub5RlI6BGJtZzhsAysGNwH4di_5NP0WVD60pPCVMs1x_L2kfkv-8LLOza9zb8hUolIhzHsNLBD8mdWrnNqgcErg_XINMtnoAx0jaQtAGUvZBcTLRtlSs4DqODab3mVslMAeybeHkDtQofpcNXZJ_4-TzYjGU8239H-j4t5-XiUPD-cqA11MoCPUP0v8tye-65N80pqF6IOUmF5ytGlLzAB3K_5rn_LY7ujAxG2GAL5zyJKnVgKL8HgEAqb4sJnm_IFPjys7VlQz9raCw6F6N30tGGrOJUKxAZc81hBVBB6KyFUcenI9JstFKHkt__8RfwMc3oU-RTVE_SkqbpwmP8ChlDGZShPtRiUVJ_qzoPgMJOC-gqe2O-b3QAZRASOYrZlspGZE3kcEFQA3zcnxG-WLRx-ji4Vbd1wso8eHrFkRYbbE-1TQny63xrKZwMB1TygdERBbgOSfY-n7SrNTq-ZlNG-ReMsNwXf2OZ8LBdOa12rILNNjq83LhffwZ1Zj6QSd505_Iwoj8FhD5sSLv-ToIke1cmTymzxnxDBe9Zv1cTGHWJ3o73svmvXt-hoeeN83asck48BDMj6-m8oQS_ODosm1MxLN3rQLTvG1Sml-MixfvoGTvGLV3hy0ry54LQmVeODTrR1bFi6iW9qWBAdcACejC8G06Md70SGuGqnj0YZgqEkBZufkA4tT41LBZ1I7a5EJvb9M-ovXjuAHtlIhbu6m6B4gzbWHBBvf1bB1k3_lObwASXhV8x8CcSI_c-0F1JdqXs5xKmlRMVcEKMaCUNPuiEzz6aQKoG2JmPGEHlT0H9amAO9caRFCHu8xlNgJqSk5G7g1jwJ_kE4O2TkVDWEObKYQDRCHfQzIqKkn_zQ6t12Tej3h0Dg6ApX2LoidQw1uFHngGek7RBJavcI3OInTyqoU_VnRx1z4dAm2eNWjmy4yNzbVt3oLyl0m9cHgxSBUvTv7d8P6jTUST2QBmKRAqB0IgQM1JJyg2TJ7IPnI0SpwLfqml2nMFHxX385_YW2EMBXPIpE6BnIUC-hlEBap85UTznHtF0XBjmfWqJiXV0hjfbPseyPLFwVeZxWw1NWK6JZRf6TP21y2TxMsnVA0KD3WPnmN_YViMv-P53K2-YXOTKc10hzIN5yEf4evRakHilkeLpEnk6-Q-p3XZQVX9cMclfQUUyva9BsJgOmhFLNLGfcs6m9ZbbLgrCub3sOZOcZkgrtUDFnkfRlGNM4cfjTncojvbM1E54QQjeegijwy8GBvdFdPayGzK2cBcZ02ns1KmBTMbUaGx9qd056aXXArqYfYlGOTNGRCsUngoYZK1GiD1meUt2sMISp4AuR2FXv9b5YE94NFwibIKvuVaNo9-NIZ5uoAY6CJs_6Cqao1Of5ZDPk0u_6vggvLrogWEIFbWEyA5qeaGjy0R3WWHMPN518hNKEoRrB2uJI0dnBcF3_1-gFgOLBHBw1L4MZ0aH2UQSRpBX22jzjUQZCs3bO8BmULOPtFkws8o1GaOYIJEcrqeht2gMEvb4F70tRFRX6glvOGS9hh_f_uLEHuFaahWiWZ_KqfkzN7MVKoaEGA-HOVmtWBtdW_hImtpw-IQVaeoTox72GZRc8jywVaCDeHmlDltIyuxQFZFQVJe31HDYw6NX-HKa8TxMRZ8Ghw6Yta3zZhWJ04Yd3owmvAY8kuB6pK_TLw2SfHKS2_eIYvbKS5VU9ca2wyZoZDGz8-1e2crolTO_pBNIF-G0u_32wCLYlZrGBDA1EaxACFBPdVgfZQE8DEij_X2URzywAEYNUZk_F2xEeBQMfR4CTp6cIWHjow=","summary":[{"type":"summary_text","text":"**Analyzing Brain subscription handling and replay flow**"},{"type":"summary_text","text":"**Investigating potential deadlocks in registry locking**"}]},{"id":"fc_038f28d3e93bbfd4016a59d31c99b4819693f62ccec2eb3894","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating registry apply locks\",\"pattern\":\"fn apply_feed_flips|pub fn apply_feed_flips|fn apply_node_labels|pub fn apply_node_labels|write_snapshots|regs.lock|epoch.lock|heard.lock\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_mUkbZ6mPR3zuWvqCWwRVCZEk","name":"grep"},{"id":"fc_038f28d3e93bbfd4016a59d31c99d48196b37099ce2a8a6e0a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry apply locking\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"480-790\"}","call_id":"call_aDilQCCfbsulc7Qi7TXjDHa4","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d31c99d881968766210332b122fa","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading advertisement lock scope\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"400-620\"}","call_id":"call_HrEqPiBo3ChW0tgJRvCE9ZFT","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d31c99e081968ace37435fed9d5a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading stream detach behavior\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"570-590,1975-1995\"}","call_id":"call_VU5vjjMdZpn07pcuT7NkRkKX","name":"read"},{"id":"fc_038f28d3e93bbfd4016a59d31c99e481969b802eea27ffc8b4","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing brain subscribe call\",\"pattern\":\"pub fn net_stream_subscribe|fn net_stream_subscribe\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_U7dNLMm39oIX0IWotU9Dt3qn","name":"grep"}]},"duration":20051.189199998975,"ttft":3980.151000007987,"contextSnapshot":{"promptTokens":229694,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mUkbZ6mPR3zuWvqCWwRVCZEk|fc_038f28d3e93bbfd4016a59d31c99b4819693f62ccec2eb3894","toolName":"grep","startedAt":"2026-07-17T07:00:45.098Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Locating registry apply locks"},"id":"d969ada5","parentId":"dc02ae28","timestamp":"2026-07-17T07:00:45.098Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_aDilQCCfbsulc7Qi7TXjDHa4|fc_038f28d3e93bbfd4016a59d31c99d48196b37099ce2a8a6e0a","toolName":"read","startedAt":"2026-07-17T07:00:45.098Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading registry apply locking"},"id":"e48c37e0","parentId":"d969ada5","timestamp":"2026-07-17T07:00:45.098Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HrEqPiBo3ChW0tgJRvCE9ZFT|fc_038f28d3e93bbfd4016a59d31c99d881968766210332b122fa","toolName":"read","startedAt":"2026-07-17T07:00:45.098Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading advertisement lock scope"},"id":"00383e8d","parentId":"e48c37e0","timestamp":"2026-07-17T07:00:45.098Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VU5vjjMdZpn07pcuT7NkRkKX|fc_038f28d3e93bbfd4016a59d31c99e081968ace37435fed9d5a","toolName":"read","startedAt":"2026-07-17T07:00:45.098Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading stream detach behavior"},"id":"518255fa","parentId":"00383e8d","timestamp":"2026-07-17T07:00:45.098Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_U7dNLMm39oIX0IWotU9Dt3qn|fc_038f28d3e93bbfd4016a59d31c99e481969b802eea27ffc8b4","toolName":"grep","startedAt":"2026-07-17T07:00:45.098Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Tracing brain subscribe call"},"id":"25c8c28f","parentId":"518255fa","timestamp":"2026-07-17T07:00:45.098Z"}
{"type":"message","id":"5ef9a854","parentId":"25c8c28f","timestamp":"2026-07-17T07:00:45.103Z","message":{"role":"toolResult","toolCallId":"call_U7dNLMm39oIX0IWotU9Dt3qn|fc_038f28d3e93bbfd4016a59d31c99e481969b802eea27ffc8b4","toolName":"grep","content":[{"type":"text","text":" 1589|    // [impl->REQ-DAEMON-2]\n*1590|    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n 1591|        self.net_cursors.insert(stream_id, from_seq);\n 1592|        self.send(\n 1593|            KIND_NET_STREAM_SUBSCRIBE,"}],"details":{"scopePath":"crates/spt-daemon/src/brain.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":1,"fileCount":1,"files":["crates/spt-daemon/src/brain.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brain.rs","count":1}],"truncated":false,"displayContent":" 1589│    // [impl->REQ-DAEMON-2]\n*1590│    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n 1591│        self.net_cursors.insert(stream_id, from_seq);\n 1592│        self.send(\n 1593│            KIND_NET_STREAM_SUBSCRIBE,"},"isError":false,"timestamp":1784271645103}}
{"type":"message","id":"acb43ca8","parentId":"5ef9a854","timestamp":"2026-07-17T07:00:45.120Z","message":{"role":"toolResult","toolCallId":"call_VU5vjjMdZpn07pcuT7NkRkKX|fc_038f28d3e93bbfd4016a59d31c99e081968ace37435fed9d5a","toolName":"read","content":[{"type":"text","text":"impl StreamLog {\n…\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\n…\n        self.subscriber = Some(SubscriberSeat::install(\n            sub,\n            replay,\n            Some(Arc::clone(&self.lease)),\n        ));\n        Ok(replay_len)\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if cur.is(sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A bounded, sequenced log of connection liveness events — the broker-owned\n/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline\n…\n    pub(crate) fn subscribe_stream(\n…\n    ) -> io::Result<()> {\n…\n        log.lock().unwrap().begin_attach(Arc::clone(&sub), from_seq)?;\n        Ok(())\n    }\n\n    /// Detach `sub` from a stream iff it is the current subscriber (brain\n    /// disconnect cleanup; the stream + its ring persist).\n    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n        };\n        if let Some(log) = log {\n            log.lock().unwrap().detach_if(sub);\n        }\n    }\n\n    /// Attach `sub` as the presence subscriber, replaying liveness events from\n    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.\n    // [impl->REQ-EP-4]\n    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n        // Same atomic install-with-replay discipline as subscribe_stream\n…\n    }"}],"details":{"displayContent":{"text":"impl StreamLog {\n…\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\n…\n        self.subscriber = Some(SubscriberSeat::install(\n            sub,\n            replay,\n            Some(Arc::clone(&self.lease)),\n        ));\n        Ok(replay_len)\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n    /// not unsubscribe a newer brain).\n    fn detach_if(&mut self, sub: &SharedSend) {\n        if let Some(cur) = &self.subscriber {\n            if cur.is(sub) {\n                self.subscriber = None;\n            }\n        }\n    }\n}\n\n/// A bounded, sequenced log of connection liveness events — the broker-owned\n/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline\n…\n    pub(crate) fn subscribe_stream(\n…\n    ) -> io::Result<()> {\n…\n        log.lock().unwrap().begin_attach(Arc::clone(&sub), from_seq)?;\n        Ok(())\n    }\n\n    /// Detach `sub` from a stream iff it is the current subscriber (brain\n    /// disconnect cleanup; the stream + its ring persist).\n    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {\n        let log = {\n            let streams = self.shared.streams.lock().unwrap();\n            streams.get(&stream_id).map(|e| Arc::clone(&e.log))\n        };\n        if let Some(log) = log {\n            log.lock().unwrap().detach_if(sub);\n        }\n    }\n\n    /// Attach `sub` as the presence subscriber, replaying liveness events from\n    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.\n    // [impl->REQ-EP-4]\n    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {\n        // Same atomic install-with-replay discipline as subscribe_stream\n…\n    }","startLine":374,"lineNumbers":[374,null,518,null,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,null,1950,null,1955,null,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,null,2007]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"}}},"isError":false,"timestamp":1784271645120}}
{"type":"message","id":"714591fa","parentId":"acb43ca8","timestamp":"2026-07-17T07:00:45.125Z","message":{"role":"toolResult","toolCallId":"call_mUkbZ6mPR3zuWvqCWwRVCZEk|fc_038f28d3e93bbfd4016a59d31c99b4819693f62ccec2eb3894","toolName":"grep","content":[{"type":"text","text":" 207|    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {\n*208|        f(&mut self.epoch.lock().unwrap())\n 209|    }\n 210|\n 211|    /// Apply one inbound feed under the gate. `origin_node` MUST be the\n...\n 240|    // [impl->REQ-INST-3]\n*241|    pub fn apply_feed_flips(\n 242|        &self,\n 243|        origin_node: &str,\n 244|        updates: &[RegistryUpdate],\n...\n 246|    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {\n*247|        let mut regs = self.regs.lock().unwrap();\n 248|        let mut flips: Vec<String> = Vec::new();\n 249|        let mut admitted_any = false;\n 250|        let verdicts = updates\n...\n 294|        }\n*295|        self.write_snapshots();\n 296|        (verdicts, flips)\n 297|    }\n 298|\n...\n 306|    // [impl->REQ-SUBNET-3]\n*307|    pub fn apply_node_labels(\n 308|        &self,\n 309|        origin_node: &str,\n 310|        updates: &[NodeLabelUpdate],\n...\n 312|    ) -> Vec<RegistryApplyVerdict> {\n*313|        let mut regs = self.regs.lock().unwrap();\n 314|        let mut admitted_any = false;\n 315|        let verdicts = updates\n 316|            .iter()\n...\n 341|        }\n*342|        self.write_snapshots();\n 343|        verdicts\n 344|    }\n 345|\n...\n 405|        let mut out = Vec::new();\n*406|        let mut regs = self.regs.lock().unwrap();\n*407|        let mut epoch = self.epoch.lock().unwrap();\n 408|\n 409|        // The set of self perches that still EXIST on disk this round — the\n 410|        // ghost-heal predicate below (a LOCAL registry row whose id is absent\n...\n 631|        drop(regs);\n*632|        self.write_snapshots();\n 633|        out\n 634|    }\n 635|\n...\n 657|        let ttl_ms = ttl.as_millis() as u64;\n*658|        let heard = self.heard.lock().unwrap();\n 659|        let silent = |node: &str| {\n 660|            node != self.node_hex\n 661|                && heard\n...\n 664|        };\n*665|        let mut regs = self.regs.lock().unwrap();\n 666|        let mut evicted = 0usize;\n 667|        for (subnet, reg) in regs.iter_mut() {\n 668|            let n = reg.evict_nodes(silent);\n...\n 676|        if evicted > 0 {\n*677|            self.write_snapshots();\n 678|        }\n 679|        evicted\n 680|    }\n...\n 701|        let grace_ms = ttl.as_millis() as u64;\n*702|        let mut regs = self.regs.lock().unwrap();\n 703|        let mut evicted = 0usize;\n 704|        for (subnet, reg) in regs.iter_mut() {\n 705|            let n = reg.evict_aged_offline(now_ms, grace_ms, &self.node_hex);\n...\n 712|        if evicted > 0 {\n*713|            self.write_snapshots();\n 714|        }\n 715|        evicted\n 716|    }\n...\n 730|    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\n*731|        self.regs.lock().unwrap().get(subnet).cloned()\n 732|    }\n 733|\n 734|    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\n...\n 737|    /// that triggered it (readers tolerate staleness by contract).\n*738|    fn write_snapshots(&self) {\n*739|        let regs = self.regs.lock().unwrap();\n 740|        if regs.is_empty() {\n 741|            return;\n 742|        }\n...\n 755|        // a subnet (even one literally named \"heard\").\n*756|        let heard = self.heard.lock().unwrap();\n 757|        if !heard.is_empty() {\n 758|            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\n 759|                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\n...\n 802|        {\n*803|            let mut heard = self.heard.lock().unwrap();\n 804|            for hex in &dead {\n 805|                heard.remove(hex);\n 806|            }\n 807|        }\n*808|        let mut regs = self.regs.lock().unwrap();\n 809|        let mut evicted = 0usize;\n 810|        for (subnet, reg) in regs.iter_mut() {\n 811|            let n = reg.evict_nodes(|node| dead.contains(node));\n...\n 818|        if evicted > 0 {\n*819|            self.write_snapshots();\n 820|        }\n 821|        evicted\n 822|    }\n...\n 1559|    #[test]\n*1560|    fn apply_feed_flips_only_on_observed_transition_to_active() {\n 1561|        let dir = tempfile::tempdir().unwrap();\n 1562|        let h = host(dir.path()); // node_hex = \"aa11\"\n 1563|        let p = policy(\"home\", \"bb22\");\n...\n 1657|    #[test]\n*1658|    fn apply_node_labels_gates_and_merges() {\n 1659|        let dir = tempfile::tempdir().unwrap();\n 1660|        let h = host(dir.path());\n 1661|        let p = policy(\"home\", \"bb22\"); // member of \"home\", trusts bb22 there\n...\n 1919|            let rows = spt_net::net::registry::resource_projection(\n*1920|                &h.regs.lock().unwrap()[\"adv\"],\n 1921|                |_| false,\n 1922|            );\n 1923|            assert!("}],"details":{"scopePath":"crates/spt-daemon/src/registryhost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":25,"fileCount":1,"files":["crates/spt-daemon/src/registryhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/registryhost.rs","count":25}],"truncated":false,"displayContent":"  207│    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {\n *208│        f(&mut self.epoch.lock().unwrap())\n  209│    }\n  210│\n  211│    /// Apply one inbound feed under the gate. `origin_node` MUST be the\n     │...\n  240│    // [impl->REQ-INST-3]\n *241│    pub fn apply_feed_flips(\n  242│        &self,\n  243│        origin_node: &str,\n  244│        updates: &[RegistryUpdate],\n     │...\n  246│    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {\n *247│        let mut regs = self.regs.lock().unwrap();\n  248│        let mut flips: Vec<String> = Vec::new();\n  249│        let mut admitted_any = false;\n  250│        let verdicts = updates\n     │...\n  294│        }\n *295│        self.write_snapshots();\n  296│        (verdicts, flips)\n  297│    }\n  298│\n     │...\n  306│    // [impl->REQ-SUBNET-3]\n *307│    pub fn apply_node_labels(\n  308│        &self,\n  309│        origin_node: &str,\n  310│        updates: &[NodeLabelUpdate],\n     │...\n  312│    ) -> Vec<RegistryApplyVerdict> {\n *313│        let mut regs = self.regs.lock().unwrap();\n  314│        let mut admitted_any = false;\n  315│        let verdicts = updates\n  316│            .iter()\n     │...\n  341│        }\n *342│        self.write_snapshots();\n  343│        verdicts\n  344│    }\n  345│\n     │...\n  405│        let mut out = Vec::new();\n *406│        let mut regs = self.regs.lock().unwrap();\n *407│        let mut epoch = self.epoch.lock().unwrap();\n  408│\n  409│        // The set of self perches that still EXIST on disk this round — the\n  410│        // ghost-heal predicate below (a LOCAL registry row whose id is absent\n     │...\n  631│        drop(regs);\n *632│        self.write_snapshots();\n  633│        out\n  634│    }\n  635│\n     │...\n  657│        let ttl_ms = ttl.as_millis() as u64;\n *658│        let heard = self.heard.lock().unwrap();\n  659│        let silent = |node: &str| {\n  660│            node != self.node_hex\n  661│                && heard\n     │...\n  664│        };\n *665│        let mut regs = self.regs.lock().unwrap();\n  666│        let mut evicted = 0usize;\n  667│        for (subnet, reg) in regs.iter_mut() {\n  668│            let n = reg.evict_nodes(silent);\n     │...\n  676│        if evicted > 0 {\n *677│            self.write_snapshots();\n  678│        }\n  679│        evicted\n  680│    }\n     │...\n  701│        let grace_ms = ttl.as_millis() as u64;\n *702│        let mut regs = self.regs.lock().unwrap();\n  703│        let mut evicted = 0usize;\n  704│        for (subnet, reg) in regs.iter_mut() {\n  705│            let n = reg.evict_aged_offline(now_ms, grace_ms, &self.node_hex);\n     │...\n  712│        if evicted > 0 {\n *713│            self.write_snapshots();\n  714│        }\n  715│        evicted\n  716│    }\n     │...\n  730│    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\n *731│        self.regs.lock().unwrap().get(subnet).cloned()\n  732│    }\n  733│\n  734│    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\n     │...\n  737│    /// that triggered it (readers tolerate staleness by contract).\n *738│    fn write_snapshots(&self) {\n *739│        let regs = self.regs.lock().unwrap();\n  740│        if regs.is_empty() {\n  741│            return;\n  742│        }\n     │...\n  755│        // a subnet (even one literally named \"heard\").\n *756│        let heard = self.heard.lock().unwrap();\n  757│        if !heard.is_empty() {\n  758│            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\n  759│                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\n     │...\n  802│        {\n *803│            let mut heard = self.heard.lock().unwrap();\n  804│            for hex in &dead {\n  805│                heard.remove(hex);\n  806│            }\n  807│        }\n *808│        let mut regs = self.regs.lock().unwrap();\n  809│        let mut evicted = 0usize;\n  810│        for (subnet, reg) in regs.iter_mut() {\n  811│            let n = reg.evict_nodes(|node| dead.contains(node));\n     │...\n  818│        if evicted > 0 {\n *819│            self.write_snapshots();\n  820│        }\n  821│        evicted\n  822│    }\n     │...\n 1559│    #[test]\n*1560│    fn apply_feed_flips_only_on_observed_transition_to_active() {\n 1561│        let dir = tempfile::tempdir().unwrap();\n 1562│        let h = host(dir.path()); // node_hex = \"aa11\"\n 1563│        let p = policy(\"home\", \"bb22\");\n     │...\n 1657│    #[test]\n*1658│    fn apply_node_labels_gates_and_merges() {\n 1659│        let dir = tempfile::tempdir().unwrap();\n 1660│        let h = host(dir.path());\n 1661│        let p = policy(\"home\", \"bb22\"); // member of \"home\", trusts bb22 there\n     │...\n 1919│            let rows = spt_net::net::registry::resource_projection(\n*1920│                &h.regs.lock().unwrap()[\"adv\"],\n 1921│                |_| false,\n 1922│            );\n 1923│            assert!("},"isError":false,"timestamp":1784271645125}}
{"type":"message","id":"c60b6c09","parentId":"714591fa","timestamp":"2026-07-17T07:00:45.150Z","message":{"role":"toolResult","toolCallId":"call_aDilQCCfbsulc7Qi7TXjDHa4|fc_038f28d3e93bbfd4016a59d31c99d48196b37099ce2a8a6e0a","toolName":"read","content":[{"type":"text","text":"    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n…\n        for id in perch::list_self_perch_ids(owlery) {\n…\n            let harness_only = info.as_ref().is_some_and(|i| {\n…\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                    &id,\n                    instance.clone(),\n                ) {\n                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {\n                        subnet: sub.name.clone(),\n                        endpoint_id: id.clone(),\n                        instance,\n                    })),\n                    Ok(None) => {} // hidden in this subnet — not advertised\n                    Err(collision) => {\n                        // Bare-id collision (another node owns the name in\n                        // this subnet): loud skip — resolution is the user's\n                        // rename (D3f), never a silent overwrite.\n                        eprintln!(\"REGISTRY_COLLISION:{}:{}\", sub.name, collision);\n                    }\n                }\n            }\n        }\n        // Ghost-heal (REQ-HAZARD-ROSTER-GHOST): a LOCAL perch that was ERASED\n        // (`api session-end <id> --erase` removes the owlery dir) is no longer in\n        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\n        // this scan only refreshes perches that still exist, so the stale row is\n        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\n        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\n        // epoch: the lease orders it, so it stops showing Active locally\n        // (resource_projection skips non-routable) AND peers converge to gone. Only\n        // still-routable local rows are touched (idempotent — an already-Offline\n        // ghost is left alone).\n        // [impl->REQ-HAZARD-ROSTER-GHOST]\n        for sub in &subnets.subnets {\n            let reg = regs.entry(sub.name.clone()).or_default();\n            let ghosts: Vec<String> = reg\n                .endpoint_ids()\n                .filter(|id| !live_ids.contains(*id))\n                .filter(|id| {\n                    reg.instances(id)\n                        .iter()\n                        .any(|i| i.node == self.node_hex && i.status.routable())\n                })\n                .map(str::to_string)\n                .collect();\n            for id in ghosts {\n                let Ok(ep) = epoch.next_epoch() else { continue };\n                let instance = Instance {\n                    node: self.node_hex.clone(),\n                    status: Status::Offline,\n                    epoch: ep,\n                    resources: None,\n                    last_active_ms: None,\n                    shell_adapters: Vec::new(),\n                    node_label: self.node_label.clone(),\n                    machine_id: self.machine_id.clone(),\n                    endpoint_type: None,\n                    // An erased ghost is gone — unbound, no controller, not a\n                    // harness seat (REQ-SUBNET-DISPLAY-PARITY); Offline is skipped\n                    // by the projection anyway, so these only round-trip the wire.\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n                    bound: false,\n                    controller_node: None,\n                    harness_only: false,\n                    adapter: None,\n                    recent_projects: Vec::new(),\n                    controlled: false,\n                };\n                reg.merge_instance(&id, instance.clone());\n                out.push(RegistryFeedRecord::Instance(RegistryUpdate {\n                    subnet: sub.name.clone(),\n                    endpoint_id: id,\n                    instance,\n                }));\n                eprintln!(\"ROSTER_GHOST_HEAL:{}: erased perch advertised offline\", sub.name);\n            }\n        }\n\n        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per\n        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node\n        // (zero Self perches, or all hidden) still names itself to peers rather\n        // than rendering a bare key-prefix. Rides the same epoch lease; merged\n        // into our own registry too so the snapshot carries it. Emitted every\n        // round regardless of endpoint count — redundant-but-robust when rows\n        // exist (the render prefers a trust/instance label anyway), the SOLE\n        // carrier when they don't.\n        // [impl->REQ-SUBNET-3]\n        if let Some(label) = self.node_label.clone() {\n            for sub in &subnets.subnets {\n                let Ok(ep) = epoch.next_epoch() else { continue };\n                let reg = regs.entry(sub.name.clone()).or_default();\n                reg.merge_node_label(&self.node_hex, label.clone(), ep);\n                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\n                    subnet: sub.name.clone(),\n                    node: self.node_hex.clone(),\n                    label: label.clone(),\n                    epoch: ep,\n                }));\n            }\n        }\n        drop(epoch);\n        drop(regs);\n        self.write_snapshots();\n        out\n    }\n\n    /// Evict every registry row authored by a **silent** peer node\n    /// (KNOWN-HAZARDS 4.10): a node not heard — no admitted inbound feed —\n    /// within `ttl` has its rows removed from every subnet registry and the\n    /// snapshots rewritten, so a dead node identity can no longer poison\n    /// bare-id resolution with phantom ambiguity. Own rows never decay (this\n    /// node authors them fresh each pump tick and is never in its own heard\n    /// map). A row whose author has NO heard stamp is treated as silent —\n    /// rows only enter via admitted feeds (which stamp) or `advertise_local`\n    /// (own rows, excluded), so an unstamped foreign row is already an\n    /// anomaly and fails toward eviction. A merely-offline peer reconverges\n    /// within one pump cadence of its return (re-advertisement from its\n    /// durable epoch). Returns the number of rows evicted.\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n    pub fn evict_silent_peers(&self, ttl: std::time::Duration) -> usize {\n        self.evict_silent_peers_at(ttl, wall_ms())\n    }\n\n    /// [`evict_silent_peers`](Self::evict_silent_peers) with an explicit\n    /// clock (the testable seam).\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n    pub fn evict_silent_peers_at(&self, ttl: std::time::Duration, now_ms: u64) -> usize {\n        let ttl_ms = ttl.as_millis() as u64;\n        let heard = self.heard.lock().unwrap();\n        let silent = |node: &str| {\n            node != self.node_hex\n                && heard\n                    .get(node)\n                    .is_none_or(|&t| now_ms.saturating_sub(t) > ttl_ms)\n        };\n        let mut regs = self.regs.lock().unwrap();\n        let mut evicted = 0usize;\n        for (subnet, reg) in regs.iter_mut() {\n            let n = reg.evict_nodes(silent);\n            if n > 0 {\n                eprintln!(\"REGISTRY_EVICT:{subnet}: {n} row(s) from silent node(s)\");\n            }\n            evicted += n;\n        }\n        drop(regs);\n        drop(heard);\n        if evicted > 0 {\n            self.write_snapshots();\n        }\n        evicted\n    }\n\n    /// Evict per-row **aged-Offline ghosts** across every subnet registry\n    /// (REQ-HAZARD-REGISTRY-GHOST-ROWS trigger (b), #2-secondary): a row that has\n    /// been non-routable (`Offline`) on this receiver beyond `ttl` is removed even\n    /// while its author node is alive — the decay [`evict_silent_peers`] (node\n    /// silence) cannot give, so a `purge`/`erase` Offline ghost (ghost-heal\n    /// re-advertises Offline once, whole-node eviction never fires) stops\n    /// accumulating unbounded on a remote viewer under churn. Own rows never decay.\n    /// Snapshots are rewritten iff a row was evicted. The per-row receiver-observed\n    /// clock is the model's self-pruning [`SubnetRegistry::evict_aged_offline`]\n    /// side-map (re-derived each call — no leak, no explicit whole-node purge).\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n    pub fn evict_aged_offline_rows(&self, ttl: std::time::Duration) -> usize {\n        self.evict_aged_offline_rows_at(ttl, wall_ms())\n    }\n\n    /// [`evict_aged_offline_rows`](Self::evict_aged_offline_rows) with an explicit\n    /// clock (the testable seam, mirroring [`evict_silent_peers_at`]).\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n    pub fn evict_aged_offline_rows_at(&self, ttl: std::time::Duration, now_ms: u64) -> usize {\n        let grace_ms = ttl.as_millis() as u64;\n        let mut regs = self.regs.lock().unwrap();\n        let mut evicted = 0usize;\n        for (subnet, reg) in regs.iter_mut() {\n            let n = reg.evict_aged_offline(now_ms, grace_ms, &self.node_hex);\n            if n > 0 {\n                eprintln!(\"REGISTRY_EVICT_OFFLINE:{subnet}: {n} aged-offline row(s)\");\n            }\n            evicted += n;\n        }\n        drop(regs);\n        if evicted > 0 {\n            self.write_snapshots();\n        }\n        evicted\n    }\n\n    /// Every instance row for `endpoint_id` in `subnet` (the reconcile\n    /// election's input; empty when unknown).\n    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {\n        self.regs\n            .lock()\n            .unwrap()\n            .get(subnet)\n            .map(|r| r.instances(endpoint_id).to_vec())\n            .unwrap_or_default()\n    }\n\n    /// A point-in-time clone of one subnet's registry (resolution input).\n    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\n        self.regs.lock().unwrap().get(subnet).cloned()\n    }\n\n    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\n    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process\n    /// readers. Best-effort: a failed snapshot write must not fail the merge\n    /// that triggered it (readers tolerate staleness by contract).\n    fn write_snapshots(&self) {\n        let regs = self.regs.lock().unwrap();\n        if regs.is_empty() {\n            return;\n        }\n        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {\n            return;\n        }\n        for (subnet, reg) in regs.iter() {\n            let Ok(json) = serde_json::to_vec_pretty(reg) else {\n                continue;\n            };\n            let _ = atomic_write(&self.snapshot_dir.join(format!(\"{subnet}.json\")), &json);\n        }\n        drop(regs);\n        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:\n        // the snapshot reader globs `<subnet>.json`, so this can never shadow\n        // a subnet (even one literally named \"heard\").\n        let heard = self.heard.lock().unwrap();\n        if !heard.is_empty() {\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\n                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\n            }\n        }\n    }\n\n    /// The snapshot path for one subnet (the CLI reader's contract).\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\n        dir.join(format!(\"{subnet}.json\"))\n    }\n\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\n    /// last admitted-feed wall ms`.\n    // [impl->REQ-SUBNET-1]\n    pub fn heard_path(dir: &Path) -> PathBuf {\n        dir.join(\"heard.meta\")\n    }\n\n    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):\n    /// a JSON array of superseded node hexes the pairing host noted —\n    /// consumed by the pump's registry tick so the IN-MEMORY rows go too\n    /// (a disk-only eviction would be resurrected by the next snapshot\n    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the\n    /// heard map.\n    pub fn repair_evict_path(dir: &Path) -> PathBuf {\n        dir.join(\"repair-evict.meta\")\n    }\n\n    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]\n    /// pattern — take is the remove): evict every noted identity's rows\n    /// from the in-memory registries + the heard map, and rewrite the\n    /// snapshots. No marker = no-op. Returns evicted row count.\n    // [impl->REQ-SUBNET-7]\n    pub fn consume_repair_evictions(&self) -> usize {\n        let path = Self::repair_evict_path(&self.snapshot_dir);\n        let Ok(raw) = std::fs::read_to_string(&path) else {\n…\n        };\n…\n    }\n\n[Showing lines 479-793 of 2238. Use :794 to continue]"}],"details":{"truncation":{"content":"            });\r\n            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\r\n            // real harness adapter + its recent projects + the EXPLICIT any-controller\r\n            // truth, gossiped so `from_resource_row` stops faking remote rows.\r\n            //  - adapter: the composite `<adapter>[:profile]` off info.json (the picker\r\n            //    used to show the resources blurb here).\r\n            //  - recent_projects: sessions.log-derived, owlery-excluded, project IDs only.\r\n            //  - controlled: the raw any-controller marker (NOT controller_node.is_some();\r\n            //    a local controller has driven_by=None so that WHO datum undercounts it).\r\n            // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n            let adapter = info.as_ref().and_then(|i| i.adapter.clone());\r\n            let controlled = info.as_ref().is_some_and(|i| i.controlled);\r\n            let recent_projects = recent_projects_for(&perch_path, owlery);\r\n            let last_active_ms = info.and_then(|i| i.last_active_ms);\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status,\r\n                    epoch: ep,\r\n                    resources: resources.clone(),\r\n                    last_active_ms,\r\n                    shell_adapters: shell_adapters.clone(),\r\n                    // The node label rides every row this node authors\r\n                    // (REQ-SUBNET-3) — set from the OS hostname at daemon\r\n                    // startup, so a rename converges by re-advertisement.\r\n                    // [impl->REQ-SUBNET-3]\r\n                    node_label: self.node_label.clone(),\r\n                    // The hashed machine id rides beside it (M8 decision\r\n                    // 13/22) — the re-pair overwrite anchor.\r\n                    // [impl->REQ-SUBNET-7]\r\n                    machine_id: self.machine_id.clone(),\r\n                    // The endpoint's own type tag (REQ-MSG-6) — read by the\r\n                    // cross-node user-msg gate, keyed by the proven node.\r\n                    // [impl->REQ-MSG-6]\r\n                    endpoint_type: endpoint_type.clone(),\r\n                    // The unified subnet-display parity datums — gossiped so a\r\n                    // remote picker renders bound/controlled/harness-only with the\r\n                    // same fidelity as the home node (REQ-SUBNET-DISPLAY-PARITY).\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound,\r\n                    controller_node: controller_node.clone(),\r\n                    harness_only,\r\n                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\r\n                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n                    adapter: adapter.clone(),\r\n                    recent_projects: recent_projects.clone(),\r\n                    controlled,\r\n                };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                match advertise_if_visible(\r\n                    reg,\r\n                    &vis,\r\n                    &sub.name,\r\n                    sub.hide_new_endpoints,\r\n                    &id,\r\n                    instance.clone(),\r\n                ) {\r\n                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                        subnet: sub.name.clone(),\r\n                        endpoint_id: id.clone(),\r\n                        instance,\r\n                    })),\r\n                    Ok(None) => {} // hidden in this subnet — not advertised\r\n                    Err(collision) => {\r\n                        // Bare-id collision (another node owns the name in\r\n                        // this subnet): loud skip — resolution is the user's\r\n                        // rename (D3f), never a silent overwrite.\r\n                        eprintln!(\"REGISTRY_COLLISION:{}:{}\", sub.name, collision);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        // Ghost-heal (REQ-HAZARD-ROSTER-GHOST): a LOCAL perch that was ERASED\r\n        // (`api session-end <id> --erase` removes the owlery dir) is no longer in\r\n        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\r\n        // this scan only refreshes perches that still exist, so the stale row is\r\n        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\r\n        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\r\n        // epoch: the lease orders it, so it stops showing Active locally\r\n        // (resource_projection skips non-routable) AND peers converge to gone. Only\r\n        // still-routable local rows are touched (idempotent — an already-Offline\r\n        // ghost is left alone).\r\n        // [impl->REQ-HAZARD-ROSTER-GHOST]\r\n        for sub in &subnets.subnets {\r\n            let reg = regs.entry(sub.name.clone()).or_default();\r\n            let ghosts: Vec<String> = reg\r\n                .endpoint_ids()\r\n                .filter(|id| !live_ids.contains(*id))\r\n                .filter(|id| {\r\n                    reg.instances(id)\r\n                        .iter()\r\n                        .any(|i| i.node == self.node_hex && i.status.routable())\r\n                })\r\n                .map(str::to_string)\r\n                .collect();\r\n            for id in ghosts {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status: Status::Offline,\r\n                    epoch: ep,\r\n                    resources: None,\r\n                    last_active_ms: None,\r\n                    shell_adapters: Vec::new(),\r\n                    node_label: self.node_label.clone(),\r\n                    machine_id: self.machine_id.clone(),\r\n                    endpoint_type: None,\r\n                    // An erased ghost is gone — unbound, no controller, not a\r\n                    // harness seat (REQ-SUBNET-DISPLAY-PARITY); Offline is skipped\r\n                    // by the projection anyway, so these only round-trip the wire.\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound: false,\r\n                    controller_node: None,\r\n                    harness_only: false,\r\n                    adapter: None,\r\n                    recent_projects: Vec::new(),\r\n                    controlled: false,\r\n                };\r\n                reg.merge_instance(&id, instance.clone());\r\n                out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                    subnet: sub.name.clone(),\r\n                    endpoint_id: id,\r\n                    instance,\r\n                }));\r\n                eprintln!(\"ROSTER_GHOST_HEAL:{}: erased perch advertised offline\", sub.name);\r\n            }\r\n        }\r\n\r\n        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per\r\n        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node\r\n        // (zero Self perches, or all hidden) still names itself to peers rather\r\n        // than rendering a bare key-prefix. Rides the same epoch lease; merged\r\n        // into our own registry too so the snapshot carries it. Emitted every\r\n        // round regardless of endpoint count — redundant-but-robust when rows\r\n        // exist (the render prefers a trust/instance label anyway), the SOLE\r\n        // carrier when they don't.\r\n        // [impl->REQ-SUBNET-3]\r\n        if let Some(label) = self.node_label.clone() {\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                reg.merge_node_label(&self.node_hex, label.clone(), ep);\r\n                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\r\n                    subnet: sub.name.clone(),\r\n                    node: self.node_hex.clone(),\r\n                    label: label.clone(),\r\n                    epoch: ep,\r\n                }));\r\n            }\r\n        }\r\n        drop(epoch);\r\n        drop(regs);\r\n        self.write_snapshots();\r\n        out\r\n    }\r\n\r\n    /// Evict every registry row authored by a **silent** peer node\r\n    /// (KNOWN-HAZARDS 4.10): a node not heard — no admitted inbound feed —\r\n    /// within `ttl` has its rows removed from every subnet registry and the\r\n    /// snapshots rewritten, so a dead node identity can no longer poison\r\n    /// bare-id resolution with phantom ambiguity. Own rows never decay (this\r\n    /// node authors them fresh each pump tick and is never in its own heard\r\n    /// map). A row whose author has NO heard stamp is treated as silent —\r\n    /// rows only enter via admitted feeds (which stamp) or `advertise_local`\r\n    /// (own rows, excluded), so an unstamped foreign row is already an\r\n    /// anomaly and fails toward eviction. A merely-offline peer reconverges\r\n    /// within one pump cadence of its return (re-advertisement from its\r\n    /// durable epoch). Returns the number of rows evicted.\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_silent_peers(&self, ttl: std::time::Duration) -> usize {\r\n        self.evict_silent_peers_at(ttl, wall_ms())\r\n    }\r\n\r\n    /// [`evict_silent_peers`](Self::evict_silent_peers) with an explicit\r\n    /// clock (the testable seam).\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_silent_peers_at(&self, ttl: std::time::Duration, now_ms: u64) -> usize {\r\n        let ttl_ms = ttl.as_millis() as u64;\r\n        let heard = self.heard.lock().unwrap();\r\n        let silent = |node: &str| {\r\n            node != self.node_hex\r\n                && heard\r\n                    .get(node)\r\n                    .is_none_or(|&t| now_ms.saturating_sub(t) > ttl_ms)\r\n        };\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut evicted = 0usize;\r\n        for (subnet, reg) in regs.iter_mut() {\r\n            let n = reg.evict_nodes(silent);\r\n            if n > 0 {\r\n                eprintln!(\"REGISTRY_EVICT:{subnet}: {n} row(s) from silent node(s)\");\r\n            }\r\n            evicted += n;\r\n        }\r\n        drop(regs);\r\n        drop(heard);\r\n        if evicted > 0 {\r\n            self.write_snapshots();\r\n        }\r\n        evicted\r\n    }\r\n\r\n    /// Evict per-row **aged-Offline ghosts** across every subnet registry\r\n    /// (REQ-HAZARD-REGISTRY-GHOST-ROWS trigger (b), #2-secondary): a row that has\r\n    /// been non-routable (`Offline`) on this receiver beyond `ttl` is removed even\r\n    /// while its author node is alive — the decay [`evict_silent_peers`] (node\r\n    /// silence) cannot give, so a `purge`/`erase` Offline ghost (ghost-heal\r\n    /// re-advertises Offline once, whole-node eviction never fires) stops\r\n    /// accumulating unbounded on a remote viewer under churn. Own rows never decay.\r\n    /// Snapshots are rewritten iff a row was evicted. The per-row receiver-observed\r\n    /// clock is the model's self-pruning [`SubnetRegistry::evict_aged_offline`]\r\n    /// side-map (re-derived each call — no leak, no explicit whole-node purge).\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_aged_offline_rows(&self, ttl: std::time::Duration) -> usize {\r\n        self.evict_aged_offline_rows_at(ttl, wall_ms())\r\n    }\r\n\r\n    /// [`evict_aged_offline_rows`](Self::evict_aged_offline_rows) with an explicit\r\n    /// clock (the testable seam, mirroring [`evict_silent_peers_at`]).\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_aged_offline_rows_at(&self, ttl: std::time::Duration, now_ms: u64) -> usize {\r\n        let grace_ms = ttl.as_millis() as u64;\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut evicted = 0usize;\r\n        for (subnet, reg) in regs.iter_mut() {\r\n            let n = reg.evict_aged_offline(now_ms, grace_ms, &self.node_hex);\r\n            if n > 0 {\r\n                eprintln!(\"REGISTRY_EVICT_OFFLINE:{subnet}: {n} aged-offline row(s)\");\r\n            }\r\n            evicted += n;\r\n        }\r\n        drop(regs);\r\n        if evicted > 0 {\r\n            self.write_snapshots();\r\n        }\r\n        evicted\r\n    }\r\n\r\n    /// Every instance row for `endpoint_id` in `subnet` (the reconcile\r\n    /// election's input; empty when unknown).\r\n    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {\r\n        self.regs\r\n            .lock()\r\n            .unwrap()\r\n            .get(subnet)\r\n            .map(|r| r.instances(endpoint_id).to_vec())\r\n            .unwrap_or_default()\r\n    }\r\n\r\n    /// A point-in-time clone of one subnet's registry (resolution input).\r\n    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\r\n        self.regs.lock().unwrap().get(subnet).cloned()\r\n    }\r\n\r\n    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\r\n    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process\r\n    /// readers. Best-effort: a failed snapshot write must not fail the merge\r\n    /// that triggered it (readers tolerate staleness by contract).\r\n    fn write_snapshots(&self) {\r\n        let regs = self.regs.lock().unwrap();\r\n        if regs.is_empty() {\r\n            return;\r\n        }\r\n        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {\r\n            return;\r\n        }\r\n        for (subnet, reg) in regs.iter() {\r\n            let Ok(json) = serde_json::to_vec_pretty(reg) else {\r\n                continue;\r\n            };\r\n            let _ = atomic_write(&self.snapshot_dir.join(format!(\"{subnet}.json\")), &json);\r\n        }\r\n        drop(regs);\r\n        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:\r\n        // the snapshot reader globs `<subnet>.json`, so this can never shadow\r\n        // a subnet (even one literally named \"heard\").\r\n        let heard = self.heard.lock().unwrap();\r\n        if !heard.is_empty() {\r\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\r\n                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// The snapshot path for one subnet (the CLI reader's contract).\r\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\r\n        dir.join(format!(\"{subnet}.json\"))\r\n    }\r\n\r\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\r\n    /// last admitted-feed wall ms`.\r\n    // [impl->REQ-SUBNET-1]\r\n    pub fn heard_path(dir: &Path) -> PathBuf {\r\n        dir.join(\"heard.meta\")\r\n    }\r\n\r\n    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):\r\n    /// a JSON array of superseded node hexes the pairing host noted —\r\n    /// consumed by the pump's registry tick so the IN-MEMORY rows go too\r\n    /// (a disk-only eviction would be resurrected by the next snapshot\r\n    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the\r\n    /// heard map.\r\n    pub fn repair_evict_path(dir: &Path) -> PathBuf {\r\n        dir.join(\"repair-evict.meta\")\r\n    }\r\n\r\n    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]\r\n    /// pattern — take is the remove): evict every noted identity's rows\r\n    /// from the in-memory registries + the heard map, and rewrite the\r\n    /// snapshots. No marker = no-op. Returns evicted row count.\r\n    // [impl->REQ-SUBNET-7]\r\n    pub fn consume_repair_evictions(&self) -> usize {\r\n        let path = Self::repair_evict_path(&self.snapshot_dir);\r\n        let Ok(raw) = std::fs::read_to_string(&path) else {\r","truncated":true,"truncatedBy":"lines","totalLines":1760,"totalBytes":15630,"outputLines":315,"outputBytes":15630,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n…\n        for id in perch::list_self_perch_ids(owlery) {\n…\n            let harness_only = info.as_ref().is_some_and(|i| {\n…\n            });\r\n            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\r\n            // real harness adapter + its recent projects + the EXPLICIT any-controller\r\n            // truth, gossiped so `from_resource_row` stops faking remote rows.\r\n            //  - adapter: the composite `<adapter>[:profile]` off info.json (the picker\r\n            //    used to show the resources blurb here).\r\n            //  - recent_projects: sessions.log-derived, owlery-excluded, project IDs only.\r\n            //  - controlled: the raw any-controller marker (NOT controller_node.is_some();\r\n            //    a local controller has driven_by=None so that WHO datum undercounts it).\r\n            // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n            let adapter = info.as_ref().and_then(|i| i.adapter.clone());\r\n            let controlled = info.as_ref().is_some_and(|i| i.controlled);\r\n            let recent_projects = recent_projects_for(&perch_path, owlery);\r\n            let last_active_ms = info.and_then(|i| i.last_active_ms);\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status,\r\n                    epoch: ep,\r\n                    resources: resources.clone(),\r\n                    last_active_ms,\r\n                    shell_adapters: shell_adapters.clone(),\r\n                    // The node label rides every row this node authors\r\n                    // (REQ-SUBNET-3) — set from the OS hostname at daemon\r\n                    // startup, so a rename converges by re-advertisement.\r\n                    // [impl->REQ-SUBNET-3]\r\n                    node_label: self.node_label.clone(),\r\n                    // The hashed machine id rides beside it (M8 decision\r\n                    // 13/22) — the re-pair overwrite anchor.\r\n                    // [impl->REQ-SUBNET-7]\r\n                    machine_id: self.machine_id.clone(),\r\n                    // The endpoint's own type tag (REQ-MSG-6) — read by the\r\n                    // cross-node user-msg gate, keyed by the proven node.\r\n                    // [impl->REQ-MSG-6]\r\n                    endpoint_type: endpoint_type.clone(),\r\n                    // The unified subnet-display parity datums — gossiped so a\r\n                    // remote picker renders bound/controlled/harness-only with the\r\n                    // same fidelity as the home node (REQ-SUBNET-DISPLAY-PARITY).\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound,\r\n                    controller_node: controller_node.clone(),\r\n                    harness_only,\r\n                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\r\n                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n                    adapter: adapter.clone(),\r\n                    recent_projects: recent_projects.clone(),\r\n                    controlled,\r\n                };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                match advertise_if_visible(\r\n                    reg,\r\n                    &vis,\r\n                    &sub.name,\r\n                    sub.hide_new_endpoints,\r\n                    &id,\r\n                    instance.clone(),\r\n                ) {\r\n                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                        subnet: sub.name.clone(),\r\n                        endpoint_id: id.clone(),\r\n                        instance,\r\n                    })),\r\n                    Ok(None) => {} // hidden in this subnet — not advertised\r\n                    Err(collision) => {\r\n                        // Bare-id collision (another node owns the name in\r\n                        // this subnet): loud skip — resolution is the user's\r\n                        // rename (D3f), never a silent overwrite.\r\n                        eprintln!(\"REGISTRY_COLLISION:{}:{}\", sub.name, collision);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        // Ghost-heal (REQ-HAZARD-ROSTER-GHOST): a LOCAL perch that was ERASED\r\n        // (`api session-end <id> --erase` removes the owlery dir) is no longer in\r\n        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\r\n        // this scan only refreshes perches that still exist, so the stale row is\r\n        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\r\n        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\r\n        // epoch: the lease orders it, so it stops showing Active locally\r\n        // (resource_projection skips non-routable) AND peers converge to gone. Only\r\n        // still-routable local rows are touched (idempotent — an already-Offline\r\n        // ghost is left alone).\r\n        // [impl->REQ-HAZARD-ROSTER-GHOST]\r\n        for sub in &subnets.subnets {\r\n            let reg = regs.entry(sub.name.clone()).or_default();\r\n            let ghosts: Vec<String> = reg\r\n                .endpoint_ids()\r\n                .filter(|id| !live_ids.contains(*id))\r\n                .filter(|id| {\r\n                    reg.instances(id)\r\n                        .iter()\r\n                        .any(|i| i.node == self.node_hex && i.status.routable())\r\n                })\r\n                .map(str::to_string)\r\n                .collect();\r\n            for id in ghosts {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status: Status::Offline,\r\n                    epoch: ep,\r\n                    resources: None,\r\n                    last_active_ms: None,\r\n                    shell_adapters: Vec::new(),\r\n                    node_label: self.node_label.clone(),\r\n                    machine_id: self.machine_id.clone(),\r\n                    endpoint_type: None,\r\n                    // An erased ghost is gone — unbound, no controller, not a\r\n                    // harness seat (REQ-SUBNET-DISPLAY-PARITY); Offline is skipped\r\n                    // by the projection anyway, so these only round-trip the wire.\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound: false,\r\n                    controller_node: None,\r\n                    harness_only: false,\r\n                    adapter: None,\r\n                    recent_projects: Vec::new(),\r\n                    controlled: false,\r\n                };\r\n                reg.merge_instance(&id, instance.clone());\r\n                out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                    subnet: sub.name.clone(),\r\n                    endpoint_id: id,\r\n                    instance,\r\n                }));\r\n                eprintln!(\"ROSTER_GHOST_HEAL:{}: erased perch advertised offline\", sub.name);\r\n            }\r\n        }\r\n\r\n        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per\r\n        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node\r\n        // (zero Self perches, or all hidden) still names itself to peers rather\r\n        // than rendering a bare key-prefix. Rides the same epoch lease; merged\r\n        // into our own registry too so the snapshot carries it. Emitted every\r\n        // round regardless of endpoint count — redundant-but-robust when rows\r\n        // exist (the render prefers a trust/instance label anyway), the SOLE\r\n        // carrier when they don't.\r\n        // [impl->REQ-SUBNET-3]\r\n        if let Some(label) = self.node_label.clone() {\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                reg.merge_node_label(&self.node_hex, label.clone(), ep);\r\n                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\r\n                    subnet: sub.name.clone(),\r\n                    node: self.node_hex.clone(),\r\n                    label: label.clone(),\r\n                    epoch: ep,\r\n                }));\r\n            }\r\n        }\r\n        drop(epoch);\r\n        drop(regs);\r\n        self.write_snapshots();\r\n        out\r\n    }\r\n\r\n    /// Evict every registry row authored by a **silent** peer node\r\n    /// (KNOWN-HAZARDS 4.10): a node not heard — no admitted inbound feed —\r\n    /// within `ttl` has its rows removed from every subnet registry and the\r\n    /// snapshots rewritten, so a dead node identity can no longer poison\r\n    /// bare-id resolution with phantom ambiguity. Own rows never decay (this\r\n    /// node authors them fresh each pump tick and is never in its own heard\r\n    /// map). A row whose author has NO heard stamp is treated as silent —\r\n    /// rows only enter via admitted feeds (which stamp) or `advertise_local`\r\n    /// (own rows, excluded), so an unstamped foreign row is already an\r\n    /// anomaly and fails toward eviction. A merely-offline peer reconverges\r\n    /// within one pump cadence of its return (re-advertisement from its\r\n    /// durable epoch). Returns the number of rows evicted.\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_silent_peers(&self, ttl: std::time::Duration) -> usize {\r\n        self.evict_silent_peers_at(ttl, wall_ms())\r\n    }\r\n\r\n    /// [`evict_silent_peers`](Self::evict_silent_peers) with an explicit\r\n    /// clock (the testable seam).\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_silent_peers_at(&self, ttl: std::time::Duration, now_ms: u64) -> usize {\r\n        let ttl_ms = ttl.as_millis() as u64;\r\n        let heard = self.heard.lock().unwrap();\r\n        let silent = |node: &str| {\r\n            node != self.node_hex\r\n                && heard\r\n                    .get(node)\r\n                    .is_none_or(|&t| now_ms.saturating_sub(t) > ttl_ms)\r\n        };\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut evicted = 0usize;\r\n        for (subnet, reg) in regs.iter_mut() {\r\n            let n = reg.evict_nodes(silent);\r\n            if n > 0 {\r\n                eprintln!(\"REGISTRY_EVICT:{subnet}: {n} row(s) from silent node(s)\");\r\n            }\r\n            evicted += n;\r\n        }\r\n        drop(regs);\r\n        drop(heard);\r\n        if evicted > 0 {\r\n            self.write_snapshots();\r\n        }\r\n        evicted\r\n    }\r\n\r\n    /// Evict per-row **aged-Offline ghosts** across every subnet registry\r\n    /// (REQ-HAZARD-REGISTRY-GHOST-ROWS trigger (b), #2-secondary): a row that has\r\n    /// been non-routable (`Offline`) on this receiver beyond `ttl` is removed even\r\n    /// while its author node is alive — the decay [`evict_silent_peers`] (node\r\n    /// silence) cannot give, so a `purge`/`erase` Offline ghost (ghost-heal\r\n    /// re-advertises Offline once, whole-node eviction never fires) stops\r\n    /// accumulating unbounded on a remote viewer under churn. Own rows never decay.\r\n    /// Snapshots are rewritten iff a row was evicted. The per-row receiver-observed\r\n    /// clock is the model's self-pruning [`SubnetRegistry::evict_aged_offline`]\r\n    /// side-map (re-derived each call — no leak, no explicit whole-node purge).\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_aged_offline_rows(&self, ttl: std::time::Duration) -> usize {\r\n        self.evict_aged_offline_rows_at(ttl, wall_ms())\r\n    }\r\n\r\n    /// [`evict_aged_offline_rows`](Self::evict_aged_offline_rows) with an explicit\r\n    /// clock (the testable seam, mirroring [`evict_silent_peers_at`]).\r\n    // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n    pub fn evict_aged_offline_rows_at(&self, ttl: std::time::Duration, now_ms: u64) -> usize {\r\n        let grace_ms = ttl.as_millis() as u64;\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut evicted = 0usize;\r\n        for (subnet, reg) in regs.iter_mut() {\r\n            let n = reg.evict_aged_offline(now_ms, grace_ms, &self.node_hex);\r\n            if n > 0 {\r\n                eprintln!(\"REGISTRY_EVICT_OFFLINE:{subnet}: {n} aged-offline row(s)\");\r\n            }\r\n            evicted += n;\r\n        }\r\n        drop(regs);\r\n        if evicted > 0 {\r\n            self.write_snapshots();\r\n        }\r\n        evicted\r\n    }\r\n\r\n    /// Every instance row for `endpoint_id` in `subnet` (the reconcile\r\n    /// election's input; empty when unknown).\r\n    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {\r\n        self.regs\r\n            .lock()\r\n            .unwrap()\r\n            .get(subnet)\r\n            .map(|r| r.instances(endpoint_id).to_vec())\r\n            .unwrap_or_default()\r\n    }\r\n\r\n    /// A point-in-time clone of one subnet's registry (resolution input).\r\n    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\r\n        self.regs.lock().unwrap().get(subnet).cloned()\r\n    }\r\n\r\n    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\r\n    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process\r\n    /// readers. Best-effort: a failed snapshot write must not fail the merge\r\n    /// that triggered it (readers tolerate staleness by contract).\r\n    fn write_snapshots(&self) {\r\n        let regs = self.regs.lock().unwrap();\r\n        if regs.is_empty() {\r\n            return;\r\n        }\r\n        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {\r\n            return;\r\n        }\r\n        for (subnet, reg) in regs.iter() {\r\n            let Ok(json) = serde_json::to_vec_pretty(reg) else {\r\n                continue;\r\n            };\r\n            let _ = atomic_write(&self.snapshot_dir.join(format!(\"{subnet}.json\")), &json);\r\n        }\r\n        drop(regs);\r\n        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:\r\n        // the snapshot reader globs `<subnet>.json`, so this can never shadow\r\n        // a subnet (even one literally named \"heard\").\r\n        let heard = self.heard.lock().unwrap();\r\n        if !heard.is_empty() {\r\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\r\n                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// The snapshot path for one subnet (the CLI reader's contract).\r\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\r\n        dir.join(format!(\"{subnet}.json\"))\r\n    }\r\n\r\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\r\n    /// last admitted-feed wall ms`.\r\n    // [impl->REQ-SUBNET-1]\r\n    pub fn heard_path(dir: &Path) -> PathBuf {\r\n        dir.join(\"heard.meta\")\r\n    }\r\n\r\n    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):\r\n    /// a JSON array of superseded node hexes the pairing host noted —\r\n    /// consumed by the pump's registry tick so the IN-MEMORY rows go too\r\n    /// (a disk-only eviction would be resurrected by the next snapshot\r\n    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the\r\n    /// heard map.\r\n    pub fn repair_evict_path(dir: &Path) -> PathBuf {\r\n        dir.join(\"repair-evict.meta\")\r\n    }\r\n\r\n    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]\r\n    /// pattern — take is the remove): evict every noted identity's rows\r\n    /// from the in-memory registries + the heard map, and rewrite the\r\n    /// snapshots. No marker = no-op. Returns evicted row count.\r\n    // [impl->REQ-SUBNET-7]\r\n    pub fn consume_repair_evictions(&self) -> usize {\r\n        let path = Self::repair_evict_path(&self.snapshot_dir);\r\n        let Ok(raw) = std::fs::read_to_string(&path) else {\r\n…\n        };\n…\n    }","startLine":373,"lineNumbers":[373,null,415,null,477,null,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,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,null,795,null,822]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2238,"totalBytes":15630,"outputLines":315,"outputBytes":15630,"shownRange":{"start":479,"end":793},"nextOffset":794}}},"isError":false,"timestamp":1784271645150}}
{"type":"message","id":"77d82b1e","parentId":"c60b6c09","timestamp":"2026-07-17T07:00:45.170Z","message":{"role":"toolResult","toolCallId":"call_HrEqPiBo3ChW0tgJRvCE9ZFT|fc_038f28d3e93bbfd4016a59d31c99d881968766210332b122fa","toolName":"read","content":[{"type":"text","text":"        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())\n…\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                    &id,\n                    instance.clone(),\n                ) {\n                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {\n                        subnet: sub.name.clone(),\n                        endpoint_id: id.clone(),\n                        instance,\n                    })),\n                    Ok(None) => {} // hidden in this subnet — not advertised\n                    Err(collision) => {\n                        // Bare-id collision (another node owns the name in\n                        // this subnet): loud skip — resolution is the user's\n                        // rename (D3f), never a silent overwrite.\n                        eprintln!(\"REGISTRY_COLLISION:{}:{}\", sub.name, collision);\n                    }\n                }\n            }\n        }\n        // Ghost-heal (REQ-HAZARD-ROSTER-GHOST): a LOCAL perch that was ERASED\n        // (`api session-end <id> --erase` removes the owlery dir) is no longer in\n        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\n        // this scan only refreshes perches that still exist, so the stale row is\n        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\n        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\n        // epoch: the lease orders it, so it stops showing Active locally\n        // (resource_projection skips non-routable) AND peers converge to gone. Only\n        // still-routable local rows are touched (idempotent — an already-Offline\n        // ghost is left alone).\n        // [impl->REQ-HAZARD-ROSTER-GHOST]\n        for sub in &subnets.subnets {\n            let reg = regs.entry(sub.name.clone()).or_default();\n            let ghosts: Vec<String> = reg\n                .endpoint_ids()\n                .filter(|id| !live_ids.contains(*id))\n                .filter(|id| {\n                    reg.instances(id)\n                        .iter()\n                        .any(|i| i.node == self.node_hex && i.status.routable())\n                })\n                .map(str::to_string)\n                .collect();\n            for id in ghosts {\n                let Ok(ep) = epoch.next_epoch() else { continue };\n                let instance = Instance {\n                    node: self.node_hex.clone(),\n                    status: Status::Offline,\n                    epoch: ep,\n                    resources: None,\n                    last_active_ms: None,\n                    shell_adapters: Vec::new(),\n                    node_label: self.node_label.clone(),\n                    machine_id: self.machine_id.clone(),\n                    endpoint_type: None,\n                    // An erased ghost is gone — unbound, no controller, not a\n                    // harness seat (REQ-SUBNET-DISPLAY-PARITY); Offline is skipped\n                    // by the projection anyway, so these only round-trip the wire.\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n                    bound: false,\n                    controller_node: None,\n                    harness_only: false,\n                    adapter: None,\n                    recent_projects: Vec::new(),\n                    controlled: false,\n                };\n                reg.merge_instance(&id, instance.clone());\n                out.push(RegistryFeedRecord::Instance(RegistryUpdate {\n                    subnet: sub.name.clone(),\n                    endpoint_id: id,\n                    instance,\n                }));\n                eprintln!(\"ROSTER_GHOST_HEAL:{}: erased perch advertised offline\", sub.name);\n            }\n        }\n\n        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per\n        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node\n        // (zero Self perches, or all hidden) still names itself to peers rather\n        // than rendering a bare key-prefix. Rides the same epoch lease; merged\n        // into our own registry too so the snapshot carries it. Emitted every\n        // round regardless of endpoint count — redundant-but-robust when rows\n        // exist (the render prefers a trust/instance label anyway), the SOLE\n        // carrier when they don't.\n        // [impl->REQ-SUBNET-3]\n        if let Some(label) = self.node_label.clone() {\n            for sub in &subnets.subnets {\n                let Ok(ep) = epoch.next_epoch() else { continue };\n                let reg = regs.entry(sub.name.clone()).or_default();\n                reg.merge_node_label(&self.node_hex, label.clone(), ep);\n                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\n                    subnet: sub.name.clone(),\n…\n                }));\n            }\n        }\n\n[Showing lines 399-623 of 2238. Use :624 to continue]"}],"details":{"truncation":{"content":"                r.kind == spt_runtime::manifest::AdapterKind::Shell\r\n                    && m.shell.as_ref().and_then(|s| s.broadcast)\r\n                        == Some(spt_runtime::manifest::Broadcast::Subnet)\r\n            })\r\n            .map(|(r, _)| r.name.clone())\r\n            .collect();\r\n        let mut out = Vec::new();\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut epoch = self.epoch.lock().unwrap();\r\n\r\n        // The set of self perches that still EXIST on disk this round — the\r\n        // ghost-heal predicate below (a LOCAL registry row whose id is absent\r\n        // here was erased, REQ-HAZARD-ROSTER-GHOST).\r\n        let live_ids: std::collections::BTreeSet<String> =\r\n            perch::list_self_perch_ids(owlery).into_iter().collect();\r\n\r\n        for id in perch::list_self_perch_ids(owlery) {\r\n            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\r\n            // Advertisement follows the resting-state machine (D9-2,\r\n            // REQ-INST-3): a live perch with a resting record advertises its\r\n            // machine state (active / dormant / suspended); a recordless\r\n            // (pre-D9-2 / interim) or dead perch keeps the legacy\r\n            // liveness-derived mapping. Every advertisement stamps a fresh\r\n            // epoch below, so a transition reaches peers as an epoch-bumped\r\n            // update — the lease orders it (KH 4.8).\r\n            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]\r\n            let status = advertised_status(&perch_path);\r\n            // Endpoint-authored blurb wins; the node seed fills the gap\r\n            // (both-authored, REQ-INST-14). Rides the same epoch lease as\r\n            // every instance update — a blurb edit reaches peers as an\r\n            // ordered, epoch-bumped row.\r\n            // [impl->REQ-INST-14]\r\n            let info = spt_store::info::read_info(&perch_path);\r\n            let resources = info\r\n                .as_ref()\r\n                .and_then(|i| i.resources.clone())\r\n                .or_else(|| node_blurb.clone());\r\n            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat\r\n            // stamp (`lifecycle::touch_active` — Active seats only; a resting\r\n            // seat never stamps) rides the row like the blurb does — same\r\n            // epoch lease, same visibility gate, no new channel. Subnet-wide\r\n            // most-recently-active is a projection of these rows (D6b).\r\n            // [impl->REQ-PRES-1]\r\n            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the\r\n            // info.json `state` IS the bound endpoint type\r\n            // (`EndpointType::as_tag` — `\"gateway\"` for a Gateway surface).\r\n            // A node advertises ONLY its own endpoints' types; the cross-node\r\n            // user-msg gate reads this at the receive funnel (T3). A recordless\r\n            // perch advertises no type → the funnel fail-closes to a re-stamp.\r\n            // [impl->REQ-MSG-6]\r\n            let endpoint_type = info.as_ref().map(|i| i.state.clone());\r\n            // The unified subnet-display parity datums (REQ-SUBNET-DISPLAY-PARITY):\r\n            // the SAME local facts the picker's `local_rows` reads off info.json,\r\n            // now GOSSIPED so a remote viewer renders bound/controlled/harness-only\r\n            // identically instead of reducing every remote row to plain green.\r\n            //  - bound: a bound live broker session (`is_perch_alive` is bound-gated,\r\n            //    so an UNBOUND perch reads false — exactly the fill duality the\r\n            //    picker keys on).\r\n            //  - controller_node: the broker-owned `driven_by` (the driving node),\r\n            //    if any → the remote blue \"controlled\" + `controlled by <node>` pin.\r\n            //  - harness_only: a LiveAgent with no broker PTY seat — mirrors the\r\n            //    picker's `controllable != Some(true)` for a `live_agent` (a legacy\r\n            //    None self-corrects on the next bind, so it advertises harness-only\r\n            //    until then, matching the local amber).\r\n            // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n            let bound = is_perch_alive(&perch_path);\r\n            // #3 (REQ-GOSSIP-CONTROLLED-ANY): gossip ANY controller, not just a\r\n            // remote one. `driven_by` is remote-only (KH 7.15); a LOCAL controller\r\n            // sets the separate `controlled` flag instead, and its driving node IS\r\n            // this node — so advertise `self.node_hex`. A remote controller keeps\r\n            // its own origin. This is what lets a remote viewer see a\r\n            // locally-controlled endpoint as CONTROLLED, not \"ready to control\".\r\n            // [impl->REQ-GOSSIP-CONTROLLED-ANY]\r\n            let controller_node = gossip_controller_node(\r\n                info.as_ref().and_then(|i| i.driven_by.as_deref()),\r\n                info.as_ref().is_some_and(|i| i.controlled),\r\n                &self.node_hex,\r\n            );\r\n            let harness_only = info.as_ref().is_some_and(|i| {\r\n                i.state == \"live_agent\" && i.controllable != Some(true)\r\n            });\r\n            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\r\n            // real harness adapter + its recent projects + the EXPLICIT any-controller\r\n            // truth, gossiped so `from_resource_row` stops faking remote rows.\r\n            //  - adapter: the composite `<adapter>[:profile]` off info.json (the picker\r\n            //    used to show the resources blurb here).\r\n            //  - recent_projects: sessions.log-derived, owlery-excluded, project IDs only.\r\n            //  - controlled: the raw any-controller marker (NOT controller_node.is_some();\r\n            //    a local controller has driven_by=None so that WHO datum undercounts it).\r\n            // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n            let adapter = info.as_ref().and_then(|i| i.adapter.clone());\r\n            let controlled = info.as_ref().is_some_and(|i| i.controlled);\r\n            let recent_projects = recent_projects_for(&perch_path, owlery);\r\n            let last_active_ms = info.and_then(|i| i.last_active_ms);\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status,\r\n                    epoch: ep,\r\n                    resources: resources.clone(),\r\n                    last_active_ms,\r\n                    shell_adapters: shell_adapters.clone(),\r\n                    // The node label rides every row this node authors\r\n                    // (REQ-SUBNET-3) — set from the OS hostname at daemon\r\n                    // startup, so a rename converges by re-advertisement.\r\n                    // [impl->REQ-SUBNET-3]\r\n                    node_label: self.node_label.clone(),\r\n                    // The hashed machine id rides beside it (M8 decision\r\n                    // 13/22) — the re-pair overwrite anchor.\r\n                    // [impl->REQ-SUBNET-7]\r\n                    machine_id: self.machine_id.clone(),\r\n                    // The endpoint's own type tag (REQ-MSG-6) — read by the\r\n                    // cross-node user-msg gate, keyed by the proven node.\r\n                    // [impl->REQ-MSG-6]\r\n                    endpoint_type: endpoint_type.clone(),\r\n                    // The unified subnet-display parity datums — gossiped so a\r\n                    // remote picker renders bound/controlled/harness-only with the\r\n                    // same fidelity as the home node (REQ-SUBNET-DISPLAY-PARITY).\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound,\r\n                    controller_node: controller_node.clone(),\r\n                    harness_only,\r\n                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\r\n                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n                    adapter: adapter.clone(),\r\n                    recent_projects: recent_projects.clone(),\r\n                    controlled,\r\n                };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                match advertise_if_visible(\r\n                    reg,\r\n                    &vis,\r\n                    &sub.name,\r\n                    sub.hide_new_endpoints,\r\n                    &id,\r\n                    instance.clone(),\r\n                ) {\r\n                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                        subnet: sub.name.clone(),\r\n                        endpoint_id: id.clone(),\r\n                        instance,\r\n                    })),\r\n                    Ok(None) => {} // hidden in this subnet — not advertised\r\n                    Err(collision) => {\r\n                        // Bare-id collision (another node owns the name in\r\n                        // this subnet): loud skip — resolution is the user's\r\n                        // rename (D3f), never a silent overwrite.\r\n                        eprintln!(\"REGISTRY_COLLISION:{}:{}\", sub.name, collision);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        // Ghost-heal (REQ-HAZARD-ROSTER-GHOST): a LOCAL perch that was ERASED\r\n        // (`api session-end <id> --erase` removes the owlery dir) is no longer in\r\n        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\r\n        // this scan only refreshes perches that still exist, so the stale row is\r\n        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\r\n        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\r\n        // epoch: the lease orders it, so it stops showing Active locally\r\n        // (resource_projection skips non-routable) AND peers converge to gone. Only\r\n        // still-routable local rows are touched (idempotent — an already-Offline\r\n        // ghost is left alone).\r\n        // [impl->REQ-HAZARD-ROSTER-GHOST]\r\n        for sub in &subnets.subnets {\r\n            let reg = regs.entry(sub.name.clone()).or_default();\r\n            let ghosts: Vec<String> = reg\r\n                .endpoint_ids()\r\n                .filter(|id| !live_ids.contains(*id))\r\n                .filter(|id| {\r\n                    reg.instances(id)\r\n                        .iter()\r\n                        .any(|i| i.node == self.node_hex && i.status.routable())\r\n                })\r\n                .map(str::to_string)\r\n                .collect();\r\n            for id in ghosts {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status: Status::Offline,\r\n                    epoch: ep,\r\n                    resources: None,\r\n                    last_active_ms: None,\r\n                    shell_adapters: Vec::new(),\r\n                    node_label: self.node_label.clone(),\r\n                    machine_id: self.machine_id.clone(),\r\n                    endpoint_type: None,\r\n                    // An erased ghost is gone — unbound, no controller, not a\r\n                    // harness seat (REQ-SUBNET-DISPLAY-PARITY); Offline is skipped\r\n                    // by the projection anyway, so these only round-trip the wire.\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound: false,\r\n                    controller_node: None,\r\n                    harness_only: false,\r\n                    adapter: None,\r\n                    recent_projects: Vec::new(),\r\n                    controlled: false,\r\n                };\r\n                reg.merge_instance(&id, instance.clone());\r\n                out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                    subnet: sub.name.clone(),\r\n                    endpoint_id: id,\r\n                    instance,\r\n                }));\r\n                eprintln!(\"ROSTER_GHOST_HEAL:{}: erased perch advertised offline\", sub.name);\r\n            }\r\n        }\r\n\r\n        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per\r\n        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node\r\n        // (zero Self perches, or all hidden) still names itself to peers rather\r\n        // than rendering a bare key-prefix. Rides the same epoch lease; merged\r\n        // into our own registry too so the snapshot carries it. Emitted every\r\n        // round regardless of endpoint count — redundant-but-robust when rows\r\n        // exist (the render prefers a trust/instance label anyway), the SOLE\r\n        // carrier when they don't.\r\n        // [impl->REQ-SUBNET-3]\r\n        if let Some(label) = self.node_label.clone() {\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                reg.merge_node_label(&self.node_hex, label.clone(), ep);\r\n                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\r\n                    subnet: sub.name.clone(),\r","truncated":true,"truncatedBy":"lines","totalLines":1840,"totalBytes":13196,"outputLines":225,"outputBytes":13196,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())\n…\n            .filter(|(r, m)| {\n                r.kind == spt_runtime::manifest::AdapterKind::Shell\r\n                    && m.shell.as_ref().and_then(|s| s.broadcast)\r\n                        == Some(spt_runtime::manifest::Broadcast::Subnet)\r\n            })\r\n            .map(|(r, _)| r.name.clone())\r\n            .collect();\r\n        let mut out = Vec::new();\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut epoch = self.epoch.lock().unwrap();\r\n\r\n        // The set of self perches that still EXIST on disk this round — the\r\n        // ghost-heal predicate below (a LOCAL registry row whose id is absent\r\n        // here was erased, REQ-HAZARD-ROSTER-GHOST).\r\n        let live_ids: std::collections::BTreeSet<String> =\r\n            perch::list_self_perch_ids(owlery).into_iter().collect();\r\n\r\n        for id in perch::list_self_perch_ids(owlery) {\r\n            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\r\n            // Advertisement follows the resting-state machine (D9-2,\r\n            // REQ-INST-3): a live perch with a resting record advertises its\r\n            // machine state (active / dormant / suspended); a recordless\r\n            // (pre-D9-2 / interim) or dead perch keeps the legacy\r\n            // liveness-derived mapping. Every advertisement stamps a fresh\r\n            // epoch below, so a transition reaches peers as an epoch-bumped\r\n            // update — the lease orders it (KH 4.8).\r\n            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]\r\n            let status = advertised_status(&perch_path);\r\n            // Endpoint-authored blurb wins; the node seed fills the gap\r\n            // (both-authored, REQ-INST-14). Rides the same epoch lease as\r\n            // every instance update — a blurb edit reaches peers as an\r\n            // ordered, epoch-bumped row.\r\n            // [impl->REQ-INST-14]\r\n            let info = spt_store::info::read_info(&perch_path);\r\n            let resources = info\r\n                .as_ref()\r\n                .and_then(|i| i.resources.clone())\r\n                .or_else(|| node_blurb.clone());\r\n            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat\r\n            // stamp (`lifecycle::touch_active` — Active seats only; a resting\r\n            // seat never stamps) rides the row like the blurb does — same\r\n            // epoch lease, same visibility gate, no new channel. Subnet-wide\r\n            // most-recently-active is a projection of these rows (D6b).\r\n            // [impl->REQ-PRES-1]\r\n            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the\r\n            // info.json `state` IS the bound endpoint type\r\n            // (`EndpointType::as_tag` — `\"gateway\"` for a Gateway surface).\r\n            // A node advertises ONLY its own endpoints' types; the cross-node\r\n            // user-msg gate reads this at the receive funnel (T3). A recordless\r\n            // perch advertises no type → the funnel fail-closes to a re-stamp.\r\n            // [impl->REQ-MSG-6]\r\n            let endpoint_type = info.as_ref().map(|i| i.state.clone());\r\n            // The unified subnet-display parity datums (REQ-SUBNET-DISPLAY-PARITY):\r\n            // the SAME local facts the picker's `local_rows` reads off info.json,\r\n            // now GOSSIPED so a remote viewer renders bound/controlled/harness-only\r\n            // identically instead of reducing every remote row to plain green.\r\n            //  - bound: a bound live broker session (`is_perch_alive` is bound-gated,\r\n            //    so an UNBOUND perch reads false — exactly the fill duality the\r\n            //    picker keys on).\r\n            //  - controller_node: the broker-owned `driven_by` (the driving node),\r\n            //    if any → the remote blue \"controlled\" + `controlled by <node>` pin.\r\n            //  - harness_only: a LiveAgent with no broker PTY seat — mirrors the\r\n            //    picker's `controllable != Some(true)` for a `live_agent` (a legacy\r\n            //    None self-corrects on the next bind, so it advertises harness-only\r\n            //    until then, matching the local amber).\r\n            // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n            let bound = is_perch_alive(&perch_path);\r\n            // #3 (REQ-GOSSIP-CONTROLLED-ANY): gossip ANY controller, not just a\r\n            // remote one. `driven_by` is remote-only (KH 7.15); a LOCAL controller\r\n            // sets the separate `controlled` flag instead, and its driving node IS\r\n            // this node — so advertise `self.node_hex`. A remote controller keeps\r\n            // its own origin. This is what lets a remote viewer see a\r\n            // locally-controlled endpoint as CONTROLLED, not \"ready to control\".\r\n            // [impl->REQ-GOSSIP-CONTROLLED-ANY]\r\n            let controller_node = gossip_controller_node(\r\n                info.as_ref().and_then(|i| i.driven_by.as_deref()),\r\n                info.as_ref().is_some_and(|i| i.controlled),\r\n                &self.node_hex,\r\n            );\r\n            let harness_only = info.as_ref().is_some_and(|i| {\r\n                i.state == \"live_agent\" && i.controllable != Some(true)\r\n            });\r\n            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\r\n            // real harness adapter + its recent projects + the EXPLICIT any-controller\r\n            // truth, gossiped so `from_resource_row` stops faking remote rows.\r\n            //  - adapter: the composite `<adapter>[:profile]` off info.json (the picker\r\n            //    used to show the resources blurb here).\r\n            //  - recent_projects: sessions.log-derived, owlery-excluded, project IDs only.\r\n            //  - controlled: the raw any-controller marker (NOT controller_node.is_some();\r\n            //    a local controller has driven_by=None so that WHO datum undercounts it).\r\n            // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n            let adapter = info.as_ref().and_then(|i| i.adapter.clone());\r\n            let controlled = info.as_ref().is_some_and(|i| i.controlled);\r\n            let recent_projects = recent_projects_for(&perch_path, owlery);\r\n            let last_active_ms = info.and_then(|i| i.last_active_ms);\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status,\r\n                    epoch: ep,\r\n                    resources: resources.clone(),\r\n                    last_active_ms,\r\n                    shell_adapters: shell_adapters.clone(),\r\n                    // The node label rides every row this node authors\r\n                    // (REQ-SUBNET-3) — set from the OS hostname at daemon\r\n                    // startup, so a rename converges by re-advertisement.\r\n                    // [impl->REQ-SUBNET-3]\r\n                    node_label: self.node_label.clone(),\r\n                    // The hashed machine id rides beside it (M8 decision\r\n                    // 13/22) — the re-pair overwrite anchor.\r\n                    // [impl->REQ-SUBNET-7]\r\n                    machine_id: self.machine_id.clone(),\r\n                    // The endpoint's own type tag (REQ-MSG-6) — read by the\r\n                    // cross-node user-msg gate, keyed by the proven node.\r\n                    // [impl->REQ-MSG-6]\r\n                    endpoint_type: endpoint_type.clone(),\r\n                    // The unified subnet-display parity datums — gossiped so a\r\n                    // remote picker renders bound/controlled/harness-only with the\r\n                    // same fidelity as the home node (REQ-SUBNET-DISPLAY-PARITY).\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound,\r\n                    controller_node: controller_node.clone(),\r\n                    harness_only,\r\n                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\r\n                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n                    adapter: adapter.clone(),\r\n                    recent_projects: recent_projects.clone(),\r\n                    controlled,\r\n                };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                match advertise_if_visible(\r\n                    reg,\r\n                    &vis,\r\n                    &sub.name,\r\n                    sub.hide_new_endpoints,\r\n                    &id,\r\n                    instance.clone(),\r\n                ) {\r\n                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                        subnet: sub.name.clone(),\r\n                        endpoint_id: id.clone(),\r\n                        instance,\r\n                    })),\r\n                    Ok(None) => {} // hidden in this subnet — not advertised\r\n                    Err(collision) => {\r\n                        // Bare-id collision (another node owns the name in\r\n                        // this subnet): loud skip — resolution is the user's\r\n                        // rename (D3f), never a silent overwrite.\r\n                        eprintln!(\"REGISTRY_COLLISION:{}:{}\", sub.name, collision);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        // Ghost-heal (REQ-HAZARD-ROSTER-GHOST): a LOCAL perch that was ERASED\r\n        // (`api session-end <id> --erase` removes the owlery dir) is no longer in\r\n        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\r\n        // this scan only refreshes perches that still exist, so the stale row is\r\n        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\r\n        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\r\n        // epoch: the lease orders it, so it stops showing Active locally\r\n        // (resource_projection skips non-routable) AND peers converge to gone. Only\r\n        // still-routable local rows are touched (idempotent — an already-Offline\r\n        // ghost is left alone).\r\n        // [impl->REQ-HAZARD-ROSTER-GHOST]\r\n        for sub in &subnets.subnets {\r\n            let reg = regs.entry(sub.name.clone()).or_default();\r\n            let ghosts: Vec<String> = reg\r\n                .endpoint_ids()\r\n                .filter(|id| !live_ids.contains(*id))\r\n                .filter(|id| {\r\n                    reg.instances(id)\r\n                        .iter()\r\n                        .any(|i| i.node == self.node_hex && i.status.routable())\r\n                })\r\n                .map(str::to_string)\r\n                .collect();\r\n            for id in ghosts {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let instance = Instance {\r\n                    node: self.node_hex.clone(),\r\n                    status: Status::Offline,\r\n                    epoch: ep,\r\n                    resources: None,\r\n                    last_active_ms: None,\r\n                    shell_adapters: Vec::new(),\r\n                    node_label: self.node_label.clone(),\r\n                    machine_id: self.machine_id.clone(),\r\n                    endpoint_type: None,\r\n                    // An erased ghost is gone — unbound, no controller, not a\r\n                    // harness seat (REQ-SUBNET-DISPLAY-PARITY); Offline is skipped\r\n                    // by the projection anyway, so these only round-trip the wire.\r\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n                    bound: false,\r\n                    controller_node: None,\r\n                    harness_only: false,\r\n                    adapter: None,\r\n                    recent_projects: Vec::new(),\r\n                    controlled: false,\r\n                };\r\n                reg.merge_instance(&id, instance.clone());\r\n                out.push(RegistryFeedRecord::Instance(RegistryUpdate {\r\n                    subnet: sub.name.clone(),\r\n                    endpoint_id: id,\r\n                    instance,\r\n                }));\r\n                eprintln!(\"ROSTER_GHOST_HEAL:{}: erased perch advertised offline\", sub.name);\r\n            }\r\n        }\r\n\r\n        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per\r\n        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node\r\n        // (zero Self perches, or all hidden) still names itself to peers rather\r\n        // than rendering a bare key-prefix. Rides the same epoch lease; merged\r\n        // into our own registry too so the snapshot carries it. Emitted every\r\n        // round regardless of endpoint count — redundant-but-robust when rows\r\n        // exist (the render prefers a trust/instance label anyway), the SOLE\r\n        // carrier when they don't.\r\n        // [impl->REQ-SUBNET-3]\r\n        if let Some(label) = self.node_label.clone() {\r\n            for sub in &subnets.subnets {\r\n                let Ok(ep) = epoch.next_epoch() else { continue };\r\n                let reg = regs.entry(sub.name.clone()).or_default();\r\n                reg.merge_node_label(&self.node_hex, label.clone(), ep);\r\n                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {\r\n                    subnet: sub.name.clone(),\r\n…\n                }));\n            }\n        }","startLine":396,"lineNumbers":[396,null,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,null,627,628,629]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2238,"totalBytes":13196,"outputLines":225,"outputBytes":13196,"shownRange":{"start":399,"end":623},"nextOffset":624}}},"isError":false,"timestamp":1784271645170}}
{"type":"message","id":"e670627e","parentId":"77d82b1e","timestamp":"2026-07-17T07:00:50.306Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying self-deadlock in locking sequence**"},{"type":"toolCall","id":"call_CnaHqYBIhguH6Ccbaoc9RfMW|fc_038f28d3e93bbfd4016a59d3220d4481968de813f052103328","name":"read","arguments":{"i":"Confirming registry self-deadlock","path":"crates/spt-daemon/src/registryhost.rs","selector":"190-350"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":9795,"output":105,"cacheRead":228864,"cacheWrite":0,"totalTokens":238764,"reasoningTokens":60,"cost":{"input":0.048975000000000005,"output":0.00315,"cacheRead":0.11443199999999999,"cacheWrite":0,"total":0.166557}},"stopReason":"toolUse","timestamp":1784271645215,"responseId":"resp_038f28d3e93bbfd4016a59d31d9380819685728edee994d792","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_038f28d3e93bbfd4016a59d32014648196a9d2c1c124ffc85d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdMirnQN8WNizj0lhS6qPenShl7-M_3AwtrlUK3d3FF0WUjD-Qof-CAoorVnGOAX8_bPIhpLuAkq8MjQMl-JINUdS9ZU02jTVY198E_wVYZHti4MRWWiiXMGWuumQR7Uy2N13c2s85ZJQt2HOrk2khrL5MyD4I0_fCODoJi-WHXj_RCbO31yUUtIioiN5wcW6iuLyIteBjDsA6fC3ihTiDtyQojsAUOQtH_Y7pwFWoKGK0J9kWzwp5WpCA2st6gU0UmXtBWI0EC34F1lcFBCo6YWIG4ORCr87AIFnPvC3QCY7RIXElwH6GgLzbTTOwhUk37kZRy58wYzNiYqWg9Z2G-pGib-qiGiSKkdeK6PqBpHCyOhuNnIj4k-t9hF8ynxjrTPMJAaaigBuh5G1NJubkeR9kRrAiGl2XfCVHu1LSbL0AyMEzBTGW5YSv4DxGABCYXb2j2df2rjfkuO7R7mFqJESZ5C3ZTIylCpn5rp5pHag29cKmI3lY0C5ZsySuZZOoychTXKwWb5YxIVY1j4ERlTlM9WOX8V928tl_JiZWo0uaBQH7FlHEXSGADWyx5qa7LSXE6XmcwznxGOpiTQX7a0dU_4_k1e3olqHm0U-uTyTTr393w6bPiHmRojIubzeqZbx9jpJ9PoGkG-ZXRPudu4Hc8YPJi-UzfdjZgmcE6lSftrg1iBj230HMgFxpgyLmFiann9yvZQcIoqTzV8rZVQa9-JtjKqIEzSHEROMqyKn1W01lulZm2jBxAN0Smpz_42qboQxLJjt0whaYYbBp0H2P_aQ06JxIBgT5zjaJz5Xzq9WPmDBqGVCRbx-4UICbN8lxrPsYRY3P6ImuAKX6RqTC1ULLmHbOCEvfJr45CDBae43I3pfylE33B3wZftRiBIJxWZ5uYUO7GeVScRGnIF6MS8K5bcJLVaGXfoexTCkbz9jHDQGdnEJ2fVeyTXfyoN2mQmfUEoS25vvZVYKpRk-WZDJkyb_JUcEIjZ9aG3e-2V2mHfAHhllQ_uFXTThZ4XcLCwH3WMUNx6hPZ1lRzl28OVCfCleEgX585on7KlBB5Lf-UNft-McYALhJLkERW9jSYRffioamlb_onuJda1xQM7XIJMFYwz7kvjN_Mqe7XeTJPWgswKh0aCRgZ4srJPzb4ECRLy8Fr80jTUMHiL3IhLsm_W-uHjdJQl1aXwjG5zf0nXodMpg3umcY8_bY2z3cIszbF-yg0ljWF1h52_EQ==","summary":[{"type":"summary_text","text":"**Identifying self-deadlock in locking sequence**"}]},{"id":"fc_038f28d3e93bbfd4016a59d3220d4481968de813f052103328","type":"function_call","status":"completed","arguments":"{\"i\":\"Confirming registry self-deadlock\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"190-350\"}","call_id":"call_CnaHqYBIhguH6Ccbaoc9RfMW","name":"read"}]},"duration":5061.849100008607,"ttft":2876.822200000286,"contextSnapshot":{"promptTokens":238659,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CnaHqYBIhguH6Ccbaoc9RfMW|fc_038f28d3e93bbfd4016a59d3220d4481968de813f052103328","toolName":"read","startedAt":"2026-07-17T07:00:50.306Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Confirming registry self-deadlock"},"id":"576cf873","parentId":"e670627e","timestamp":"2026-07-17T07:00:50.306Z"}
{"type":"message","id":"9f578818","parentId":"576cf873","timestamp":"2026-07-17T07:00:50.332Z","message":{"role":"toolResult","toolCallId":"call_CnaHqYBIhguH6Ccbaoc9RfMW|fc_038f28d3e93bbfd4016a59d3220d4481968de813f052103328","toolName":"read","content":[{"type":"text","text":"    pub fn instances_of(&self, endpoint_id: &str) -> Vec<Instance> {\n        self.regs\n…\n            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())\n            .collect()\n    }\n\n    /// Run `f` with this host's **canonical per-node epoch** — the ONE\n    /// monotonic counter (identity/epoch.json) behind every locally-authored\n    /// registry row. Every other per-node-id producer on this node MUST mint\n    /// from the same source, or `node:epoch`-shaped ids collide: a notif_id is\n    /// `node_hex:epoch`, so the pump's consent-notif producer and the CLI\n    /// notify front door (which loads this same canonical source) would\n    /// otherwise issue the same id from independent counters. (Field bug: the\n    /// pump minted consent notif_ids from the stream-open op counter\n    /// `pump-ops.json` — a different file — so on a node whose two counters\n    /// were both low the consent notif and a presence/notify notif collided on\n    /// `node:1`. The rig's presence-redirect rung caught it: `UNIQUE\n    /// constraint failed: notifs.notif_id`.) Locks the shared cursor so the\n    /// dispatcher's registry arm and the pump never double-mint.\n    // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]\n    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {\n        f(&mut self.epoch.lock().unwrap())\n    }\n\n    /// Apply one inbound feed under the gate. `origin_node` MUST be the\n    /// handshake-proven stream-table origin, never payload bytes.\n    // [impl->REQ-INST-7]\n    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n    pub fn apply_feed(\n        &self,\n        origin_node: &str,\n        updates: &[RegistryUpdate],\n        policy: &RegistryGatePolicy,\n    ) -> Vec<RegistryApplyVerdict> {\n        self.apply_feed_flips(origin_node, updates, policy).0\n    }\n\n    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention\n    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was\n    /// an observed *transition to `Active` on a peer node* — the wire signal\n    /// that another instance of the id just became the driven one (its\n    /// daemon advertises transitions epoch-bumped), which rests a local\n    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes\n    /// `ling@desktop` dormant).\n    ///\n    /// Flip = `Updated` merge where the stored row's status was NOT `Active`\n    /// and the incoming one is. Deliberately **never `Inserted`**: this\n    /// registry is in-memory, so a daemon restart re-inserts every peer row\n    /// at its *current* status — counting inserts would knock local\n    /// instances dormant on every restart; and never the periodic\n    /// re-advertisement (`Updated` with prev already `Active` is not a\n    /// flip). The lease has already dropped stale/equal epochs before any\n    /// of this runs.\n    // [impl->REQ-INST-3]\n    pub fn apply_feed_flips(\n        &self,\n        origin_node: &str,\n        updates: &[RegistryUpdate],\n        policy: &RegistryGatePolicy,\n    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {\n        let mut regs = self.regs.lock().unwrap();\n        let mut flips: Vec<String> = Vec::new();\n        let mut admitted_any = false;\n        let verdicts = updates\n            .iter()\n            .map(|u| {\n                if !policy.subnets.iter().any(|s| s == &u.subnet) {\n                    return RegistryApplyVerdict::NotMember;\n                }\n                // [impl->REQ-MESH-5]\n                if !policy.roster.is_member(&u.subnet, origin_node) {\n                    return RegistryApplyVerdict::UntrustedOrigin;\n                }\n                admitted_any = true;\n                let reg = regs.entry(u.subnet.clone()).or_default();\n                let prev = reg\n                    .instances(&u.endpoint_id)\n                    .iter()\n                    .find(|i| i.node == u.instance.node)\n                    .map(|i| i.status);\n                // The epoch lease (4.8) decides; an own-row echo from a peer\n                // is dropped Stale by construction (our epoch is newer or\n                // equal — never authored elsewhere).\n                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());\n                if outcome == MergeOutcome::Updated\n                    && u.instance.status == Status::Active\n                    && prev != Some(Status::Active)\n                    && u.instance.node != self.node_hex\n                    && !flips.contains(&u.endpoint_id)\n                {\n                    flips.push(u.endpoint_id.clone());\n                }\n                RegistryApplyVerdict::Merged(outcome)\n            })\n            .collect();\n        drop(regs);\n        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin\n        // node is up right now — even a Stale merge verdict is a liveness\n        // signal (the lease judged the row, not the sender). Refused feeds\n        // stamp nothing (an untrusted sender earns no liveness credit).\n        // [impl->REQ-SUBNET-1]\n        if admitted_any {\n            self.heard\n                .lock()\n                .unwrap()\n                .insert(origin_node.to_string(), wall_ms());\n            self.stamp_admit();\n        }\n        self.write_snapshots();\n        (verdicts, flips)\n    }\n\n    /// Apply inbound node-level label updates (REQ-SUBNET-3) under the SAME\n    /// fail-closed gate as [`apply_feed_flips`]: a non-member subnet or an\n    /// untrusted origin is refused; an admitted update merges via\n    /// [`SubnetRegistry::merge_node_label`] (epoch lease) and stamps the\n    /// origin's gossip-recency (an admitted label feed proves the peer is up,\n    /// exactly like an instance feed). The endpoint-less carrier: this is how a\n    /// peer with zero endpoints gets its hostname into our `--nodes` view.\n    // [impl->REQ-SUBNET-3]\n    pub fn apply_node_labels(\n        &self,\n        origin_node: &str,\n        updates: &[NodeLabelUpdate],\n        policy: &RegistryGatePolicy,\n    ) -> Vec<RegistryApplyVerdict> {\n        let mut regs = self.regs.lock().unwrap();\n        let mut admitted_any = false;\n        let verdicts = updates\n            .iter()\n            .map(|u| {\n                if !policy.subnets.iter().any(|s| s == &u.subnet) {\n                    return RegistryApplyVerdict::NotMember;\n                }\n                // [impl->REQ-MESH-5]\n                if !policy.roster.is_member(&u.subnet, origin_node) {\n                    return RegistryApplyVerdict::UntrustedOrigin;\n                }\n                admitted_any = true;\n                let reg = regs.entry(u.subnet.clone()).or_default();\n                RegistryApplyVerdict::Merged(reg.merge_node_label(\n                    &u.node,\n                    u.label.clone(),\n                    u.epoch,\n                ))\n            })\n            .collect();\n        drop(regs);\n        if admitted_any {\n            self.heard\n                .lock()\n                .unwrap()\n                .insert(origin_node.to_string(), wall_ms());\n            self.stamp_admit();\n        }\n        self.write_snapshots();\n        verdicts\n    }\n\n    /// Record an admitted inbound feed (any origin) for the health surface.\n    // [impl->REQ-PUMP-STAGE-TRUTH]\n    fn stamp_admit(&self) {\n        self.last_admit_ms\n            .store(wall_ms(), std::sync::atomic::Ordering::Relaxed);\n    }\n\n    /// Wall-clock ms of the last admitted inbound registry/label feed, if any\n\n[Showing lines 189-353 of 2238. Use :354 to continue]"}],"details":{"truncation":{"content":"            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())\r\n            .collect()\r\n    }\r\n\r\n    /// Run `f` with this host's **canonical per-node epoch** — the ONE\r\n    /// monotonic counter (identity/epoch.json) behind every locally-authored\r\n    /// registry row. Every other per-node-id producer on this node MUST mint\r\n    /// from the same source, or `node:epoch`-shaped ids collide: a notif_id is\r\n    /// `node_hex:epoch`, so the pump's consent-notif producer and the CLI\r\n    /// notify front door (which loads this same canonical source) would\r\n    /// otherwise issue the same id from independent counters. (Field bug: the\r\n    /// pump minted consent notif_ids from the stream-open op counter\r\n    /// `pump-ops.json` — a different file — so on a node whose two counters\r\n    /// were both low the consent notif and a presence/notify notif collided on\r\n    /// `node:1`. The rig's presence-redirect rung caught it: `UNIQUE\r\n    /// constraint failed: notifs.notif_id`.) Locks the shared cursor so the\r\n    /// dispatcher's registry arm and the pump never double-mint.\r\n    // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]\r\n    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {\r\n        f(&mut self.epoch.lock().unwrap())\r\n    }\r\n\r\n    /// Apply one inbound feed under the gate. `origin_node` MUST be the\r\n    /// handshake-proven stream-table origin, never payload bytes.\r\n    // [impl->REQ-INST-7]\r\n    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n    pub fn apply_feed(\r\n        &self,\r\n        origin_node: &str,\r\n        updates: &[RegistryUpdate],\r\n        policy: &RegistryGatePolicy,\r\n    ) -> Vec<RegistryApplyVerdict> {\r\n        self.apply_feed_flips(origin_node, updates, policy).0\r\n    }\r\n\r\n    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention\r\n    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was\r\n    /// an observed *transition to `Active` on a peer node* — the wire signal\r\n    /// that another instance of the id just became the driven one (its\r\n    /// daemon advertises transitions epoch-bumped), which rests a local\r\n    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes\r\n    /// `ling@desktop` dormant).\r\n    ///\r\n    /// Flip = `Updated` merge where the stored row's status was NOT `Active`\r\n    /// and the incoming one is. Deliberately **never `Inserted`**: this\r\n    /// registry is in-memory, so a daemon restart re-inserts every peer row\r\n    /// at its *current* status — counting inserts would knock local\r\n    /// instances dormant on every restart; and never the periodic\r\n    /// re-advertisement (`Updated` with prev already `Active` is not a\r\n    /// flip). The lease has already dropped stale/equal epochs before any\r\n    /// of this runs.\r\n    // [impl->REQ-INST-3]\r\n    pub fn apply_feed_flips(\r\n        &self,\r\n        origin_node: &str,\r\n        updates: &[RegistryUpdate],\r\n        policy: &RegistryGatePolicy,\r\n    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut flips: Vec<String> = Vec::new();\r\n        let mut admitted_any = false;\r\n        let verdicts = updates\r\n            .iter()\r\n            .map(|u| {\r\n                if !policy.subnets.iter().any(|s| s == &u.subnet) {\r\n                    return RegistryApplyVerdict::NotMember;\r\n                }\r\n                // [impl->REQ-MESH-5]\r\n                if !policy.roster.is_member(&u.subnet, origin_node) {\r\n                    return RegistryApplyVerdict::UntrustedOrigin;\r\n                }\r\n                admitted_any = true;\r\n                let reg = regs.entry(u.subnet.clone()).or_default();\r\n                let prev = reg\r\n                    .instances(&u.endpoint_id)\r\n                    .iter()\r\n                    .find(|i| i.node == u.instance.node)\r\n                    .map(|i| i.status);\r\n                // The epoch lease (4.8) decides; an own-row echo from a peer\r\n                // is dropped Stale by construction (our epoch is newer or\r\n                // equal — never authored elsewhere).\r\n                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());\r\n                if outcome == MergeOutcome::Updated\r\n                    && u.instance.status == Status::Active\r\n                    && prev != Some(Status::Active)\r\n                    && u.instance.node != self.node_hex\r\n                    && !flips.contains(&u.endpoint_id)\r\n                {\r\n                    flips.push(u.endpoint_id.clone());\r\n                }\r\n                RegistryApplyVerdict::Merged(outcome)\r\n            })\r\n            .collect();\r\n        drop(regs);\r\n        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin\r\n        // node is up right now — even a Stale merge verdict is a liveness\r\n        // signal (the lease judged the row, not the sender). Refused feeds\r\n        // stamp nothing (an untrusted sender earns no liveness credit).\r\n        // [impl->REQ-SUBNET-1]\r\n        if admitted_any {\r\n            self.heard\r\n                .lock()\r\n                .unwrap()\r\n                .insert(origin_node.to_string(), wall_ms());\r\n            self.stamp_admit();\r\n        }\r\n        self.write_snapshots();\r\n        (verdicts, flips)\r\n    }\r\n\r\n    /// Apply inbound node-level label updates (REQ-SUBNET-3) under the SAME\r\n    /// fail-closed gate as [`apply_feed_flips`]: a non-member subnet or an\r\n    /// untrusted origin is refused; an admitted update merges via\r\n    /// [`SubnetRegistry::merge_node_label`] (epoch lease) and stamps the\r\n    /// origin's gossip-recency (an admitted label feed proves the peer is up,\r\n    /// exactly like an instance feed). The endpoint-less carrier: this is how a\r\n    /// peer with zero endpoints gets its hostname into our `--nodes` view.\r\n    // [impl->REQ-SUBNET-3]\r\n    pub fn apply_node_labels(\r\n        &self,\r\n        origin_node: &str,\r\n        updates: &[NodeLabelUpdate],\r\n        policy: &RegistryGatePolicy,\r\n    ) -> Vec<RegistryApplyVerdict> {\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut admitted_any = false;\r\n        let verdicts = updates\r\n            .iter()\r\n            .map(|u| {\r\n                if !policy.subnets.iter().any(|s| s == &u.subnet) {\r\n                    return RegistryApplyVerdict::NotMember;\r\n                }\r\n                // [impl->REQ-MESH-5]\r\n                if !policy.roster.is_member(&u.subnet, origin_node) {\r\n                    return RegistryApplyVerdict::UntrustedOrigin;\r\n                }\r\n                admitted_any = true;\r\n                let reg = regs.entry(u.subnet.clone()).or_default();\r\n                RegistryApplyVerdict::Merged(reg.merge_node_label(\r\n                    &u.node,\r\n                    u.label.clone(),\r\n                    u.epoch,\r\n                ))\r\n            })\r\n            .collect();\r\n        drop(regs);\r\n        if admitted_any {\r\n            self.heard\r\n                .lock()\r\n                .unwrap()\r\n                .insert(origin_node.to_string(), wall_ms());\r\n            self.stamp_admit();\r\n        }\r\n        self.write_snapshots();\r\n        verdicts\r\n    }\r\n\r\n    /// Record an admitted inbound feed (any origin) for the health surface.\r\n    // [impl->REQ-PUMP-STAGE-TRUTH]\r\n    fn stamp_admit(&self) {\r\n        self.last_admit_ms\r\n            .store(wall_ms(), std::sync::atomic::Ordering::Relaxed);\r\n    }\r\n\r\n    /// Wall-clock ms of the last admitted inbound registry/label feed, if any\r","truncated":true,"truncatedBy":"lines","totalLines":2050,"totalBytes":7522,"outputLines":165,"outputBytes":7522,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn instances_of(&self, endpoint_id: &str) -> Vec<Instance> {\n        self.regs\n…\n            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())\r\n            .collect()\r\n    }\r\n\r\n    /// Run `f` with this host's **canonical per-node epoch** — the ONE\r\n    /// monotonic counter (identity/epoch.json) behind every locally-authored\r\n    /// registry row. Every other per-node-id producer on this node MUST mint\r\n    /// from the same source, or `node:epoch`-shaped ids collide: a notif_id is\r\n    /// `node_hex:epoch`, so the pump's consent-notif producer and the CLI\r\n    /// notify front door (which loads this same canonical source) would\r\n    /// otherwise issue the same id from independent counters. (Field bug: the\r\n    /// pump minted consent notif_ids from the stream-open op counter\r\n    /// `pump-ops.json` — a different file — so on a node whose two counters\r\n    /// were both low the consent notif and a presence/notify notif collided on\r\n    /// `node:1`. The rig's presence-redirect rung caught it: `UNIQUE\r\n    /// constraint failed: notifs.notif_id`.) Locks the shared cursor so the\r\n    /// dispatcher's registry arm and the pump never double-mint.\r\n    // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]\r\n    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {\r\n        f(&mut self.epoch.lock().unwrap())\r\n    }\r\n\r\n    /// Apply one inbound feed under the gate. `origin_node` MUST be the\r\n    /// handshake-proven stream-table origin, never payload bytes.\r\n    // [impl->REQ-INST-7]\r\n    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n    pub fn apply_feed(\r\n        &self,\r\n        origin_node: &str,\r\n        updates: &[RegistryUpdate],\r\n        policy: &RegistryGatePolicy,\r\n    ) -> Vec<RegistryApplyVerdict> {\r\n        self.apply_feed_flips(origin_node, updates, policy).0\r\n    }\r\n\r\n    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention\r\n    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was\r\n    /// an observed *transition to `Active` on a peer node* — the wire signal\r\n    /// that another instance of the id just became the driven one (its\r\n    /// daemon advertises transitions epoch-bumped), which rests a local\r\n    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes\r\n    /// `ling@desktop` dormant).\r\n    ///\r\n    /// Flip = `Updated` merge where the stored row's status was NOT `Active`\r\n    /// and the incoming one is. Deliberately **never `Inserted`**: this\r\n    /// registry is in-memory, so a daemon restart re-inserts every peer row\r\n    /// at its *current* status — counting inserts would knock local\r\n    /// instances dormant on every restart; and never the periodic\r\n    /// re-advertisement (`Updated` with prev already `Active` is not a\r\n    /// flip). The lease has already dropped stale/equal epochs before any\r\n    /// of this runs.\r\n    // [impl->REQ-INST-3]\r\n    pub fn apply_feed_flips(\r\n        &self,\r\n        origin_node: &str,\r\n        updates: &[RegistryUpdate],\r\n        policy: &RegistryGatePolicy,\r\n    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut flips: Vec<String> = Vec::new();\r\n        let mut admitted_any = false;\r\n        let verdicts = updates\r\n            .iter()\r\n            .map(|u| {\r\n                if !policy.subnets.iter().any(|s| s == &u.subnet) {\r\n                    return RegistryApplyVerdict::NotMember;\r\n                }\r\n                // [impl->REQ-MESH-5]\r\n                if !policy.roster.is_member(&u.subnet, origin_node) {\r\n                    return RegistryApplyVerdict::UntrustedOrigin;\r\n                }\r\n                admitted_any = true;\r\n                let reg = regs.entry(u.subnet.clone()).or_default();\r\n                let prev = reg\r\n                    .instances(&u.endpoint_id)\r\n                    .iter()\r\n                    .find(|i| i.node == u.instance.node)\r\n                    .map(|i| i.status);\r\n                // The epoch lease (4.8) decides; an own-row echo from a peer\r\n                // is dropped Stale by construction (our epoch is newer or\r\n                // equal — never authored elsewhere).\r\n                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());\r\n                if outcome == MergeOutcome::Updated\r\n                    && u.instance.status == Status::Active\r\n                    && prev != Some(Status::Active)\r\n                    && u.instance.node != self.node_hex\r\n                    && !flips.contains(&u.endpoint_id)\r\n                {\r\n                    flips.push(u.endpoint_id.clone());\r\n                }\r\n                RegistryApplyVerdict::Merged(outcome)\r\n            })\r\n            .collect();\r\n        drop(regs);\r\n        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin\r\n        // node is up right now — even a Stale merge verdict is a liveness\r\n        // signal (the lease judged the row, not the sender). Refused feeds\r\n        // stamp nothing (an untrusted sender earns no liveness credit).\r\n        // [impl->REQ-SUBNET-1]\r\n        if admitted_any {\r\n            self.heard\r\n                .lock()\r\n                .unwrap()\r\n                .insert(origin_node.to_string(), wall_ms());\r\n            self.stamp_admit();\r\n        }\r\n        self.write_snapshots();\r\n        (verdicts, flips)\r\n    }\r\n\r\n    /// Apply inbound node-level label updates (REQ-SUBNET-3) under the SAME\r\n    /// fail-closed gate as [`apply_feed_flips`]: a non-member subnet or an\r\n    /// untrusted origin is refused; an admitted update merges via\r\n    /// [`SubnetRegistry::merge_node_label`] (epoch lease) and stamps the\r\n    /// origin's gossip-recency (an admitted label feed proves the peer is up,\r\n    /// exactly like an instance feed). The endpoint-less carrier: this is how a\r\n    /// peer with zero endpoints gets its hostname into our `--nodes` view.\r\n    // [impl->REQ-SUBNET-3]\r\n    pub fn apply_node_labels(\r\n        &self,\r\n        origin_node: &str,\r\n        updates: &[NodeLabelUpdate],\r\n        policy: &RegistryGatePolicy,\r\n    ) -> Vec<RegistryApplyVerdict> {\r\n        let mut regs = self.regs.lock().unwrap();\r\n        let mut admitted_any = false;\r\n        let verdicts = updates\r\n            .iter()\r\n            .map(|u| {\r\n                if !policy.subnets.iter().any(|s| s == &u.subnet) {\r\n                    return RegistryApplyVerdict::NotMember;\r\n                }\r\n                // [impl->REQ-MESH-5]\r\n                if !policy.roster.is_member(&u.subnet, origin_node) {\r\n                    return RegistryApplyVerdict::UntrustedOrigin;\r\n                }\r\n                admitted_any = true;\r\n                let reg = regs.entry(u.subnet.clone()).or_default();\r\n                RegistryApplyVerdict::Merged(reg.merge_node_label(\r\n                    &u.node,\r\n                    u.label.clone(),\r\n                    u.epoch,\r\n                ))\r\n            })\r\n            .collect();\r\n        drop(regs);\r\n        if admitted_any {\r\n            self.heard\r\n                .lock()\r\n                .unwrap()\r\n                .insert(origin_node.to_string(), wall_ms());\r\n            self.stamp_admit();\r\n        }\r\n        self.write_snapshots();\r\n        verdicts\r\n    }\r\n\r\n    /// Record an admitted inbound feed (any origin) for the health surface.\r\n    // [impl->REQ-PUMP-STAGE-TRUTH]\r\n    fn stamp_admit(&self) {\r\n        self.last_admit_ms\r\n            .store(wall_ms(), std::sync::atomic::Ordering::Relaxed);\r\n    }\r\n\r\n    /// Wall-clock ms of the last admitted inbound registry/label feed, if any\r","startLine":184,"lineNumbers":[184,185,null,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2238,"totalBytes":7522,"outputLines":165,"outputBytes":7522,"shownRange":{"start":189,"end":353},"nextOffset":354}}},"isError":false,"timestamp":1784271650332}}
{"type":"compaction","id":"d0e5cde3","parentId":"9f578818","timestamp":"2026-07-17T07:00:50.459Z","summary":"You are resuming a prior conversation. Its earlier turns were archived to reclaim context and are reproduced under HISTORY below, oldest to newest. Read HISTORY in full, then continue from the live conversation that follows it.\n\nThe archived transcript is compact: each turn opens with a heading — `# User ¶`, `# Assistant ¶`, or `# Tool call ¶` — assistant reasoning is wrapped in _italics_, and tool output sits inside `<out>…</out>`.\n\nReading HISTORY:\n- Plain-text sections are the verbatim transcript — rely on them exactly.\n- Some middle sections are attached as images instead of text. Each image is a page of that same transcript and belongs at its place in the reading order, between marked delimiters. Within an image, a solid black cell marks a newline and runs of spaces collapse to one.\n  - A frame is one grid 196 characters wide and up to 71 rows tall: read left to right, top to bottom — there is no word wrap, so a word may break across rows.\n  - Text inside `<out>` is dim gray — that gray is archived tool output, not conversation.\n- When an exact earlier detail matters and a section reads unclearly, re-derive it from the workspace (re-read files, re-run commands) rather than guessing.\n\nFILES\n===================\n# crates/\n## spt-daemon/src/\nbrainproc.rs (Read)\nbroker.rs (Read)\ndispatch.rs (Read)\nlifecycle.rs (Read)\nlivehost.rs (Read)\nnethost.rs (Read)\nprojwriter.rs (Read)\nregistryhost.rs (Read)\nshellwake.rs (Read)\nsync.rs (Read)\n### pump/\nmod.rs (Read)\nnotif.rs (Read)\nregistry.rs (Read)\nsync.rs (Read)\nupdate.rs (Read)\n## spt-live/src/\nhistory.rs (Read)\ningest.rs (Read)\npulse.rs (Read)\n## spt-runtime/src/\nregistry.rs (Read)\n## spt-store/src/\nbranchstore.rs (Read)\n[…7 files elided…]\n\nHISTORY\n===================","shortSummary":"Archived 132,799 chars of history onto 8 snapcompact frames (+27,833 chars as text)","firstKeptEntryId":"dc02ae28","tokensBefore":238764,"details":{"readFiles":["crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/pump/notif.rs","crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/pump/sync.rs","crates/spt-daemon/src/pump/update.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/shellwake.rs","crates/spt-daemon/src/sync.rs","crates/spt-live/src/history.rs","crates/spt-live/src/ingest.rs","crates/spt-live/src/pulse.rs","crates/spt-runtime/src/registry.rs","crates/spt-store/src/branchstore.rs","crates/spt-store/src/contextstore.rs","crates/spt-store/src/info.rs","crates/spt-store/src/notif.rs","crates/spt-store/src/projderive.rs","crates/spt-store/src/project.rs","crates/spt-store/src/projinval.rs","crates/spt-store/src/sessions.rs"],"modifiedFiles":[]},"fromExtension":false,"preserveData":{"snapcompact":{"frames":[{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"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","mimeType":"image/png","cols":196,"rows":71,"chars":13916,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"},{"data":"iVBORw0KGgoAAAANSUhEUgAABiAAAANaAgMAAAB98GweAAAACVBMVEX///8AAACAgIBJX6m2AACV8klEQVR42u1dS27juhKlBRkgNFKAZE54ZHgVvkAW4EGyHyEjL0ND467yqepU8SdKltNJd/o+6r7XscVfkUckZdXRKWN+6PH+/lMt+wE9bDac+QFm/oBjy0jNe2in/8xpiAqNzVis3+dx+u+A/OEcHZ2JyltN70JRbnHxsDDzdGvGbmxMnL9bLOTyCoyzcY9CaTdr654t0rb0abJraIbTTWvqNJ/k5ZEsjtTUn6ja9/epb26q1TUxEM6Fdu10pFY0mcUu+ndKs5TXxblt3BX61ug1MaBFqrFZBYKKORuAsNF1ZWd2udnVFwMRLLALQDRlTGwzy+qMdZaHS/vfJEAEEPKRslZHoeEeTrCMk5Vj4/M6Ndua+AKMh2oZCLXClWGzWjYFYuViVCCmzgoQbGEJCLMCxNHm470GxII9hXSyavovAsJsBcLbDyAOpnFTD/2MGKiomyZJN55GmoI0Tt14mL51A32/TVOKUuizszRFDyPlPsiEJwtOI52jHLSc0Bnk6a6Ga5uWK57c9G93nf6bSnTcapj4WoJqnoAYPBBs4Ylr7ridE+ektk/SqlrE53koDpOlVD9ZqRYEO7UtugAHexhDTVqGv012Ui7UgeMwEhCHUXNNNlE+7mkAojRSBETHZygPLU0TntYqEJgup4Gqt5M5zp5G+pfWp4an+Idt/Lo7TUrLV9uUG9dcwyDZ6RylUC105jRa+m7dkTpveFI6XiDonDtiaXJcG4PMSxhKUNuTmTxkjYkWFUt1TGbxooXaLOaypdad/IuhcFdaQTi/tyDYGdoiIKzVmkIZXrCPbK87RjM4tHTk/NyzqR7qaQCiNFKnkfIexUICgia9UyCwEDl3usm1fbNslPWDH4CgjYq3KdtNuZ0NCxGVoJSO8siZqcYbd9p0t4avBp4RN4AFIKgMX0G3UIIsoD3iaDEjrG+hYeu6G+eUtjsuPVk0naMlY6oRQ3GcvjvKHywIdmpbZN8HX3J0BkCgTHfDME+5nIuXUoBtHff/dMMldrq5ZGkqjZQ1yNuZAEQzeCB4ATa86o0Wm9EiEM5MBZHb8kaTAMEr4ejCGblurVw3hq58nnkKhB10xZZ8sv4CCCdAOLnV4hkhtjmujVsavT1sg/ObteP6A4hNZKdv6xiAQM+0jJPrfRq4YRkIJ4PrTApEaaQAxFG2ZwLixJdUE20RvEhgSgIC/tdGS5ONJ5zjSZ0AAfOMDrJ0+UgdtTzFsTRNQBxlabIMhJgv+bgEtf3+7jwQsNBaWZqmvNpagyVmOoMFQ8s0MgRUv9hyFCBsuDioLVrZr1cbavJlyCIqNeWSy6SRQee7Jt/fsDQhPV2a4pFCDbTH0F/cvtJ9UgLEaeBtNtqsLd338mY9TLtGk2zW07/XTnA+8TxqJAVbviwHvC3zp+tpxGY43TnTVn2lGk9j4+/O/aZOqSMBwZvpGIBoZJOm2hrZrKkWbKrYFMkGzjGwjVy/2MIWBDu9dXzrbkNNvgxZRKW0p1P+gf8b6XcEWqOz3DPerJF+GpZGyvLe6QwsnH4pTb8hpuXLbfhZXM7y7818+/Frv6y3/+J3dnueJvwaWmg1Ty+NlJO8f8mzg99lps7IX82z/XC/uYd/CRC1h/8NMysQFYhqZgWiAlHNrEBUIKqZtYd/kZl/Rw/XmvpOM763i5+v/Y9dXBWIR0qy2yF+muLoGeENTw7JuTH9f6QniqfhxE8cTzd6inkSdyXlxHNQfh45PYX/9yb5BuUp6BP30AqlsYvQ0RNf9sKNM7uCCyBifQQmRGyx/Iunc7e03W6WmxwSTmpL05oh9n5blxeN2Rd4MtwVbdGjWynpSkCocz24eOTZPPtUQVqAx8I5TZP/vNNSXDpOHCWTO1B5CnDVsDdFx5hAGpBqxU+wDoSnDdj5E1APxADHqudHWHxPiQHMwoh4FQkVwSnfR78lZg0x+8LNarez4W1WSrqoFOxyaVVNBARZ7CKXz/TfkABhnPjPGiP+MfFOHxkI8BTKQJxuSarZNiNKJJw5EKgZT/pXgUiGQrxyq0CE2h8FIi+ZA8FeFxrvg3I1hgSIwQPBVRzMNiAODAR4CgeeiuwWHIUrwjMCqQoEOBGHEfwI5UUcRmV9HIQR4pkQM6aEeMd8uwqMslMMe6UPY+BykMebyBLc+xFAHJg+gQXXCLfD6YIydL52BQK136Y6TqPUfhMOiGedZCWJkiD9RVvTZ+/JnfyP7EAnr9FInA7aKeCcFGbHQH+PvFfgP3bVDxjahisNQLgrBoRWsqMNMwJcESN7BKUKTONR2BjgRygv4miV9XHkBTJiQsyYEgCi8/wIBcIlfDkwMqiGE9st095qP49sYXBvhouw4Stba1eGCWr/gCmBTeU93sd5yWsjOblZrB+eIeHU12yGcNXDuX0QY+gvpR14Yh3EVY8qGjjVPathGsRBeQpYq8EOEa7IQHcQSBWYuA3wKk7+X7AgnHirnUuYEDOmRDwjqGZcZHDRg5Why0/gckx5Bgtn6c0oj8MqSyQAMVkj/mjPvrBx7QqEevOYAyK7Jd80RCWNcAHQA8vsk8CQ8EBMmad94KDLj+F9Ad8G2SP43sINJSD8jJClic2MZwS4IgIEpboICIcZ4VtWFpGyKWImRIEp4fcI1KxABFYGBjrmcgQgnO4RlmhMDpwTsYuYHc5vubHdWnsMhLbi/C1MWjIA4dC6YabTEANh5bOZlpYAhJO7Jid3TXxvIXdR2V1TDkQgTjIQVrgiAkSUCsOOtEeA5oKlafomrI9o6HgdLzAlIiCoZmv84mHByvBLk+dy8NLE1aAmSu+uOUvEDlp/l/VKa2cgbFiaYLGMSlYSNxFHK4wQJcUNfIdP5ENwNRxPW2xdfpkYwAXE7whgCcZE+jsCu0r4HQGegnC6afMCM2M8MRBxqixNV7ryeBOWzdoZZX0o+0KZEAWmhP8dgZqtkCyFnSKUSNokm6i2yeKpJWvCZm2sskS6AdwOJoqOlEw7UGy31j5Y4pagdu4rcTrGkzBLspIDNmvKwa2PaxSGDeyGX/kluZTmkhvAf28u+T2lrAhnAkNizpT41V/WLuKI5y24zbXPf+3kJb/v7ZIvByK9Lw+/zN2n291yrLM26rOmH9LhCkQFogJRgahAVCAqEH9wqCoQP2SoKhA/ZKgqED9kqP6jQPy6WV9r+k+urQLxQ2qLtDJwBN4BPy0d9MnL9PB5emZqtYydMyBUzaMZSm3M/c94KMzKFkXDKFW5Hqfb0ktTJRYF8zdG/0TKqQmhtm78ldpcVhvYIgnnw2b9LzxCjfkoznitjND9INVgwWqwMNGKe0MrzhkQTgyZkVAWGBngNERv3md9aCKuh7HdghxFidGBF4T90A2aL9Tm7K/U5rLa1PEVd6LJgJj3MfBRrAeiZMrQea8ZPz0XD1WqjZEzIPRvs2lGEKfBOGfXgADXY/JsZ0DE8iLxJaRD56Kh6zwQWlsOxGO1uaw2OHoemREYkKAs4vS1WmIa3EavLMG8AzdYEUQgF2UDqZuBPivzI2NAuOAkZOrVHUaGcBrE94uXeaEAgpzyLjK4HnIJREockyUHUeeQ//jVW7bFTh48g1rYSzZ49Qupzdhfqe2Q1zao4yvqsbBOchbKjbU+IJoBPgo4JQZaGeSu+wivb8NHLEAoW4GbH4I2RsqACHtEUNdYZ2SIK31UKSAnblRwIfSlcHA9mlFfYlcljkbcmPwavX/NXfgQk9OZOsh8DCo/GFG/0NpwDX+2tmNWW6M7TtRjZZ3kLJQPaH2YwEcBp0Rc6dYAiA4cCeZiYGmCDoVBEfhVPfMjZUDoHkGf+Xq7w8gQcoms18TD6ITpAf5II+8iI5cuFarEATsoF2lqdHKm4zaIJ3EUFgpdtR1xKgYT1zb8Sm0qaqK16Q4Q91hZJzkLhYDowvh5KkMGhBkBBC1D7poCoc75oI0xZ0DES9M9RgY4DQoE8TBITUOBcLJHgOsRAyFKHLK2gthmRrkKRuFNKTeR7uEG0qnBqq612S+tTYGIe6ysk5yFQkCYMQBx9EAcYyBiDobjWRrtEcJF8NoYOQPCZZv2HUaGcBrkrgk8DCiAGKV3Ba6H19dQJY5GJIIaz+FQy6GdoWxE3oUctdlFtblfqo34F0ltCkTUY2Wd5CyUDy9DpHwUkSzCZm1ZccMrSzBjgbbY6QaNNloDtgL9juhMACJlQMR7BE/UO4wMcBpY2YJzY5O0/PsEOhr0OwJcD3AJoRMiShzTZmqNbLXXsL1Sa8osAesE6h9gUfjabr9S22FMa/MslajHyjrJWSgDj0YzBj4KOCUm5igsaWy4hc/3jjUigLPLv0yDHct55j/w7ulguN9Qm8v6aGbf18b22451RsZ/8RGHm7FO8u/zEn/8qM+afshRgahA/MDaKhA/pLYKxA+prQLxQ2qrQPyQ2ioQP6S2CsQPqe2bhzY/E3/Xz2t5fq+131f3bwatAvFFQLjwr1tSq4gjr4SjIfVkp7wM8eGawNTA085TYG6MTQQEPdXHOeShB+N0BqUSC2zRXnzgSC7WpyRPdmyqNmJEsYPaCIyOPBJMPCI+Gsv8yZ2ciXU30rpP8t04PGku6o4sADEsqVWo1yGrid5IdcrLMKOKI+gZEU4fPHPDxEB0zEWwPk8zdPLa8EylowyEXByNWGv867zeQS/uyZDa+HYDkSCPBBOPSOPbmhFxhox4MMzqHuT7CN+LtVlnNgGR5XQLgUcABFgNjVPPtmdq+EEO3wMQzBCJ8vAr9sazI8x9IDodyIiNkSjciwK+8alNaDcGwnwVEEndA743ztlcu8N/KGlaIDjHglqFg2w78h0khok1UYyQQZhKQX1iOIzhDWj2cpl0aSJigvMDRBM4UBWg0hFHQJlrcLjBR3LB7BgICI4JYyQHegQuxSAzD+16FkkaCQav+oYRYRkkBw0BGo1BoqqI8ESiu+HrPoZ+T+4kDwR0OLwb6mqMKWla4NqA3kWuVnHiLk4ThvNNfkZ2cJ7GSOJglNAUXn2iGSHzYEb9fsj2iCkdXeI8BCSdARBQ6YgjoMw1OFgFgd/wRhAcLOZHXu7wl2MAWOFSOFXsOFqN7OXLJy3FIwJtHgXiaAfmgKgCSKq74eu+hn4frRMg0CPxhjuJxQIFijz6R7OoVsEzYvK8dsLLCDInPliLVSDCGVVO0u9kfgyE0S555gbKhFfh4wgocw2OLkRyiWaEEyC4+xwVQ7gUfkYwmREzYhYJRnknOiKsNGIUCPLxW+ZtNJ79F3Q3tO6438YDIS/3M4eEVT6USFiI/jGsqVWY8UOuk6MHImhNGCt3O9EZ49fKwNzIgQjraRmIEAFlrsHhrhrJJd4jjh4IopQpEF0sFBHanUWC4Zb8iIiKlSyXCgR0qcAIiXU3UiAUGOgyaI+Iy8HRWoyz2dIURf8YFtUqeNJfr8RYoDU0B8JEglVefSIa5PCdPmtkkRA9xevW+FqtySOgzDU4VMbHmviuabJO1DB4ZVVY/F1T3G4UbidpKeN0COeQ2qM4LDwKOJPobmjdEqQDejlccSwAY5Ujcsw26yj6h1tSqzgNEiLsirnEkUlGK78awGrQu53Onwn30+H7dEvHEUjoXIiechJ2hDA6IiBCBJS5BgeYF6fRx0XinjlisEPpglghI/rSjfo7Im5Xy4PN4WOtSAvdIJqFbMxkNUf0sszbkDOJ7oavewj95nEZoqWJlEmuGiIuOh6Nk+JM7vrOfz+6O7+sdUaY1VryX0/LGhwlC0s1rLXTFFrKSdXNrI38lnZ7n2DNL8WouQ/EdzzieCS6iVut4d5jh20tldvY3qevjdbyG4H4Tmu/r+760K8CUYH4E32qQFQgKhAViApEBaICUYGoQHyjgfeB+y8dP6pv/19A7JJPG/sWmAVO/8WHlNtQKuceYU6YdvqPj15M61vNs397fz+3j3S1N/tHsrdUonS8XF4u+8v90rt7edLj7HtzJiBeLnfatJ6/YE2ZG5ENflLWjI8wJ6aRaNGrdvrW9jEQL/0SEK1cVW1ybjdVFl91bXY1tlkNy0BMNbV9e+fKntLb9g5S2Wfqzc7P9tBCW24zSGWkQARuRHEuSL7GPcKcmK6NXoexBETfr3Ux72oMRJpqHgfC3J8R7b0M2efLczhTBCIkQ/2iEweP6mDMuBFyiYOnAGYEh7NnF/UjzIlp4M77S99P0/Q8TdY3AmI/TdGnC4BoOZW+4e8Tp9Gk3psnnsx0jlKmz9OZnaHpfaY6zIt+Kue+7F+Nofrov76XfNyyDsqbpFJeTZ1KGM79SgNI5d8usBp2wkJvJZfEZ2NeX/dspQIR50zbJKc6uxBPLJ4jMUHm3AgBgt17wleA7/Ywwhm/lTnRwgC6OHqdEfQfTfp9T0BxamvkbzulPbe4gugvn5pS6DOd2fGMmpaAFksTferbUu6pvun6fObWpjy9LAxT7ZT75UKD8k/bR+mUSsfURo/SLxey559WrGY7p/KRlSgpn6f6yK7nFkBMw5/0J26TtSjgqe1uGvCiwI0QIEgro1OHvAGtxLlHmBM0XAJEmBHm5Y1NC0DwskN/2ymtb3Uqt5yz5YEBpFN33qgz+zcAQZ/2xdxUD7U2/Z1uC6ZrnEvu3wDbk8GgvEyl95yO1BZ7UY/SXFsMBN9sUC61EiV7DwTZ1QsQvYlzpm0yIca7zFUHY8aNkL0Eq/4YAyEEErOVOWHOvQx1tEdcsIOXgehNDESPwZAZQUDoVaVAUH2l3L0BEL2BBYavx17mT3+WeqaFfUpp92ekAoj2jNIREM9qJ82xYCVKKhBntisAEefM2qQwLsxcZTUMu8CNYJUmjiviFSus0kqEqbCROTG1rouOSZamORCtACELFw9ti6Gh1bltw9L0+to/7/iehj71xdzx0jTlkaXJ5+ZGdWmazgK2Zw8Elw5A7F/VzmkPeg5WoiQ+MxDPsHAHIJ6T/kRtGmJnNOAviEZEiRuh90+sXqF8hQEKGunviHvMialPvKHtLvo7AptjAIJSW5x/pY5Pm2JL9+8v/JdytjQM0z39y+VJ7sWnheb15bKTT/ti7mSzfqVcvB2/0mBQbr/py1aNVF4cL7AKQOwvfIvdqp0951IrURKfub5Xqns6f+al6TXtzz66vciOMp/AFVLK8oSufLbInPiVX9a9eeR4LDeXaLem95tK7n73UwNXPFtmTvxkIO79zg7p/caS9VnTj+x7BaICUYGoQFQgKhAViApEBaICUYGoQHwhEP/lQfhvIlaP7waCXCF04OnJ7jw9IXyLeAtrj8H4CWZ+UssuMS36wqf0UIuW6phcKhdyOuymh2q76RmpgaOInDqTu2V3fnnDp/2URv3Bs148AD+3xKN4pxyGnuG+wIU65X45x9aT023nLXk5I+/u/P625+eqL2/nNu/ry2VplPJjXwKiT4aFHaqetrBbBMK75VvJF1z9cma3YEfv646B2BWh2hV7w16DyQFJXrWWOtXS/+kM/UfeH3wiLyA89b06Q8Vpg7Z7ceDwfwYEhtb3/Wny5vmx6ZGv7ZWF0TNXI+3rfCyC6enZ3ezqnQEB70xfwHO3AkScsc3yLwGxPCO2ALGb/nlit+OToU+m37ETjhx8vdFPT+zNQ4ln9lMXgOC0XnriCViTL+spACH59C88iXlf52ORfikA4d0g7LiA2+SJHSxcGTvzmPNgwE7g/OfAXdh7fgSzMV6nfOywgWORGA97Ax8Vzu+FBQEOBFrbS23itjFqA4aN8oOtAV5FXMsTueOnS3KnHKfzjkfYnOm/afgv+mnHTkzU+DT9twTEkwBBLqUXb8eOLVsHIvSVvNL5WHA/X9E/cmdhqdtzv7huZSzQjHg5s0ORrxcAsQMoLU/IZ75eWlxZYCNQaeVHkMcJE5dXBV6BzeRW3AkQet6o871/Fv7GDn7fFo5MTHkwH15oUKf8z61fCJJanpkZwDsEU2uwV/SA5kK10M5B3rldNCP61/6VnfZcgnYE9uex76x/fTmDv/FyAQsEQDwLGQz5kL/nnWPK3/OuEPU1HwuDufaM/oHF8XJBbQbjGBgLNCOIY7DHsPWBbDIh3CvSvWxbYCNMud+UH/Ek+fbi2eWr5C0Yh/NUgn2+3MKe3fuou/VX5t5z4Kar/Ez5wdbYm7yWaXM+g4gA/3XLexlZQc332Cn4k7rwGYhnunD+mZYrPyNkSIK3GvwNWIUdcu+XJsOjEGbE5P3N+jofCx67Vvei1t9s7JhHQnV7xgKA6PmMYXe5QWe4KdChwIEAEL3Beur5EVMte1oa2E8MV3uvVwkx3i7KyeAhvDDUFwUCPA5cMeYSA9HrjCCuQ14LaDIY0FaWpxaDpED0cyBacdqfC0C0fZi/yg/FrcolBiJemgBE6Otk0WwsepnnCgQu557tfqa69RrYKRBiZnuO7gtaBuI5YIqGafUjpgqWJuZFCHuCHPac5xnGYQ9pZRnLliaZ+MKI8HcvWEgo23TtXiJb0lroCvBAnHFFTv8jrkjLTI2zzDPqT3zX9PqKRS0AEd81MX+j1WsXabzH9HpJhrsmASLu62wswA3R/umU4G981ihjAZt1K/fZfCN23p91gyRWGuUDPXKPqThdzeBjRJs1b927SytM0f3rWfYKruWC9s4tOBpPwt/A9iuMCH8/vz9jQlP+3rxcvC1JLfSr4SzM6pfLmfMBiCkn28+sEb7zpxrD7wgaLNolmORm8t8RxLfYiVVGWt/73xG90T0Fy0ve1/lYcI9ee91f9FcH5eGz/marxDLYLdxU7hZvOiNuQ1s+v37c/z3xK0dc4/tb+fNW65bL9u3yWPTr9e6FJPtox+bVBgZDymW4++6B2fp74ucf+8vyWPRbelefNf2QowJRgahHBaICUY8KRAWiHhWICkQ96vH5C69elP/XQOA94zdwJnbgI8jjj/wZUR/9S4/liSUBdkQf1ZZ8T9phRgTxLtjhPj3llNwLrIfF83rwi7yX+JvWOGdHBJ7FPhpsWJ/V+kZp9Nwaz1LT8vnnxf7Grum2bNMs+16eysMDZuCCMelT03YGxFPfs8cAbqISELuZWU9UJb/CR29Vau6YGrCLqimxIVLFjV2kj7GLkvMnx5Rv58sACPiM0hbYidkTELvz7rzz3Ai042kQM05AX3pWnRm9MyXbkrafPBD4tOvnOM+BID+Uvn/cJ/NreUZQGaqdVCp6X2zO0WizC2EJiBkjoi8DEfMsYiDyFgIQPNsDSaWgtDEfmW1AlOcCO3V2BhwK5Ri1/V5ZFIkeBj6TMgXYDU/CUXiBZoVnhGjOvfHcDFG3MDKHXl/Ja026HDDxSdQxSGGDcspruwU2RMzigFdYHEnQ5TjDWnXbeOYJ8572TCOA2zRWAtmLYwi9jGeEKmvs2SfJfBOvyTG5dPowMspFYZ91pueBF4t3XtvjKbxqLL0TBgb0Ip7ZYxWRaUAJivQjnvnzdFWLs+8ZDITJMKQYcrzyEkDfVYcipAA8ckWSW/HlvPOLz7OoY0BwAdyQMhsiZnHsoGgRueV3wi/Zqb/dM09e2N1q2gBEeN1eHPrSy32ve4RyV1ppB3aqJgdem9eR2XlVjqzHzN9ADsOcKq2F65DeCQNj52cfho54TB6ISA+DqQLti2c3GJEgacUhrowE5FQdijQFRvzDe0QrGxj0BoinASDADSmxIVIWB+8RTB+Aj/jlTa3dicrGXtQ3dqLvRDbFMwLaGzKz3oICgvF2oe/QEIGdxmgrQXdDmFNv7azH+zf1tu/kc6gl9E7W+BgI+GHNJQYiKE2w4/7sd9kECDAVlO2ghhuTp9AAnFuRybloh0nvglgPk2Fn1FZihqQsDgDBO9UzO+tNL/ySncz1aTNi5okC0XtWSVACMRe0EEtRBCDQd27nWWlqYgPNCBMDwXY/pz1WssJzRIvQWkLv/NKkgyZErF5YFJkehsjt9MpuAAvUCN8BjCE0wjmfPRCSIhtwr3dNZ6F9CRBCN2PNCu/gz9kQMYtDgGjZMc89OAu/hAYb+ZlrIpynnrUxaOXfzZcmXkr43igG4lmtarkUNJYwJrRHhJFB7Z6/4nvMNTxjj0BtoZbQO9msmaVwhoYFb7Y9ttxcD+Ppgs/KbuDN72yYVSeMkFewHTin6FCEFPlt0gsX+8wMi3Ng2hHrARTOXdisMzZEzOIg1gQ2a9LGeGHOBvglxI4Q9oSwOEChJJvo1wGxVnBZhBYYzEsMhHJXdlye2H/Qz8CYTLW8hpHhtFfPX/E9plr5kuDaeuZAai2h7exWMP60fLN173jM1T9vZ4kBssYFycpE5/pi7t2dX9YlZoef0eW72DtW9sVcW/ktf+RYYoCscUHStD461y/kfvwRh5/Rq0AsWdkXc23lt/yHj/rQrwJRjwpEBaIeFYgKRD0qEBWIevxfAPHdRtcrsALxtxz8dp8MFB6EKRcBbAhmPLT0XHJeNn66wk88F9+c8UD02qo2H4dYmBwhbZae2QoVDRwltohavnTso0+sq+yN2i/0Kz12F7x5TW/s4clzD00Q0vw406d3dkaRG7ZcQeiMZ1HIM1gugmf36VNZOOHZvyvuobSGlMmhrktN2WVAyPdzGYg2AmK3AkTfhxbnbJGcRZE+Rt2Z9Al028ZAzN/oKz2dxkNw9tf0Oy8K37J3h4yTN1vl8l6fAdlf1ne5BwQcebPuZa8m9ssv9fkZcQcIjVCyCEQbD9ecLbIGhMmBMCUgdiszgokXkbPpWdX5vTMoUj/IeR3CjSixKJhzoDMC5DOwIIxnQwgQrHYRooUElkJgcpBGBlgVb+CBSB7ycj1d3t/BgQDPgSM3XJT9gIWNX+RNIpTk8U+Yk9GHFudskb3Rl5XfREVE23mRUqoaQnnw3rfqguzUonPcL+WuSE5y9p7FrzgN+VMKRN9LvAv4DN+8qglHZml9/JGcRYF34nWPwIzwL6ILG0KjoIDjAEUNeYG9T5kcQZGDZfrPmmdqdfIvcoAPuW6exf3SC/sBVyyuoDhCSSH+CWl3+BxztohwRqYz//hQCb3YbKIgCMr6eLk886L73GrfnzOGirBahLfBeiAX7BHcl1deUc7yPvf55awMl77FOIiqCaIjqIk5iyKOCqUzIigCgA0BtyLV8iRqE636hvuUybHzLAwy4CXkoaOVSCtRm3upLwciRCiZxz/B6qc55myRndxeAIi9LD5gfqCUqoa8SHyWnuOz+BHywiiaXzz1bOHeIMwNz0todTxjRkxzANEi5OV4I0DstW9CDHgusyhSILIZIWwIAMEsinMvQg4oyXIJEZNDlSvMRWcE8kxtRUDQ8vXMLA9z6U0KBLgfQVUjj38Culhocc4W2RkjijCIuyJxW4QKIEA8o20FgvVCohmhQOjsDUCgbugO9OdgBffjHPYI0lQDEGCweCDA9JuzKFaBEDYEgCBJJKrs6QL6l7IUYibHjtUx/NLUa57eREC0orFkvGbGM0ElhC0TRyihs3n8E8wIzZGzRXRpovR/fIApmcVS787rZnggnn3wmxbskLhfT0LCiVhPrc7yBIg+7BEM5HTqn9aTagCEacssihdR5Qh3NTsTbXFgQwQgWvAlWo50ApZCzOR4YU0Mw/FIWPFC8jBxsaWIVMyL6PcX9TdDvUIZ3/s8QomZxz/hPcK3mLNFsOUbTkfJnWzWrLrBpVQ1BKwPXppeRXEDSh0G26u2ouol+zP/Fe2Sl7NyNYyKK/FOBeIb3y5Mt0AI2QYVD4nlM/shlp15f9v2y7pfqaN09F6ALq5/FqGkXHbV4s8f6zXlv3+CKmH4Nx25O63FX+Y8gvmZx4HYwk7AjLgHxJbj67gQ6zVtu0i+8ajPmn7IUYGoQNSjAlGBqEcFogJRjwpEBeJL66hHPerxnzqSJ0Tx5wvHK+G31jK9jflx5+X7vbg6Q4v3Y0ckFXBsF7yOC6tg7X5Dzz7zPCguiXgsPnpLZFWmyNEuNdX6EZ2evp5pJFDy/a2fNzp/fpjIlfd5O7t5iIpzeiKuEQ7NEhCtF31fqZn/Ss6IMbJbEXpYBqJdeDtwsWQrb4j7F5RbFYbIlSDO5fp2YUShV+6l3wtAmAyIXQTETG9jZngJiFCjOEz6dSB2yzWDA4KcEWPks0CY2UW3XBLskyf+ytFbBIidXDCRDatABPZGiDiQAZFyLZS1MDlAyOP2bESk34iU+hNzhUhP4iWwF/BirI+58nQJLBAyjuqaajpL/JYQO4X5Ez4uy3mpZq7h3IO9QV6oNvBI9j46S1AM2XvHlO+N6IeIi8cog2JDSYnHYtiNSysDhNUnJ5A6g85q6W6pvnMMBI+rAAHuDFRQMq5F71kL5C8lP5TpNVDNs6Sd5QXwVHWCnYHC5AgsEDGin3xtZ/h0UYdEY2lb/cSmlmuGitTThbVCqFu9SVQ1+LX8SDEECwBc+RIfBi/pi0oCXKX+hf7VkhqPhVreX5QSAUcx/Jo7b+luqb4zlKdod2BfbqsBc56Ff+JlHQLXInAVdmfEO9qdn73MA5Q7/hFJBNo8VXUCQCDKSmsiFshZ65IN7633rn1ELdFPdJRrxkwHQ4KH/xx4JHszVwxB4LOYg/HCPA/Vydgx/8NsKmkkDAj+EhB7zylRJRG1dLdU31m1pEDANEJCAHB7XcYyroXnKrQSeKoVl3jrdwsdLpExkCAV5kxcCmxFgQXi6Y+B4xbtPK2vNQUiq1m3SgFC2BfgkVwKiiEUh8XEHAwKttH7OC878D/MlpIpEOCXBCA8D0MuunJ9nsypbCrP82NoLwJEyrUAV8HTXxA/zOttaCySViekV52gpUc3ozZigex0U+pTVgSGmNakZ3xiRsNCzdFM9XIpwiPxS1OkGKILTNxayxQh1skQOg0IP/dKJswVjEfbStutXAxqKe0ipfriu6bA/FMger0JS7kW4CrQJkX3zYhqEvQ2wGigslgzg+oEta4CViwfIiyQVutiIvf+oqwISGK1vPaL9oZZrNmAH4GNGQuX8kiUCRIrhuiWGzgYIHOix2SZMig2lDzH5EzT43dVull7S/tyffHvCF6I5VZhJ+2wWUXNCbNViSOU7ttlBsS2uvqVmh/RBdGSy2oapfaWSi5HlUkVAtdH55Nj8lkexffoSnxlrVX5oh71qEc96lGPetSjHvWoRz3qUY961OPbj9nDurvPZLISFFtF3g67eP2LezEVRLlifykrZkSqH3esiZkdacntvVh+vlZu/ROxXhK9BDoTHgruU4Mi8YZ2acx3RSCeJq8LP8bmx96if7EQZSSJICLPbNP4KtrErmBNcSQTLZDIyoeBSMUh4mAibcZzKcV6SckXs1gvCRC58scKEH1bHPPkAvJmTX7kJyOh71X/YmVGtFqJeIxKihntSgDzuLKdSbVAfgUIUwKiOAol7ZHcgpxQswoE+AfgLcDNwu7AFsVydgO5U3gZEEcNmBBc15m9WM/UluhfzKKMxBFE9hDhoQXsOVPMuKhw+s6re4Mdoa4h0vN4C0yJM2uBPAsHhBU8gqaG2v102c9YGtrvp0vKZYmjq6B1VRgJGiLzWC9PEpnFa3fksV509ZCIL3ihmpU5SLMG/APmEbRtwLlHPKqc3RBCGqgCi/GsN94jXskI6F/kUUZEt+J5J15yI8oV1K1YMYMds6oKAjmIC9gRwcn6D782D9s4TtiZajkLf6L3Ej3Gl3lWl2kfM0TAJpGADJ7LAr9yq6EZjMZUQc0aUyWP9fKsGjTK/shivfgZJJoeeha1Cv+AHZMSxYSH9Yz/703KbtizygzzNDiOCQ/TW1j7wU/4R/eIJMoIYq6A6xArV5g2VcygtojrEYAAf6I1IvNgAIQyJaRtITX0XtfAQKVD9i44/rUfre83WCcpl2UvoxGA8GSIXjVE5rFeMOB7o77uPNZLWMpkFII2Qb8L2kGtd8vDUFnRLs8Ju8EgVBLlu/RJcADjmQ6qf5FHGXmWmCsAIihXmDZVzOC2WgHiOQVCRCMwI8wSEBSlRDQ16KrXqCZRPzzvgq5HE2LFCPSXVhgmZSCYdjCL9YIZwW0WY70EICTiy7MGzpGlifkHpvVRTHSDwlA+XcDHCEtTy3FLMOX2ssYbJbf00T1QFmVEdCueJXqJv4ZyxQxu6xmqILTayzD2SusJS5PEPwEgPTggGHKvqfEMpmDEAeljhgj0qiSMVaTi0QrDxLfeCislaIjMYr08XcKSXYr1wkv32fiIL7qTCRDCP0DglotuKTqRiNGxY40J3fRemCN+FvIkMyFekfOFGQ34VUD6F3mUEdGteJXoJbLtUplUMQMaHFAFMa3EdWF2RNiszy0UPXiPOTNv5CwiQWyp19R4Ne0+udmIYpm8Is4JWBYxlwU8DVboOOuWLwojry+B55HFeullk1ftjjzWC9dljI/4Ylps4aj17h3eOqOjN9tuGhdvZu/caAZO0PJRjsiyNWJJ/2DdcZ1prJd+pe9aV2/SFeeB42vUJvaX36mxETNK7tXwK1alsV76B/rQm8ooqUc96lGPetSjHvWoRz3qUY96PHrEwiePiqDcz5/m+HNha/6CgDkViNWW53wHfqKZfItzl/gR57ac3kdnW8MBP+T8+/tpMKaLK7H6oYnPOiSgC4Nd7IZFOJFQP9t+LvSXxYTpWfXaE89y2rp+yNbYMwvPpHbJZ/bTJO+stZlTPo23gs/nNk9Pzd3xfxzww+Cd4fd352jImwQIjHMCzwBgAhBNDJfzCFquvQ3183uwPr5KIANwvAuOE7McRWaJoxH6U5LN2Bx7Jmu1AIRRh9mSLkYERFxG45zcBeKsBkxA2Oza90AcZkA0AsR4LMyZAMRZgd7JcORMiwAEMaHmfSzRidrNM2Jz7JniyErmvbzwSm6fVl6G3cvL3E/MmlC2wU6mruhZTOdeLq+v+yz9SdPC2TMp4iM/fXa2G5vp6neuGw9jN3ZXY07TXwwzn5nOTzlGM3QDgLhep7OUNtC8OYyGc524NEcpOof6RT9AmBWBlRHPiFIUmSWORuCRBG5KytzYHHtGnGpx5JlprJUwdm71nX74jlR6YfIzIdaBsA3CJJagAVO2f9o+S0fMkMn3Gc6eKeBHLx5XAsJZutadc9ZOn92Rrm2dJ0c6M51v6MxAM+I0QfRhzXTmaBueFccpO+cyVJpq30X1t1GsH7Ay4H3f97pHlKPILHE0eo57ok7TwP0IzI2tsWdUGiW0xi3IjHhDrBIsTfRpL+5ScBT+8b7VGAg4vgkIYUr4dHKKT//FZ3lGyEDxjDjdAMTpZs3p5mwMBKXSeZoztDxhRnzY7sbQMBDT36msMyitESi0fnA29sw7ASsDvuldr/cui1FkihyNPXup90bZJcr9iJkbuDO6F3sGvI64NWbP6MD9I+EoIB3CbAQTA6Fsg2RGSMen2XRJ059bRFkJZ7FHID/2CMervx1oKJ1JgZhmhKHzEwhjvFk7mwKBXFSaazehftkjLsQ7MRJuYw5EKYpMmaMxjUfP+hwause0JmduAIi7sWe8+oi2xuyZHdzuz6+vxGFowXCA4I1kymcEIpxES1N7bvssHXGlwG2KgOhFRofvmrA0TUDI4jIBYRWIw4hFxwzI5YE4EgBNAMLqwka1n02oP1uaWvBCCkDMo8iYEkcjWpqMLE0mZ24UgZjHnlEdEt8az7kXYWJMU/OV1ZAM2AzK0YCyxLlVtgHyez0LVtE4t4GNgPTeKJtBz+J3BPLjdwRtsyfejq3BZt3wZk27gaMz03njOHVQILrraYKINmv6Sxv9tOXLZj3tcybUj3kemBZMarnEQJSjyCxxNCgdm3UrEmKtyZkbxmyJPSNaHFFrLfzXy+7z/oGzaXopzyO/rF3yrcny6+2rc0s1rv++XVfpKHM0to3Q1tgzpXa/5fhaIJby08z4DBClsykjo3xmSz8fbfkPcDnqs6b60K8CUYH4fS1XICoQFYgKRAWiAlGBqEBUIH6T8fWoQPw9R58DMXu80q4+BdzNGBH0dh0KIj1+2uLf3dPvl7y2e5bqp3m74HTsM4v7eQ/awqfYsd2WWg11zHku+23aIDlX4Q4Q59yaEhChzt2MEZF6z3fhJV0TFDGW2BQxdaFdAGInQORlI05HH0Y3BiLWvGhXgIiVM+ZAzOkVu9WgIUkMma8GwsyACGf6NgPCtIXBhPl52S1AmASIvN0NQPRFIMJ7iEm9XwEEHB4ZV4GVM3q8pKufny7v73CL0PvBUO0AL+JJhH5CqVhvQ/1X6RnhVlwQf4WA0KgqbxTKnlQ2xIlI7su8tifvcAmlShyLUrtPooGBzqMm7YFqkUBnRFQ+XlE/IsOInscZdWp+5XLIInRRBozwRhJui/R7OvvixwsIULiUnKtAbkV5bZ0/ky+3nbytvWpI8JvgUCqQSBNxqbZN+Q+7GSPC/6cvtV+kdfKMn+lfzAWvZpHUpk5Kb7N346Yci1K7z6KBgUFDP7QHqkWCS4cYHy8U2KftRfVDX9NXxYBnyf+syh5GR8KrLSjPI6JUqDYIWySKOpTe4k/MVWDyQE9XyT9c3QufYSC8hgR0KjRCSVqqzfgPuxkjgmUP5C+zSNpWtDH+YXbDP21YlJ5mtSl/AmdQqsSxmLf78pbKkwSVEHkZn7VImPGhYTimHC9vonGASCpixV5LS4yWQK6kll/eNJhJxm0xFKcFo6Tj5YGYcRUoE+tVAIieI5FMS9MZGhLmjGtSjUKMkVCKzsd17maMCESdglRDAAIKG/h351U48rKBPyFnhNgw51iU2pUZ8awzAtcthFa8FgmxV1SBhG0UkgS4IMLKoBx9G4Aweod26UWrpPUiMDEQsPEFIX3kroVsbKEylnAVWh5SjpvCTdGaRDNCNCTMmWJxIaCRv56iUq1J+Q+7GSMCpC5Zmvx5+s5APAMIKjkvu4tjlfhSJY5FoV3ZI6ifwjTx6iMaBYZqOIsCCatPPfei+nEW2ZSzXA5auvfBqSRCiy5NbTojJFXitLQysnwWWmczrgJYPsRk6PfMB9ozA4P1LuhqmXAkBsLusvexduJSrUn5D7sZI+LlHDZrlNpBteL1TEvTK+tysAqHbNaz2nYSL1JLlTgW83YxE0WlAwsFMT6kB6pFAp2RHpz4V7IRqh9n5KFU5rj0kp8GEywMidAiW/5OeR4RtwV9prxs+6tGdWH2zBauAt2+6lq6zuIAP+JzjIjsZrSNtrhZ2ZSHsRy5JFfL8Ko7d/pxXxiij25Ot5RZZsT0GkVUT67xCWhGbAHCLNS5rZX1Oj7DeMjVMmINjK8C4lePh3pVnzX9kKMCUYGoRwWiAlGPCkQFoh4ViApEPb4CkK8B52+E+OtHoQLxdwARP08p6WnQW2/N2DxQY5efuL2/W9eM9KqvMacbn7Ok04FPcVZ6ty75PsvF75tavG/kuDU9s1iHthjO21K+tJ73d3pvrzGn6POnjhvURbgncwmIGIg0Qkiup0G22ZIgQ9L7eDRVfcP671NHyBp+U9RJAf9unJ0NBpfH6XmuHIjGn7FLdWiLmbmzfEk908WDd1uH6PPmMYhHA0DE7wIuAGHmQJyRjz/TG86LQIRBb1IpE8nViMoGvbRuXFMalgIQJh2cJNdxIxBmIxBmEQgnQDTh8+YxCJekB8JiyRF+xoxdIPFDlvU0FAi8ZjstBeNhpEl9gELG0A3pJ9bWMF4/YzprBpoRjrukL/V2V9LpwCcqYwxq57NXKn8a8R52mmtqFzlGZ6DvQUAc+AxUPgp1ZC0e+DXjUltxPTr4Db94PH0+bhuDwVLqDTZz73GGWmN35HOrgQQSdkHbnxFhZElPg5qjwXX84jmZf6SX1FlJA9dA+ESvpR+hvjGtZ1Y7QlPbTcPIQJwGxy+v87/yiSvk2vFmNZW3XNssl3FXKT2VOIpKwZFepbdWrvG8Dm2Rz+Pl+WMpX1rP+ztgJlUE/nzdNgbT9T+lfljUTb3/sLB0Kim+3badsQv25sXEEQ7mehrUHA3udDXwttex4VDS0Cmtn6CYwUCYjtU3+PSAWjBxnSEFDjKLliv8pdLdjTb5A9XgyHzLYhF5rqn8Uc92N2h9YHhDe7M6pEU+b5wHbN5WXM/7+1EkXfDZbBwD0hJxExCd7goDzlAOUaEAIyFlF5gLZsSynoYx0JiZqh255vEoRji/tuonUsyQGYEO+83aGOeB4P+o0/4v6jO0NE1LWEczZ+TBOea5jA6YPyszwob2ZnVoi4MzMRDztuJ6VFsHm7UTIO6PAc+FCQgz4g3xRs7wHN750C0zdgEP/9NlWU8jAKHT8jBai7NWliH9ZDlVzh2tXFEKhFUgsNS4aNGRK92gFS5PK9G0rie5qGt+YZtK6NI0rcBHtMc58jqyFmnQi/mSegIQ+Ey3GlvG4Hol6z6sCsLoGSprXiRAF5gIKbugb18ucG+X9TSwjU4T+Kob1dSAbKsNb0/hk+XUk8hcNbpZ31CLAoGtk3Q6dBvmpenqN2su39BZm+bqwhY7/SrprrJF0sZ9lfbMvI58s6ZBL+VL63l/7+R3hHweto3BtHxNLQ1oi3uPM9QTU+ZVLImm3ftNSZPz31u4QUs/udkN3KO/TJ2dfzKFukvn3d061vLF9QSbXcH+9THQGvPbWXOPRfA4ECvDWDj3GBC4CU0/mQfb3FpHms9tHgX3UD8+edRnTd8zChWICsQf7kgFYq3GCkQFogJRgahAVCCWaqxAVCD+BiC+symt+6fD8Xn7vrBnFYjfB4R/vDVEHAZ6CgmewuRiGT7JU+AjfbISfwtANOy3bqTNmO9BzzJn7A9+hB+e69jS87vo3FL5ZBCW6wDXhHWZm6wH4uDyLeT8D/RMWCp3e5YBETgMolRv2YxZAbuAaMZhkfq8g8jaAhCTQdMDZm0zjpxibf7s0rJMeABi8sy4xuQBPkx08ZTKN1kflusA14QsBE0g7oHL6s/5H+gZWCqberYEBLtuXJmnYGfdnAMBn1wCREKmUSAOpnGHAhDK+DAbgDCFQez4mlsqPwfCLABxupGFAoTJgUALjfJGoprRM7BUVnuW8iACq4BJARY8BWjW5zwFOE4O4uQAB4JdMfY0pmnOxGnOhLR4abKyNGHyN8Ld6MRcjqQywkl0kgXCucMIBxA62RnY1zFTAq7Myek5LJRXpgVsWakDXBOyUPlXJylzY8bIjecy+e7gBor5H+iZArrSs5QHAVYBcRiOiNTAPAUFIuUpwMOrTk8wHfiMs3maidPID6ZpMRBOgGD/tEWNZCgNs7Ih4MQUf97kYjxacCSMRCNy4qT88D5lBWJeXvkVnkexWAe4JuyXdhhMcDvMlOc00L8YgYatTfkf0rPxbs9SHgRYBQdhXGAiclUFnkIn6z22K9RD34i5t5YGUoryIeZAgHxJVwrlaDwZjbkOEoEFVzQuDbFdrnvY98EcDrnCF8rzNTzVzjwKWLpQB7gmbCH2HRrkqUR3+yBn602BoKSc/4GeGXu3ZykPQlgFAxgX7ojNWoFIeQpmtBEfQwd78tjSYK+l2ZAWA4FQNxgO5g9ya7G59L0bJNIKpbtgu3R05PN0NStp7equ5fKY8dazRpbrwMwlC4VgykA4gzxOZwQzonL+B3oWNuvFnqU8iMAqYFbeFTwFbIZznoKVUEu6NMkZ/G9DGtWtXI5w+8rmRhM4MtcKGcciDy1NqFkpjPGyIkA4jXiXl1d+hRLalutQIOhalLsmiQLDQBwBBNHMmhn/Az1z9l7PTMaDYFaBs2BcGAuewnQdFHgKiGXCNchmPTXi/92URjd3lD5wVC2HTfRk9EYA8VUs351zJJURdU2tO95Qr7ixIGvoQjqNutHSTQfWCuJelMufpKxyOJbrANeELbzJhitRYOjmi/6l+k3YrCP+B3rW3e1ZfLef/pIxyl5Y5ynM79LTYy3NmTAjzKajWWBehNvC2K7Hyq/VscW+wMyI+R+//MzAGf0F+Tmewrbj8Uccv8p/+Fz5LfaV2CHdWJ81fYudv7Pkd1ZVgfgzRlQgKhDfZOfvLPmdVVUg/owRFYgKxDfZ+TtLfmdVFYg/Y0QF4ht79rNIND8NyL+OXFOBWD428iLwWNdE/t1FhoS9VwueZS7X0hnV1FiuRwcgcCzYI8PPhxfrNlt0Oz7zVAqePGW9PML20GHfzIsQVsK4zpAIQKzVEstHLPEsHKct16MDEDgWAMLawNKYS1Bs1+3YeihVgNgeTux9hO1xBwhT6DqxEhq3gSFhzZ1anFmvJQfCrAAROBb86nQiTFJ6EL9Bt+MRHDwQxPZQdY5H2B7mUV4EWAkySIsMiYO4m9ZrOYzrtTReU2O5nrA0kcNOcg7s5DHak6huqWGTbgf4GdLi5IJyymG5jVi6fHqk0kGL0kH5Tw+wPcyjvAg4w10CxJwhcRQH7HotR7teS+M1NZbrCUCQd7Hx05+gtNKTUHe47u/rdkwDC3epulQdylrSDsDioukuqHTQJao2P8L2MI/yImIglhkS8AIvMyNibsMaz0I1NZbriYFAfVYIbo2xnjukdUsNty26HZPr8/ZhwVshi8guwzoiAKIzmj5dArfASmn4HuVRtod5lBcBVgIGZ5kh4RUmVmsRutYqzwKu+OV6AhC+Pl6PAYQZUw5II9yiLbodxko7fFsxDdfgZAMGEN4OsjhipeRAbGN7mEd5EfHt6zJDwvmlaa0WWUnXeBZHnebL9QQWCKziLUKACD1x0dJEQ7Gu2yGz+hgtTQQElyUgRPlD042WEv6T9XdNm9ke5lFeBFgJwnhbZEioKsZ6LcKtXuFZqKbGcj0xC4SvMA/EiZkW1JPAlJAarnd1OwYjLA1t0fKtA5W1REw1WbqUAttjanV4lO3xMC9i+bfjUk1fU8tSPY+xQNYPl7W1Jads21Lq82wP8+gvyLWmttf0NbV89SMO98lyweLPsz0ebrQ+a/od/apA/JB+VSB+SL8qED+kXxWIH9KvCsQP6VcF4of0qwLxQ/pVgfhz/Vor+rlq01LfPVS/H4KvblHqq0D8GCDC+5yZVsQNaff1J1KB2thjcYo9BqNGTslZFW7dUlE0z3NZA+ckRWShp7WN2fq0yKWf5oXg2B+7hRbXewL+yOBFGLpRGxjyx5n8rA/1Mf9hekRbVMFA2jb9iRiIjr33qDEAAXKFOnli577WVzrIRVQGohHnJL0ybA14JXlNZc0QF2qQXqdRX/DVZrIXoUW4e5qFnoif2UcKCSM3A4IflsvIEP/BmcMCENsZCTEQB26/yYBw/mxTBMJ8AgiNyOJMU5wR60CYIhBawJpmoUXnI8kUgYCzGI0MJoq3gnMRcWjAI3weGVpIoGKfa0XAs3wYt+hPHJj/QQ6bwyilojeUIyDkLJw9U00O0Ue4ZioFbQNmTaA1IgHgM7EoDiMYIsyeGAze1OaQA/CJxTWpSwk8CViuTBWJnmLA38hyGERVgSsoxHSht87RIrV3GMs9IYYB9PYh5M6UgwH9wjmNsjIEvX0FQgNppFoRnaTd159gb+9ADsdh+iQ1ivpExDwioRUxH9N1ciA6RB+xHIPhaHWpoxrVg9xwOushsOPzqOyJgSxDRBYagCarSdkonifB7IvYHax8izSHepVPo74Sr45VbZHaO9pyT5y8LQ7vv+XB1ymIc8opGXyUIqoP3l5bUsHAjJjS7upPMB/C+5JRSneDAAQNrZ4FT0TNZ92DW/DNOc8CceJDJM/vSUo5ZVcMoibFcV2c59ZpTT4eiwmWK1OF2XjC7WiyHI3XlxIgDHgcDP1wrydOOmEHsj7MiHAOI3biEYzqi4HItSKQtkF/Qq7XORDJHmGdNx87TgwErkyZwhHHgvcIuOAHja8CG7QbciHcmqwmrzAyBsuNMFXAyxBux5DmKAFB+Zm+NtzrSQACS7ETigT1S84hH3FBZLPm+lTypIm2FmU8IG2D/oSFfoez16vosXhaWPAqY3D9hLZiPi8iFpGroqVJ/yM+BySGQDwL7AleMAcFwujSJDXFy0qwPFmalHY2pDkaiaoSA8GMp6u2uNYTAqIBEGyVWm38OR2x65WA0PqYT6EUx1Qrgn5HcNpd/YlOYlZ1fLWhlKpPBJ5FF86Cp0FsiGvDWxwRSk6jAgHtDvAc6BcCpzMPQ5YmZk/QXOl8XBcjm7XWpJFMUsuVqQJeBrZoZpNEOU4joqpYc5J80oLVFtd6QiwPglFueSxonXrXpZs1xoquPK3vF39ZlyKD5L+sv5JnkbM43GK9TTH/ulJHyOFKbdr1FudtL6U2pR78GhAl9sF/9hGH/Z4W67OmH9JiBaICUYGoQFQgKhAViApEBaICUYGoQHxZfb/e0N8idfL7BvsPGVGB+MTRiIS/P24xS0EPZ4qe5dTV68BygG4wusPPbu/qUgyxu90s+7BFX8PGGdx691ZkHNxKEfL74bnzPdtLmhpuw6jPeuJfLdcsCd9CDQtANAvdtOz2Uz+GAuGssfd1KXIglkbPKhdgSC2b8za0L+Frk9luZ9G1NTOcNQ29Q2o7c8/2kqZG2f6EaDDviSsAEfgWcyDMo0BsiEICCMZjbPLK5W3JK5ACYZYGwS5PFbdQP4Bg16jEeHlQU2PlMbhd7okCoXwFM8R8i5uPOTJnSDCvwSi74oToI9Zk7lEEAVlngSCOy/WKGtkbnehfIB0EL36V1rihxAAxC3wMMDbI3YNc4HccRHNDdEOCKsbgZ8TQmPsMllxTY27/rIVSTw6eV0HMCXA3mFXAQFCEFWuQEjMkwHmQF8cd4qUwEA41hD2iGe/rUiCOC8eucHrZxfoXA6dD354cmmR+mQFS5mOAseGc5jpyr44iAsUMkVgVY9A9ohl1j3hIU2Nm/6yFUk8CLUTDu4BVABM+WJvCiTM+MCTAeQD+pFHhFIhhNiM26FIgjssHcSsAxJDqXyAdrSLWS7fAACnzMWAdhgLfuVdWl4VMFWNQSoInVTymqTGzf9ZCqSfOpjNC3fwKBFQmFAhlSGhIHF4/B4ftbGhEQWMZiDILBHFcpuue3/iX+7VI/+JDLhIuz0PlriUGiDFlPgaA6Ab9fpRAIqzswfljVQzsEXMgNmpqFOzPWyj2JAaCVlAFwioQrDLhbMqQAOcBSxO4CdZq3LdYIYNT7upSII7LYCMNukT/AumIYQeil3NzBgj0MUp8DJkRRnOR5gZ6BbZGqorRRNyQ5uEIKiX78xaKPQlsBGEb3MAqABBgJ3S8wcQMCXAesFk3BpFS+HIbpIaIubFBl4LjuAwS10RjVwT9C6SHLc445nFkDJDO5IwN5WOAsdGNmovab6D8YbTWoIphTeCGqKDYA5oaBfvzFso9mf0QeYSlUGZFbGdu5CyQkg6HtnGPgeGK5VxWd3oj7mbtLkWRWbM909Qo2xHlucsl2QrEvWP7I46cBfKr8VK2teVW0paiyJi7Zdbt/4TuRn3WVB/6VSAqEBWICkQFogJRgahAVCAqEBWIvxSIv8n4nzRguS3x999qZwXiF4Fw68mbwlmA+cC6wneVOzSNHwyOeIvUp0dRTSZfGOp2wVL57E7m5NkW/IRTo6nMjsbAa+i/j+Hf1K4wEDaxs9SHnLshmhomsFZO4bs4BsLoQEc+rt+F9h/nQcyZD8ENtMZ7sH4AOnHhRzni8DLLQJDqwKBsCw5KM7pMOcMGG4dY1sImOhtJvJUh7tdj3A3Y0uGVYi9+ge+dqppYldOQmC9qkYmenq9MAbtlwngg7nI2QoUHcxBl++hSuAsE1+IARKMRTKyPpnIHiFydI4m3MnQpEA9wNxpVLgmDPej3wzytHPPlszyIOfOB/97hbMRD1LCmhapdNNwamBzpFdqNqp5h+NVZgkN1LHghsBpNBS/eglUi4unsnGk0xfuM5/FWyJF/UM0N8yB3A7ZMFWs0FZULgM6JflYZAAixsCqI0eg1n+JBlJkPnddNWuY9pECQpgV85fIi+JEdiBkQGtkBPnVWeRnNCB0LniNOo6kY/5q8vhrv25WX1PE2dyneCgGBfqHlh7gbPhSDjwAz6vcQVkF1TRpZGRBThryHWBg+wYMoMx/Y3X5XuSMGgogtTqQfNO6JszkQdM5JPrZGZoQGzmgGH53l1gl/wkq5RrzVSGn8jJjHW0FvqF/S8iPcjUH8koOx8YxoBuOB4M9DY0LMF8hDaPSaT/EgyswHJlje5T0kS5MHwvm4J3MgHGIW+RQIoijbgq8pjaZCzAzwJ7AUs34GHPcI2qYsikK8lcFdfb/u9CHnbsiNg4TeC+IX1gQgrNc10ZgvAQgu8RkeRJn5EALRLPMeMAiNj5CifKp4aUIas4OwWvPKqwoh3JNBdSxEh0xjvBxlaTJQ4eD9hrmGSOF5cF2ItzIQrQU9yEfiHncDtkg0GBeAcNIv/YzR0VAjAoTeQX2GB1FmPnQblDt4C2WOx0lUP8ARodq4Nfo/0ge9b0eQMsk3hN8RYFuc6LeIRFPh8uBPjIib1XDERql5PHkWRSHeimMdDS7dmce4G7DlhF8K/ncEvtN+5D/fAhBU87Qksg0uvRXNb2SXeRDhm35yGzkbuiit6VWEdDezrKhjUTzY+mJkzZhFMWdUpMJ4pT40m/r/jY84Uv7B48yHz3M0lgbmbpv2cSvW05e5G/VZ0w85KhAViApEBeIPtlmB+CFtViB+SJsViB/SZgXih7RZgfghbf54ID5n9k8nrvzVxJoKxCcOPBWEbPi/HIxe2RNz7oMLMVQKNRUfltn0c+c/pbHZG3mXz7gt/I/4YJbGLXoLMmkd5+nf8CnNF+c4SYKzwXj4AMlCeVvUParMUbR01rp3xeiz8m7GZgD3AQ6N5heBaApAIKKLulPucydSEyD9Hw+wumObO0CIryzOMaTPVSOeCI3LZgtTZY7c0lizowDEUYA4FIDQar8HCER08UCscSfK11kGRPRBnDTLM8KaJIcAcUyAYJ4IjYvbYOGKsEsZiCYH4mC9PMpgwC9IuA8dcykk3PwI7kU3hm/iR/NxVWTKXg8hOsoVmh/k6E+XJpah8GEAVrgT3IYsDcLW8L48KnVVPgpzONgXpz4+v9zO8/kcjVyEByt9GjAjxMJhg4UzZY65pdA2IUdV0vq0RwzkJ3LX09AYkTYxTtkInvtAn48+MsOR1TFENMJaq2dDXBWsl3ixXKKjHOXFekefYiBYhsKpOMsyd4J8dEevdwFeB5830voRVh+FnYEWo5mwkM/nGIVIc5V2Bt0juJ3xvoVlZY7UUosRcy5t/aACD8d0Rojv1nMf6DNDcuOoJKyOMW3zdI3dLMJvJHFVtNs2REexHohss2YHu1xva9wJIctI1V3QBhg808OHqjHJQtEMYXvN84UcOiPg34bKDLziPF/tfQuLyhyZpZZHjFQ6ktaJj3CINusUiMB90OAcPAwjMHYSlUiIk0lclRgInhFmHQiroURWuBOkhxOAgBW6ZDhRE7kHxDxflMNv1oEGkQOxbuFcmWNuqQWBKdlrCAiKJ3EtApFwH+hzWJpooQGzQnhTetbHVZFucxoHY0qWpi6KtCLxT4y/J1mJ3ELudvBIMOGhp9OY2KbGszPSpUlX43k+vzQNyt/QOeN5IraJ7+s2sztKllrLo5K3zgvMaIxG6aE9gzZZ2nZi7sOJt7iTbM88JBImDdwHnA1xVXSz5lgohvNeRfPjSttZ0OsAOyTcpa9FbjFWFTQQSowHZ7o7j206QRmELY63Ys8IyfKFHGCGMA1H4rcoT0QYG3ctzNkdJUubUdkiaevle60Nh3sgp5udeSzSStCJ0ZadW7dJ2RlN0V6X3lYu9spt/mVdZnesWGpLrX/qcL8AxKOPOPTXbMScGH7Fpu22bwVimd1RtvQ7VUceOuqzpgrEp+ysQPwQOysQP8TOCsQPsbMC8UPsrED8EDsrED/EzgrED7Hz/7P5H2XLH22/AvHb21/iLghrwQXJgn8lTDwOfFZNi26WEjgfrvBQEeW6SPugJF0LL5DasoVFEfsdkvY2cT3Wx6IZv6j9YuuL3AVhLkQiHk3iKodbwwprIeJ7REA0ERCNL4nW1dul3XUF5QkFQnge5j6Lgj2Y8UA4b/EGrsf6WNivat/E/JgCECl3AcyFxgXtiCZ7kBzLZsR8jwIQsyvBK3j4QTmuACE8D7OFRZENhFkCIvrguR7rY/Fl7c9mRBMy0d+Mu8DMBX1nGMIiYFBAlQMMCBuxGFjSnRkOeMgLzgcxO8xAeTphTuA95w4KGdDdGKLWjfEqGbpGwxYfcmSVRaEvDQuP5CGux/pYqAfz19pXXowoiyStF7kLYC64SDuikRgkUOUAA4I8U8F5PrBT9ShThbkZzOygmCnq8z5y1BRihcAVa0SJwrduNL5DDMTRNj7kyBqLQq5I0e0wD3I91sfiMH5F+4G4cMzcuQvcBTjLAYQ16oxHfBD4ZjHeMYvhw6rWRXeDz4q5HtO5D44d0onPGnWcvP4He8m1daOxF2IgtrIooMIxtbrAoFjneqyPxfEL2iedED8ONm+9yF1A47hTgYdNFCtArRkUiJjFEIBQUQdhdnAK+CAqIaGCEn7zj6gya0DcYVEgLeh2PMT1WB8L9wXtk06IjkMRiDl3QRp2QbJAC2MQwUGwEYuBIp80Qqsis47KAnFDtDQFMHVpChFIlKEhEVrkGvU8jy0sCkoD6eYTXI/1sfiy9nUc8tbL3AUwF3IgEB/kNEoMFZYYURYDtXriKFuGYWHOB9NfpvIWW5ioXhhV9sBm7aLW1RL/C8XzPLaxKCiqJHFPnH2c67E+Fl/Tfjcqz8PZvHXzOHdh/vPLbWaCrOd02fdHeR5pTY9zPT7L43ik/bXWs+Ne43MOwnZWwqP8hcd4HtnZYQvYv7v9L2/89xz1oV8FogJRgfghna9A/JDOVyB+SOcrED+k8xWIH9L5CsQP6XwFImv8s0b8mvGfKf27yiyVXqvrF9qpQPx2IE63MnNBq+AXep3qG8fl8Ff5C4MN/wYFC5WKfky/4pFW9SmtlmnG7e1tbwexvjumMPBYYYza6T8+ehmx9zf6dy+l9vxp/4acLxc+2fpG2rTXtsxcUFO56dHcjZOSAwEFCzhBHotOElr1OuMrrVqblrFmW3v2od7Bi6LK+x6IHoNJeLy/t70xO869k1I7/vTSA4geA9/uPBC7BJEF5oJ2j9kLKoG+wl8ABONRy0HBonHN3ZJz/Qpt1Zm7Zd0nykRAbO2d+iibdEacJyh4IHf3gOhNr0DMZwQ+DGXmQpi8h1FpJmv8hYFLXyUiiipYkDvqXsm5foW2Cnf9Wlk7WOFDaBkF4k57zODY3jsnMVcaZjgxM2UqPQ3zeX/p+2nROb+/v7zR0O8v+wsNP/56IM7m3Ld0bv+6N0/Tv1OO6dP+Mn2+vEz/J85dkbkQgCDeha68y/yFD1bn+EBEEq9g4Zxz90rO9Su0Vbjr18qitI3KHMZN7Vkb9Q5ArKuAHG0ICkLMFCpNM6Fv+VLvdUZM33nJmU62AGLfE2Q783Khc/3zzjxPZaYcz5T7eTrXUm5eHwFEpkChQBir8UrW+AsfrM4x/WsABPgOCsRj+hXbW6UZAU0LLXO0W9rrTFymMffKGAtBAKU8nAxmRKtA+BnhgZg2aQCxI5CmnE/m5W3/Rqk9gJg+7ad/928tr3B2SYEiAKHxStb4CzwXaFzGbghATPdkt3sl5/oV21vl0BvRoCKYzf32EEtFyzR3o8AYGRkFAjNiWm4EiLBHBCD6NgDR03fkZiD2ZwBhLtPsaBkIBCoqMReCUoYParbCX7heaUYNbHBQsPCRTh7Sr9je6mBVYmR7GeiFWBvKWHO3jHXR0kRLKfexPwOItp0DwYuQAnGmfb2nc1iQaAbg09Olf27pjmratorMhVgpQ+MervIXHDE2BgtCoSpYhN8Rj+hXbG+VgIi5FpsstdDBCGUAxLoKCLRJSDMEewz3sT3Tpvy6u+jvCGzW9IuBzr/wJw9Ey1v1K+0Ve8rxSpv0tIRN5feX7HZNvz2ilJHzF+j6fpT5kOpXPN7q7ypjFks/9su697eu/XrzjzziyPkL3fg48yH99JlWf1eZpdKPAeFnwFcCsa3p7y5dH/p9exd/VpkKxA8pU4H4IWUqED+kTAXih5SpQPyQMhWIH1KmAvFDyvxmIL65si+x4v+UZlOB+OYR6YvfiH2w889D9nGzrWaiP21avMxQaP3fttRWqGOfVtZPjpTL7kxPZl7edmd6VsxueCk9fbbGpsoayulYeiuIz/r3qfjvyotkvo5ZHmG6cOtek9bA3Qpl6dOAN++a4d8b3pfDs2lKx2vM/06+min/bUAsmzIQ9CS99cO2S4E4Bzx0ENvoT7sCRMxbmAOxS7P35Ehp2ZfVEwmi34E5YZQ3EQPRRNogsRCFDpv17AsScZA3WVeBsCtANBEQNtIZCa/2wvmLeO8aw34QmXvlhDj+b4AzbgmIeCw3A9EXgej934S3sA0Idgi3gGQH5oRR3oT0JBnuZSDM1wPhYukdEYjwoYJkyOWvgTw9D/8xAWJEwAhiETxNi9C5pb99j2/kTW3ZyUFODHJ3vL9TTnaDGDAPni7kpQ08hOnR7pm+sVvklb6/oe4z8kx/E94CtfPkXSJgPex9GtVN3isG4vnpwk/wz+QnDrwJfu94YA2QsTOqDUKcjtPodUNG0sdgNxVHc1FNkZPGhOEXjDmfE30MKXvSSDAjXlZGKiLGiF9/kHgQ5jaCtEBvYS8DYQcAcUiAmHybBAT7TMn7zX8nvyp/U9YB/iOX3uQKbAMLgdatZ750OT/zEJBv8vtRyWdaS/5p6fuO83Bqwlug8q3Srnx59uC2WBaf2/1r307AvD63CkQb8SYaoYBNA2OxNCmnw4puCKt6OF2a4GsmFoaRuBYIpoB8LGuhkWGkDrA68Pq+1AzP/GDEeYaaP1hvg95L9xFqyOWMt7lp57ghHoDjqGYUvYbUSKZP0/+JM0JLU/vyRn38h4fgxeAb7xHTZ14cCJQ3GoLAQmDmQavrDX2b0ugTDSIP6/7t5e2fFgO754WszXgLVL6N6InThswttlIHtgJuRf/yjAi8Cd0iiEEie4QJ0hSI8jJ1f/BAmJMsHxzTxGicF+TTGCeqCYK6iNWB6xmpEL2QoDd+9/mwqkvgI9QgMBAUR8IcwbdjPCM+ZI9osXQDCGIaBCCYe/BMQPTM4pE1+yzMg5aWDx24aaG/9MxKaOX67g2AoLNSV8JbABBG780uaEfro7pzILBHBN6EbhEfEmwkBcIZCU8SAUEzgpcPa2XTVCCcsaLEpJog1vhcEpUsVh8x0n48I3hwfYSaRuJzJECsbNYtrr/XVx6eHt8EiCmNFgdZmgxYCObsmQdYmgy+9e2ZWQktr+q0uv/jz1LZc8ZboHoxr0Au8UtTqzOib5XwgLsn3DUF3oRsEcwgaYxogwAIq3cqvDSpuBGT3wwFpDpa4axQdBfJJ0Ivqp6BgpSDlibiICoQ1yv8+WgdQGCPsCFCjdONO9w1ETlnBYj9ha/hnv7qN+IfYLM2LbZPAgIshCkrMw8ABOUHD2F/IU5Dz78/KN/0vaXN/ow8lJrwFojd07L3di+bNbZ8Smt1Az/z74gzfk/gd0TgTSgQxCA5jaINwoylRjQ/wEyhlMbrXxgK0XZtRtH+uHr1DynbSdmGhx4RYFihcBRhpJHYVMRwQeu0E9BgcpQxH6EGvyMAhP6OIHJO+B1BTHT6b5BYNvd+8MlyHf+OXHN49/drNOnt6pYyenzHL2v3qVw5w8Wld75ff+jt5XcA8fjx5x5xuFVbHtX3+KIh+HNHfehXgahAVCB+QrMViB/SbAXihzRbgfghzVYgfkizFYgf0uz/GxDfUemv1Ellf1rQm99iTwXiz4xRdNCTwxO91jeGc+AjZIdbriP4g50vb24zw6GnP5B7YM5xAMOBnktSyvv7KSIHlN7aUXtdZtpJYm6nUV7K1rvkk1vppryDOpYqcfeGpxBRQvVA2EZ6qxvcAjMBYVPN+HnFA6pMH9jGcVK8O17c6AAiKuFYO8MNRY7DYCVCPH8nf3AAItZO9vQAmw+h2iJa3F5Jw0bWh3wpwaAEhDVpmXSMwrW1DoRNxifVAxHuiWMs2VlCQJhtQJikMzkcJSDiGsYjnDNz17oAMTh9nXEILA1nCkDMhtAEILIBz63/NBBmOxCNXO/W5GF9Yv2RBjXAH0Ua6tSIchTwgiw+6d8bu02Y+wDh8UEEyA27QQYd6FCe6oxLwJDJl0btLQJxMApEYwJLwzGz4jCqhgZk3g8+comkcQ8UCHb0cLnTGJgbBxZPR1QXZWmQWynUErgc4HlMy+XkSFIfNVJia5qs3IFHS0ZoQJthfDiujH/FmNYFDmlCLdAHw+9TH4X1ADc4MR3UYSheWnYd4oVxvvrx10G9A3FRQnl+DzkqYcTNry+kpxwHpEw2yO4xlR0DS8MxswJ1q7TMaYRlsNpahNow0Nowxkc40bgpcHzaKKqLsjRIpkJr4TkpXA7uPDlLKZWBwBihVbWmycodWctDejmgzTA+qR4IAcGyE/S6+MEACA3A4fxQdTdMLQWC4oRgWCXuieu4LNQ7gHgozzMiKdEwEKebzoiU4/DB8wFhlYzFjFCWhpN6Tzc48LsboryQpoYwhkwn8VlU0geCDeSWFCAk2omzIaqL+KQdLhR8O4m2R8eRUSy80J61wfkSa5qsHI3bh0SG6URfJIxPqgcCIETsyA0xEMcIiOn7GIAAdYqKWV0VnRmDlFQcF0X3iKzEIAuQK3EcPnBVDcy0kM3a+UFVnRDnQ6VBN8OMYQhLQDiVwxqg2WHhvpeoLgpEN4RanEbVGq3GRxGhlRwIsWZIy2G0ZISu0Bdpop1TWCOsBwIgJKAaq8d4IA5jBMTR2QSII00+HtZoabIaaYTNOKZARCX0ronv0gocB9o9+NrkegGEJwfY8J+VeC0SQkaoLjrAAoQNS5PEZhms1/rwUV2snxHG12I1epLjyCi8mDlEzUpXDW/NkJdjLQ8/Qtba5K5J7HJ+aZIpxwmnsDQZ3RAp8gg2xQ4kQ0ffG96esBXRd3Cfw31y2FDpd0RcQn9HAIgCx8F1o0x4q78jFAhEJVENDcRrsWIZLh/L/AjEKHHcGpQzlMNBnVOtj9OcpRFqETUSKHN012kQKSqMAWtDAlml1ri0HI0bU0BlhJoxjkzHcWX8Zi3ck+l3hL8f0x9fehOmsT70ryncnN07ln+JzmvNz3zfL2sllM3PrZQxWyO7hH4sj1CzJYKK+8Iu/8xHHPKre5yfWweiPmv6MUcFogJRgfjuMapAfOKoQFQgKhDfPUYViB8yRhWIHzJGFYgfMkZ/vqmH2vx+ux5rYWvuT19OFYg/DoSjf+h5aXa+K+Y+3Yrljb4tuOmZlbz/XOJGNANaaQb+FPFBqF7mPORWpo/ttlrh5Mko9Zz8es4VGBfSRpRO0TE0W8jfFYBgLspMwcDndElfNJYJlMBT93wTlw8chHxM9czwGBBzboS05zxULqUhOD/KTdoDG+y8z6zwx9CY2IllBpf1OArtEaWTywrl5wwNm84IPDzPgAjx5FWfxcYDcB8I5GyiDuq7958FoilyI+SxtNNw9BkQNm39O4BoHgSi2QhEE+U0yt9AKWE/DIb9dHDpT3N1cuOB/aCOHpqMnoMgzIXDCAHzk3wD+yPnNKQaF4ONNDAGuFuQjklLn8S8QRhKU56bV88wzHmA1Wid+RrXmHexxKwwxmtpmBvYJoP2HP4yjAS88OB9UGuwKaQTEAfubxflz8ZqNMJFiTU9YHEjTBB4zw9SCuwHB95FIz7eydnnZEbYEATJcxAoxpKFGxQljvLNzDgNQePCMPNh4JyigTHAUOdDNoGJIQSxEUCQXR9ePcOA88BWqzMUUR4C72KJWWEMLBqs55MMILkwEAO3PvjVH67U6R/hbvj0yWYeAXbO+vzpWCEgCXFRYk0PjdECJgiAANFD2Q9+RoCLEIDoJPqIAoFWrX/H37MfbiHeVcxpCBoXpGnRgcdhRANjUJd82kKIYqIz4sOrZ4C/o5wNuPLxOfAu1pgV4oEn/Q6TzoiTiQZ2MML7cE491SGdZgTaiRks6VhRa8pFiTU9yE686q58EljtFSJ0j3AmBYK4HFYqXgPCCW1kxmlINC7M+ME5nTj6twDR8IxQXYyUsxEDEXgXq8wKDwTFVkr2iMEILQJKAOB9BCBCugKhewbyp2Pl4j0i0vQgO8E0OYIZ46T/YD+EuyZM5g9MGQ7tEi8cGlVEgZClSalfw5wLEWlccAwj5voZLE2Nd8mnQGAbd9Fd04dXz5BwNscm4mn4pUl4F0vMClXduF6xNGVAOCoXtlzwPiIgJL0bIiBswtCIxspxxBawioOmR7w00X4DShX1TdgPTu6mT8KTmLbUK/09jWAiyHQTDgKoWhzJBJuPchhczoVINC6urIzhOolbAskpbNZ+QjOfohmkXv87AvaAbUGcB1itm/VpxGatvItFZoVYRLd29D37HWFRTu+3rK8R3A1N50gsN7CjYwZLOlaIK3Pi30lB00NvbpgJwr8jqH6MdXq7dfdosjv4LXyO5SOOSxIzOdJ6l+yKcy3xTeIUt5j7kd+/m+PFFH5ZpxFdFo/6iONrc68/4nB/ssMViG8wpwJRgfgjVlQgKhAViApEBaICUYGoQPy+DlcgvszcUsWPNfh50762U98XfufbyUAViN8CRPoEsFOuAT2SHqyyFyIGg3IRrH/KOKvSxZyIht809pyMmas6fm+n8AjPWXRu7TUrV66Z381jBymUPjp+HhoNFdEYJDJ8eQxsOk7OLg20o7FKlUT+vbn5WM9HPDwNTNkU+rL55FcW/4A6OqTaQZ/EN+oAmQ/REDvUp39HpQJY7+5vMtSsb39eVwxEXDI445dqJh/AEL+aa5N34oYSEPEYxNZYdu0sADHQWKVKIoFsYTOLrZ0RDhoJDZOeQdUHccE094AwS0BI8HDTuBiIuM3ADUmFSyJAZkCUhmGhZviHnQfCbAEiHoMkcPrUj2UgpOZISWQFiPg89CrIYWGF7cAMBzaiY0UL8dQy46KJGAyetTCobzaKOBJYEjJA9DYz+59Yo+MkjAZS6rjpK68G4exF+cOfR414G5zdS8J76IyyQzqvAbJQsz3w294xEKjtJmyKht+/NsIG6WZjwBQDPi/94NJkyWA11oryMnQ8VEkEOiBgmaiyieqJdEY5HP56aUTdAv5cnZbqjqQqG2ElKIPBsxYGOe/VPBKWhKhQkNF0TYIxAWdnEwXBwPvWcCqifT2PGhWIyeEovIdGWB2irgGXabHmKe2Kq5ve73YsPYHaiB0ibJAjUx+IW2LdbAwQnwCOWu6HAHG0A+uAwA7ldaRKIo1Ed1EtEMNjjHfCVSPExPpGVqOFREZ0JgChrARlMHjWwhDYCgWWhJ8RR4neQMRO63eExii/gjraMXdCgYh5Fx4Iz3tQyztRpCEXfrlmx0QWmREmnhHEDlGwGtmnoEGSjgHPCD4v/RAgyDJrNOaKNfGMUCWRRqK7dKofcvuwKiHU8KzYAASYDs6mmzUc58paaCK2wpwloSsiGQ0mCF0v6XDJGkqEAkoxfkaYAITuEYH34C0fRU6ChqtYs8wWFwOB2j6gbzPEQECDJB8DPY9+iC0ChM5Mm+wRqiTiFaRGK/yVD7ZagQDzw/MgmiIQYDrgrklZCREQorWRAxGzJHQzAl1BGRNWyFZUOiwgRC+wNl+apMZBWCZH5T0kkUrA8BiWao7umky4a+IcxwMuNlma0Kv5GBg5j36ILY4sI2vABbTJXZMKWAQ7rbA5dGkCEML8gF4FNmviGgxc22k8iZYFxZHC74jO/44Ag0FZC3rfrECkLAncZ0CXQn9HgB1BbTYcjaQT8qSxnMJbczgvNfK9+/T9qrwHjZAC3SfeSt1SzeF3hDPhd0Q3EDvE8e8I3ay5hTEfA6s9kH6oLbJQXrmHzGMJvyNiILBZN8JfGaxu1qcRXJH5jZpZ5kO4B35ZL3MqQs4m+SWgC4Mp30RLyTlnpNTSvOZSXq3NZWXv2V+yxSUl/vpHHPc0Mb7nEYf7pdKlOuqzpt9YW33oV4GoQFQgvmToKhAViApEBaICUYGoQFQgfhcQf2K4/ttWfatlFYhPWTY9lxyaf1VTuFsr4UNneH7HQB4veudt+iRRnE3EX4BOMj1D+vdGnxFNBfrJiJ1CJejJKNXk5GXctD1n8n9LNoUjPMlMWSXBqseOLh0Ti3q7KOpMbJUr1uF8XXkuFyzjtyWd86/SNsWe2gQIdalb0ZlnN7A4EuMuayh78RgYiaYiXmN9axqRVVihO23dBnGJvMvNIhA+rkHGkYhZFXnQkhmYyeeZTX6cglU6Sq5Ya37W+coiINif6px4mZqFS64MhIsC2ov2Pt6310goOthGJdnHowJx9CD5/0ThfgsQZhkI/28qbBKseizudwEIcx8I8xgQZNfBA3FgIDrhHATBdY1KovwJ0aAw3i9m1DvmZ4RGQpkBQb47DPohB8LB2UnvQqtyBaKeIK5Lpq2hUVSuqvGhXArrFTESjkRkVWcQlQU0AIkQM6IeiLVrzJXDCBaLLCzjQV9KlrOwk/qMAFPB2rgG5PAcDxN4H8gvlrkrAcEMiiuQPqoPW19jZxff5AUT/sQxRGpgBh2igzRj2COUvzCl8Q5AhuPzB2vV8+cr7S8oy+dojxjglFTlCqMzNtHWILYIWXkURQ/Hn44SoGAaZOFFxByJU8KqkJfgeUXUEAJGtEKwpEnMFaveegaT0o9oAWcRdcYNRigLam2o4SiRk4xyqxiIo0SCQX5YBk8sr/DQAlDOgZGIJapvEfgTHOiAaQC05R6MzIhoj9Bd8SCiDWT4gUPYfOjOQe0aLUvxRXRG+JZvGr2CBRUybQ25CG46p7D5Y0HqJO5KwpFIWBWImMLOZ4d4L7SNUovqSQ4xV6IZ4dNJCUSjzky9MgEIWBtq4FuS28l4AoMQjU5R72CZrO8m2qw1ahUilnh9C8+fQBQUv0cMCRAWShUS1YQEhQQIxNMaAhCWY6cYibXl9whtWTsNa1JtjaPSBWRxO8rmr8INMRDCkTAxqwIRU6g3dpB4L6NYctRh1JgrTRSyLNjUhPgxJp4REiLE13BEXwcTSVV8eMutBLbSDdW55K7J2WBGpG/h+RM6xSDpwVcOYyThOOJIKHSVX+UKdgUgnNw1Od0jvBAXK1fo8pgrfIjSkV+aEAcGadYrYiQcicSZb61eVgIEbD8Sj8Rfz2YGxFGBAk+Eh1nCudkoMFZcg1jmFAiqn2PHRPlhGf+OgKIFuGfCiDC67Xl9C6P8iYPGPnS8slv8juhkj+hMHJJGf0dgwQMQpEKBpQTKGhJJZaBJTOoe0vKVjG4k+kmmrSFRq3SzdlGaNaqIkXIkglUnRGVBv4xEiMHNiMVdkcZcsRKXRZamqwJFSiAnbMoSjQU7ZOPLaQ3OR30Bk0Ti/1kthegxg/DwVm64Hj9mShUP/oRqNrXxuLUb9TPs/NOnR8IWLLWftewTx/c/THB3vn/eqsAjucco2VaXW6z/PwHEf8eqCkQFogJRgahAVCAqED/cqgpEBaICUYH4fwPiv2DcevvbrLuX6zf3sdlw5jNGNg/U9ncAYRc8wmmg4fyzPPbmJ64345W+47JSwSCxSIJiwBDXHDgSeEoKjkjEucjM8jLog7wUPKz01c0ZFAvZgt02jvMSM0FCHBRVX3CPAmF9Tx3a6LYBEdzvMyAG58kCRv524Qq1/pPxITDyaCKzKxusCw2V4YEY0nguzUJcl6K8wpDyKppNQMRxXmyi/a2kiXtAeLpCzhcJF+OINprSVfMgEIMHwrH3lh2fsSUpEMc7QIRoPQKEOtSGuUZIswqEWQRi24xQDf+4VhvXsAgEv5e6CITWraok1EZs1TDnSIAfoWLigZ2AHJSHfdDKWaEaZGkabCfukkEjMWgsEomkgrO8nLnDqNFFyBVlRReEzsRKFuCR3ERxoxGmB3XnIJFYqLaT53KgFsSDUculHXMbhVlxpb4ofwO9h92T62cwXlkkZoIow4Xeoy5HbGE1fokwQ3Vk6hv88nBQJQEQnTA6TCkKikQ8gXsyYicgNgr4T0fWPuJX2Ami62kEEI04EBMg6IXyK5gWYU1nMQXbSJwI+MqUI5JyLqjVD0R+YSkIXEfQBAkXMxgmcNEfrdIEjv6leMOvwIuD8uhdtD7uizg+jxIzhZVFYiaIMlwIiHLEFl5aBw2RAs2RoL7hX6oXVRJqoxF3tFzSOUcCEU+6G3gcEZeCY6NgY8aM4LACFmIi1MiHVwH4iIA4SWwFriUCghYtKHp0ImOiHJGUc0GtombiT/h6EwalBX/DdsIusR5EhNVomHvxAVco8TEcYr0glIGCwu7bwfjX92MmiDJcGpV1KUZs6URzg2dEor7hQ4+IKglmhPraJY5VypFQFgV4HBE7gXMIcWBwQaPK3zXFQER7xKCsQOVpAFZ8Z42LsVGXO3NEAucC67z1NcebtQIBCovMX8QrsX6PAAtEZE8+hD3IM4IXCc/fcEHJKQcCTBDPcBkUiGLElis0N4xojgT1jQAELo4MiMHMORLKorAJ24CB4MBO0V1TFIyGOnC9Nihrr1e/qbKaS1AOS2aE1bkiQBx1aVJX/zSYbKoAcYQqSGFGWOtr8tN98FFYLBYGAYKEH2ZLk5OlSYOy5WoZynAhIMoRW1T8hbJ3gCtS3whLE1P6hgIQOUdCWRSNbNbKTkBsFN5apAvTtBt4GwfHjhYvykvcBmdlwBCLBMyFEDtFI6xA0YP+Wo5JAo5IBIQjKyxrY1DNzMyQeRADwToXWuNIzEOUVR0Mqht6HNQ7Y7Exh80aHA1wO7ohABGYIMpwod8RCxFbBmNUn6Qz0BwJ6huQ9FI1D8SSwe2QvXMzt+loPpm2fqz/Kn3MwpVb/UW75+3nWhullPRXQSlXs5Be1P74gm7+8vHtz2Hsr7T/Vz5r+qFA/FL7FYgKRAWiAlGBqEBUICoQFYgKRAWiAvF/DsSfM+QnDMEPmg8ViMUD75bJZ9ZvbqL3XviZZ/RKP71T7aRkmSPR0FvWA31y0Vln4D4RgkDh6NLMy6lme6qNKlxueelw8i+/VTis99rNxrRkY2JDyeFuQ+bJXdNFb4I1ykPQLAP8EYHhkNfL75q6EhASFsTGFIXI+PBxMAup4dX1eapdZFPZGRB28RlxEutdLBfdkLmdC0A0kTZIWvMCEIEXoecOFDfEHRIgTAGImOGQ1yte6mUg2An3OSDKl5JJLoj0fGMy9YuFGkoXZwSEfw99CxCJlkkOhEtCrUD1QtkFJgoJQ24rCIwjXkgDHgK/UquDRKoTkHjoTCeKHYH9QfoeQX0iKF5Ay0KYGkaZDl6Dg5ydwq9oWDY+xCwhaySV/uXXaOEqjcsm0VZGZY90Q6Z+IaodmvcAJgdKXTVOC0sZKYslAgJS8qrfwZdupAiisVq6SBtE+85MFBNsgGqHvG+v7IIYCHqZHXFONG5J41/6ViCOtvGvhYvb0AX2B+l7BPUJPXcUz7NoUBhrUpesvB5vVe9j8DobvCxIasPyDda/MBuXjaOtOKvsEbQVqV9IRBjNa8HksFhTjcRpsSZisURAIMoLwizAOwkrUVeI1XIavTNU+q5ecbUBCh+k1XIL7II5ECpXcLopi8HFS5MFywKRRwBEYH9MeY+qPkH8CsQVARNCY44YYTqAAEDlMJiooWHHu+pskPQWe8kdR1FJgYjKxtFWnInZI4n6hah2NEFughy6OO+I84EILxGLRSK3AIgP5psEC2ClqI/4WC2qaIJ910VAeBtk+R4dIusYEcHyQNDgNInUT/iWAiHSDex77YbA/tBZACCoJW7POtkjnOwx1ugnx+lBH8MMAELYEkaBYNbHwG0flcWhZWMguHes1eE366B+IaodKRCwuxtEwmQM+h75jGA6g1hAx5FrC0AoJcJ5PRAzxjPC2wBL+Vq9NhEQPqQL3766iAehS5OkKxA+bonF9D3aSMeJNh5Vnwj6TAAC9AAbEdk0VfUxCAiUFsZVtDQxEFZ3rFCWaBQhigmr3zBALC4Rq1/4iDAxEKLKgdgqsiT6/qgCCNsCxoo1asFhFJINK5l8MCek8YomvAuLagii0wQbsDSNzGkYfMQQxCAZmMXnNI6hKPEJKVFycNUnjoGCyCOqcdH4zRq/I6A+oZs1mBYnr0EhTIdR1T8oVfUxWEnKKVtCy+KGoIGqhl+atGwabYXtZ/YJL3WJ+gUYF5pXWSBGbknA2aA+KReElEVYU+MG4a0TGBhB61C5HYbJp1caHa9oYlQ1BDcIwQbnLyZPjg03euv8i7Uc2zUsvub37Zb2XPTvtpYfUeLwe0TW3poNbsGGXxYACcd2DYuvAWJLe+7hlh9R4tC8bgtoX9z7Lznqs6YKRAWiAvGTbKhA/BAbKhA/xIYKxA+xoQLxQ2yoQPwQGyoQ32/DY9X/Fcyev9PeCsSnD5d9R6yQbiHdRX/xUq4/RPgZLnF9/oK4IyGyCjuWUHo8DaFjbmZXJ+fdquWxtS6pxq13224bky7OzpG4QyshVjeYKqH0/ElVdIbCkkfpDdwitzyKiX/NvKR4YRMgmlnYDRMxMwLXwkaRVUQX3PAzSwEiVT6xiR0KhF0Yrtha56kGocamOPJN9HWu0WELkVuQi94TdV7RP45e35i4ZTfjprj4hfAhfk2/kajwBT2J1LSHgUiYGagrePm2ARHrbThvcLMChHwfOjOvsTgFVvhUjwGRBkQvAcFOM5MAYQpASPQPYSNorJDGINJI4CZQsaBsEWKNhNdj6ZNyE8gNc2NdDQNV/RkQhoG4+ZgtWiu7oia3E5QvwKvgsyIlz1FQMhs0LwVHuLHDxiuK4GVkM1fFwFnq/WS55HIDLNBeRjwNGnx+c1oIFdTGwE7agV2rkfXshROX2k1644xaTt5Qa0RdhN1bGCNzFI0LazXkhk52uE6Vm4CxO2ZMDFwNiG2C2BGNp3h9CIuB/HwaWWXyg1McCT5LHfuw4HyAcWHlmzprp3qHowQk0NfNOS5UZoPmJSA+LKShjlxbg9fzzVwVQ84KG0RyDYjQoL2MeBqDYW6i6IqgjcFvTaL/oX0JkTY+xEbaHa28VA9mB6mLGKszvdNKOmEjaKwQAAFJrBOzMmTS2pSJIduxFaZExMxojL6Lb4YQWSXMiGYAEJ1nPjiJ3nK6+QltT/ABexYE8yRcboPmpZmhQLA3+aayCwuqGCDTuJBLRwK9ingafkacTAbEgDH0ZcVTr9IPIu9l1XL1Y1uDvvsZ4SUTDIJihK4xk0KYDrpSC8vNMzFMxNZwxkTMDJ0R3G0fWSUAYQ2AcDYGQhggfnARayWwIFj2Ycht0LzTIniNgdBgMsYsqGIwEN3gv1/BZFIuR+BpRHsEdEUc9gi6FET7Zjz6vsRA6C2WWu4sFEKs+ZCZDjEkDRNzBBtBY4UIEHKnYzXUy4yJYcLd0BECOsLMYCCwR9goskq0WSsQEefDCgNElyYvVcJLiF4wwa74shHFD8dAaAyWo4aYmatiyFneSH0uEvARe5qEpxEB4QQIm941MUPjaH0MMWvjpSlYTjJEHgioGQGIAxQmrtjoNVbISZgUvBGBiWACmyEwMYzPw8wHTxKh1ME66HNEkVUk+BEJXd3odwRxHRrPuOAuy/amEbnA/2j8Zg2xqdQGzWsca2Jcm1H0RcDEYHtmqhgGLfLOpbmuji3gNoeYpxH9juAYZZQr/h1xAldE+hKYLFDoiC2fLndSKWF1EcolN6638r3uwl37vSPXsHB3bhiXfqk2RTZ3fC7WrijqWKxYjvwu67fL8jaFkfmcQsg9Tsz8Lno0v3ysAHE395864n671RzfLtXydUd91lSBqEBUICoQFYgKRAWiAlGBqEBUICoQf9Te39EgtaH//2sG5ne3WYH4g21OzyA5SDtUrcWd4R7jKqhMuclDTiQFfRvy/7geadezJ0IaXhl3vnXDT0ONVWXytA2zfMKHHInCapQLZTV0CsSqjkdJe8PYxRZsUgKOFDix9dVTPgbzGFfB+GeNVl3soYTGI9E2mDwggufpgMXh7U3yySX1zYGw3u41IJokc6zSMRvWqIZmrqGRjKCIPdh7QNgsxaZcgnUgHuEqLAFhloAwdjZgjcnZDykQ6qiBl+n4i0A0Zm0KByBkRhTIVhEQ5mEgdPKz+2OQN0sHRE9AtBJ2wQyPcRU6cX/AfXMaVSJd2CLXxrfRyP+dCQwLquHGdZDPLGY/OBv4JKIjMChn5OBfyj15BQ8tdxhDuZvEUhHX0lVeOY5UOpQdMqgCibBUyHcHvZFYxyMwSk6jtm29CkgUaeWKcT6MB6/LIdohVyrBLXv/q8Ob/hJPhKOVNMZHQHFbuAqnwahj8NgYfYVdORTMtHABCDPi/+StUoYF1fAhdaTsh9NobVC7IXs6zxmB4oeqgJiE73G0odyHxFKRUAtHeQk/Uukw0Mawg9VQBcJScUexyrdpg2UOb8VLzIAQIUUjrYANQHUcvS4Hesy6CdwzZSRowBg/IywvWD4CyrCFq+CEBXKSuCgKRCe1uQQInREck0YYFlSDAgF7PftB3uGPgp4NyhkBlUdVQMBzCqyJUO5D/MmNhGwjD3pnYpUOI1FZPkT9o1GWihPfc9DQSBglgX9iRRymu4VIK6IKIjFbdAmjHuNi5ZIBCOwIukdwc4N7iKvgVCiCZ5IzCgQ4FOKin+0RAELrcTojoHAR2A8q8uDtaRI9i0gFhPri+R4ScoPLBSC01PR3jFU6pK7xQ9Q/GmapHBMgZItIGCWBf6JAKMHhQ9VLRo3ZYv3NgUpPWCw4npEgQHRDBMRjXAVhgYCroDodyqHQwDDaRnTX5BkWVAOA6K4Z+8HYSIhL75pEz0KVMo7WK3hoOcSHWVuaZLO2XofDQv8DbdA52gvC0hTaDIwSa7VtD8QxRFoJS1PQ5dCoNRqUR7UzptpPRkIm8e8I3Bd2D3EVnLBAwFWgc40GH2O+BOKR+Db874jAsKAaKHbKifVBYvaDk20Qvyz0d4QOispdNV7Bo1RuANvkKrcQslkj8otnqLAeh5Oen0QwC5FeQqwXbNaBUdL4tsHQMCAnaaSVK6w7iLoHAmjBFhnzcXa/vKwTMbsRzO64l3T0YpZF0K2498s6Duu0dGRUBfuIvXM7t/X5K35Zu60ZPwPEtiP8Ur4HxBZOSVq2XOLX7P0eIDYf9VnTD2mzAlGB+LmDUoH4IYNSgfghg1KB+CGDUoH4IYNSgfghg1KB+CGD8gfbLDQfDx3+llJ+/4CstfyVVv1xAlAF4uvrmh3OpNwDFydaH+1ADDkln/MU6A7HRxx1BErC7Fq8yUu8o6qQy1NV/+CO8iw/KGIdj3b6j49eL4o3+ncvefb8af+GnEhbO/q1s+0+B2KWve379uUy/f88JbYvb9Ti/vL+9nJ5edudzy19o9T9ZX95of/O0/kpJ9t6kYGP1SFcJuyQAmGG+HOe4l8AjsQTYiaEfytVfOZWFe39C7c+LzttUld7FP2ErOgBBOHx/t5OA7PjpJ1k2fGnlx5AhLN+4BaA2HHaLgNilwNx1rYjIHoa2L4nIHquoW/5zJRGQEyfKBP9S+d7ykNAtIDDmbi79s6MSIFo5kDkMXaSuRcB4aR6cygAASdS43IgTAzEGaNEg3YHiN70u9m13q7MiHY2IzYBcd6Z55YG9VmBMACiby/P9I1B4L87AesZQBjhHFhhHAibIVPYYO6CGBIPvXjaohQCQlgQBpFCoJOhEVPEW3U8jGBCwAt28HFJ8DIvuz2nPISX8kDIztMIzQ1iXry/Tx2dpnnfv1zM+f395Y2Gnqb9jqc6/fVAnM00RE8XOrunv6+U4+WCM28XXjR6fHviHC/Tv/TZcCkqsePv7+/7V5xBfbTE0LepxNT+09TiEwPxNAPi9VWA6D0QPCOeWqqlN6KDASbBhwWbockUNshB6K/7Mf6czIiRgOhGsCDA2fA6GRY1QoW+u05tjNDaN5x+tLpHaCQVyuNc4IEwHwN1OSjbtOgk9bvXGYFJT4sAXcMMSU+Q0X/tdI5Sn2XxmL7SoLX/YNHon3nJaDkH6n7ma9tMf/tn+t627+9Tey2l8LVMdfD5Z54Yz1TqlQ16fTnjgtgZ2g16bkX3iH46N50/Ux7K39Ie0RnvwBYgwNaIFTY6szQjjMlnhBEvtmgBqE7GDURMF4KoCM1G1JOsnxFKu7QmBsI4H4aGmRM8I1oFws8ID8S0SQOIHYHEy9fU3Te5Qnvayunsy9v+jYeIrs9pQ+4NIBCQGaKpFNf68kZ7EbX38oalZf+GOmSN5xnRP3OpZ8wIuhyeplpfuBXDe8T0L3Jij3im8rTCjSUgUoWNZBbc2axjINgXrKSBcRsQRu+6rLGBkJMC4WSP6AWIsEcEIPo2ANHzEtHKqjwNIK3vF76aDa5VcyEgpn8ZiOcECL+58k3B/kylphlwphmEOlq+S5rynqNSHoie7TwrEDQvNacsXAxEROkAEFalRFyyNDUasmyIP3dDmqJxTaBi4YwHwmt4gMkS1DeMT2+SpcmAq+N5IKrI4fzSNPUGQLTtHAheTBSIaYsAEM+6SfLS1DI0z9nSROv2pW2jIX3udWmiGcHDPu0e56fLVKRtuQxmxMJdU69ARHdNfbhr6rkO0/kZQVss2AzCXbDxZo0oKvJrwX/uTJYi/A4iY0oUEihd+C2XY7cM+l2ZEj5F464wg4OAaHzJCSrRDRE9wPZMm/Lr7qK/I7BZv/Dmu3994U8xEPtX3hjbPefYXVrOhzt8bML76RxtnTvehGVpeqX/v3DaNCO4FI31tPxNbe/4vACR/I7QPYI+Awi2jn9HUE79HbHnOsy2I7ruk88uS8l+Kj4UIKRZ+Lxk0fK3tZwYMPOJn3aoS4Z8Pddb+rf07ZPH5x5xfIW6x3cAce/6WwMCZdeB+MajPmv6+roqEF/WagWiAlGBMH/2qEB8fV0ViC9rtQJRgahAmD97VCC+vq561KMemw56ihKen9CnF36usvPfTfZJj12h7NITHeJWLNUYu++RN5yXz20bNU8f29SctvApfJbMq0+J7tRQGoNgw77Y301H5CZv46HojT6l3GWD1M9K7gpltXQbDW1r4NJfqjEHImZS7ISyETrdJkC00Z/2U0BsqyG0PKs26Wc4s1sccSP92mVAzK7SEhCmDEQhhxobTCkDYb4SiL44jL6BORAzes2dGr4aCP89cBkmP1LCZNizCwX8hTwlZUEQEHmOyX8lbpr9efpPOA/gVpRqBGOi76nVnVGmBJwzTxc44ffMv6ADtYE7wXVw3ZNdZ/rGrqFX2MY1n5GHvGrcrz4wNMht+lgNoecvUocu7fuLuqa4jjfqjwKxB4dkOkulyQI++7o3zBh5NYHLQAbGTAb4m+AkzFNSFgQGNs3RtjyIbStzAB5ecCtKNbZgA/VwdipTwghLiMrBsYlOg0HxLH4xNkXcpFSOvWzPVPYfqQfuSPZGU6tntNd6x+ojNfTcMtyqqEOXdnHEtmB2oD9+/rfiscaI98G5yv7xPuYytCmTYUJU+Ao5x4Gvw6jkzszLUouU2vpdZM88CpkRppCf4dmzn1eZEsSgAJNCz2mn95470XO9bAt9MhggykHcCQzM3jv52dFv0F4rG+ljNYC1gf6EOmhEyPU52SjtoD9hId6/UQ6Q0vr+5a0AhHAZ2pjJwLQo4h9MMyLlOMD5HrMgFIg4R8tLJwyS7Zx9u7JHXAr54Xi/UItgSuxkDsgs4dZ0RsiQvpkwwFTr9P9nHYSpVgxjz9aC2MWDeO5lEM1FgdheA3KjP6EOw2V5kZba6LKJgUB/X94AhNxZngHEtEAGLkPKZHgWxgHtEfOUlAVRygFjiefgFyOa3M+zpSksW7I0UYtgShDrh3YFLJC8qDyT8bybPOuiwG3xt8lFz+VaMIuesbDgLNV4ZqIM+tUKwYZqe6QGZV3osop5iVr80tSmM0JSn2VpalusB2cwAZkxMl2rnsuQMhl0sza8A+QpKQuilKPF9vS6480a3ImdcCvK+Xnje0WLYEq8XHgTM2BR7GSz3mOiE3tDmNQtf6Pazlxuh612quvMO9UZeabUJ3AwWv5+ZhsfrAH94NsYsojrgE17v+HvpD+w+0XawOYvI/6qLVO/XjaxOIqMh0cYCfMbuPWjT340zRkX9xz3/Wa7lnL3D/RtS4llwn/Y8z4JxHceenWs/T7/CiC+s4ZH+7qZ11SPetSjHvWoRz3qUY961KMe9fiLj5hK8jitZLnE30pR+WN2VyD+iN3x85Y9u0VYSkHSCkbYwqcogISWcNG/2iHS4oAGuZ47JaGR9Vt2dpx/ghx6l53RaDCF82PaCege+zyzulPlWutIQ6Tx9buiXWl0GLbulvciVizJnu710TNT+BBYSsGo9oV12Vjb9J85EDYoP2RATN/wTmkyBKE4fWsicQqcDfFdIgUPp7Hh4+GzEg2mcD57yw+vEhup3xmzUALBeZoBsvQcMsGYOJBJBF2IDoN3aBvpj49kY7J3BnMgzByIM/LtSkC4IhCay90Bwt0DwmwDYjAZEMdo+JbOp+amVsxKmAAEBVPQ15I3AGFiIEwOhGhN5OwC+mQSJsKZ3vUP2hesSODjq3BcElbLUPHzG6tn4Cy0MyANhKgiiHUCvY/3d8oHIDT+iTPIoSGeWJEgOysaHvIiMel+kMBKJ0D4M7bDS8EkpV44L5FNJKYLtXDQ72zdrIRRC2hGkIYIZkToVSORW1D2MDYeJhopiSrgOonDonmEQTFjF7RQWomYCGeSUgjaF3x1WyvxVTTaCmviH42EMoCqB16h1xmBCCduQNwR+vz+fpSoJojjQimnQaKhYB3l6EdT3clZaHjQ2+8nEVuZajdYmsIZd0Uoj4aXv/w8NEicxFzBG+JH/3I9fcpL0NDBAuoRaYio5dqrxnywfAXKqrKJvFUukWcQOoGDh0geoX/k7IK9eVH+XhvPiKB9wTFJfCQRjiniQ1h007WPoBmWo41QPp0R0rxBZBEEMDCijO9EAYHFhiQaCq6lZsAeEZ9tfDCRg0HUEpoRACKccUeOfEL6IrfCeUQ24bOONlEewI7PQ0MkL2GsWsC3H6qY4EKvCIjuRpYeJLaM8wG5EHHGIRYH917zKA8nYxeYC2ZEYCJgjwjaF1jvfSQRUtAYjwIEdepDlTxGaDj5PQIzaHAaqWosA8FRVGZAxGcxB3kI6Cyu2kE263BGQoAgatHsvFHr6TvdNLDAxKhAuFkJY9WCBIioVw33naJpOI7oEqLDWB9xxg4AYmpJ8nhCVMYu4OEnNlwCRB+0L/ygSnyVgaOtWMQkOWJpwllqfoiXJtHTsLgTsohEVwDCRx+ZvjmOzpKclfguDiGXpjV5iO6awhkJytRwtJfZeQ2YE/7TpUnljfISPioKKy8kM0J79UEaJvKdo3VJeWsl4gyAQG2SxwijYcYuYJ6fCUwE/I5ItS84GolGVeHoJQ026yuirBAAnGcc/C+Pw4h1FpFMLHQ+RiuxjKxEJKHfFVDf6Pg8raXNmJ0dOVjTVWKhWEgbnUZV/9AzFMOEo8Fc5+cRb0UVRBCQ7CBxVhAQLS+hqiCQTupGv0f4Xp2o71evSmJOUh5xV9i+W4ONO86z9lMudaLf/2XtzPJRKtGs1LVep5un2llOu2CPXbNzawl3z24b3wKvt3jXnu8G4l7K/SMGYj58S8P6cDP2cbv1F7gz9zVJ3GPW1GdNP8TuCkQFogJRgahAVCAqEBWICkQFogJRgfj7jzoUFYjvOnKGQs4umKfr52WWQ/LZruaw92pxwdKoTXI24ennvNT2Hnwu79qoPZYv9fFHscltVLgppMeVl1gO8zwxEMUc9l4tCkTKrOCw9955n5ba3oPP5V3Op3FftuZLgFCGwhIQcfoSEMt5tNJCDpMDUc7j0m8zIOaltvfgc3mX89kH8yUDrgyFE7MyiI2QMhBCOtwmN+E/HAosh3melRxgglxXa1lkVhD/g3xepVLbe6C9eCTvco87ifvSbc6HPEr1EIaChWd1xkAI6RxGQDga9P85y2GeZzkHOBGTW35czbPArCD+x+ShW2h7aw+YNfJg3iVbKY1dwEX+RzmfeLRThgJYGcS6SRkIIZ1ZD8LRoBxzlsM8z3IOcCLWa1lmVhDtQDiCs1Lbe0DnH89bzncQHgd935pPlrCUoSCuexfYBXk6czL0GhHmwt08PqZWngOciPValpkVERDzUtt7YKx5NO9iPuFx8Pet+QBEylAgngFYETkDQdOZrXAk0ziGVoHlUMgzfVvKIXFbVmtZYlYQ/0PImsVSG3sgi/EjeRfzOSygyunYlI/HxqQMBeIZNJ4uGBgIIZ1ZD1fmaDBvYc5ymOdhAuJCDk6/rteyxKzgCC4ga5ba3tgDqvfRvEv5hDPimSpb8mFsVhkGbjU9Or8hz3Ir92tZyn3/l/XmHjyYd7Otm/JxnjWGgVtNj4HYkGeD0ebB3NuA2NSDB/NutvVxxsjfeNSHfhWIelQgKhD1qEBUIOpRgahA1KMCUUGrRwVCjsGGf50/qywDfvZicyZFSG/+jb93q6nxp8EupaSf57yMZiyllHUv4rre35ux4Se1SUnPrwi9c6b0zOm+SsdaOnnkSK+D3uVrhhPHEKd36brx3xt9//fGQ2JnQEiUdTRhA5OiiTzdjRQJdJYmK52mximDaEWkKabIfwjnWb/CllJKFqSfKPq8dak1JmKXaN8DTyR9XndPpSMev1m6Q2R6w74M6+AJhMOsIb8XObfGIzU5HudA+DM2ZlLYtKNudRjc0rCgPVtIyfIng9GYiCORXbG57sUcCE+9MTMgTIknkgFhllU6yjMiVgiBQ0veQWfPuwAhr+1Pdl0pcP1I/96EpYCqO+YYOCOqEsykuI0qtKPpxkm+VHWikAqugqZc+fXXU55C9d9i/oOTl287g/rxcvA8Za57QecGe4gYHwEITQutzHkipPGhfJClPMF2KHEsqniQFgKAcBj2wxyID3b/fbB6BlgKsrJavGLOjkxhUnxYXZo03Tjgm7IgSqnw3mqKCkTEKWKQi/kP3ajXEOpHXfOUue7FdM3ZIWKZMPXGNkla1ErGE4G0hBNNkXKeiKWBuhbTSSGEmBzYI9jmK+8RA4RaTiMDQXwC+pcpKDcVXSDVCLi26Swc+AEITcdQKwsjXI+l1DiF2ksZEQfWxJgMGmL+A7E8Olac0UH3y1mSknM2iLIG9kVgfBxFJSCkhVZynogAwY5+kblaU+lgC5bTaUbQ5cwqNvwyPwF0YMmJhvke1G9mGAyqnuEH2tkAhDNzIJCuQ52xIAqpacrA0zJV0ZhaUREdf5ZdjaMZS0DEKTlnw/A1+hHxQ4hooED4NBMDkfBEIiBk5V9X6aCU5fRBrMRYyOXCPZbxARDTZBrC0mSgfkEMOtGpOKpGBQNhk3Qd6pQFUUhNUwarGhlBRQMDNec/KAQBiHnKnLNBHMDrNWV86F2TTwvskpwnYvn2RgBbyJNbuZwe3zU5D4RzRseHgSA+Af2L7cvyvThpXAjHYISuBDEpBlbESNIHbLkZC6KQmqaAv9FkTIduxL11OCsbskhMgR/XjfOUue4FtQRwA+NDf0domjIo5jyRbvQaI9a3t6jSoRvzsopH+B3hZPfgs7fOz/XC4cz64VbOr3BCHk25wy65Z4HL6op/WbsHensnzz2WRpL+7638+YsPZ1Y5IfbBFPtrFrisriUgfvGwv5j+/3fUh34ViHpUICoQ9ahAVCDqUYGoQNSjAlGHKbL3PwdvBYKPLSyDyZEzZHnpdddBXpG39BDrJE9I8UArznkakS8/T89zKTdzLIRo4FMXauNPGociPntjG7gH91PhaiUvWTN0ozp4lvPz098h2KtATHaPTbEsxu7fGz+F3ZRjE8vAadSQkFc8ByqA7vXr5aFultM6cTrG5weRZyeORQcgfOpibaw07/Kz1vny1t1P9TbJ/+7m57zBXgXCiqZGoSyPnbqEtuTYxDLw70qHvOpC4fwA4hgPXZQzASI+77Rjjh2KcepibUMEhD97jDrnH4WvpLpYnJ0jsWzJPx5zIDBGxbKmuWdNlmMTy0CBiPLyX80PIA7R0MU5YyDi83YIQOBTSF2q7WACENHZAhBrqS7y7NFFsCm/u16XgCiUzYb5fo5NLAPyLeV56a/ml6Xpehp16NJaseLn5xujHTvIHhFSl2o7RkCEs1PeIQdiLVVoESP7zMzxbn7sER+zpemAwDSFshg7hNMxm3JsYhnEM0Ly8l/xux4FiKOyh9KcB6rNHmbnw4w42jAjkLpUWz4jpK5juIYpCNO9VKVFcIwWc7hfG8+eORBHBKYplMXY8X5rzaYcm1gG8R7h4gXHZJu1iVd1n3OgesNK7OZ7hEZV8amLtWV7hLe8uEcspQbr0z1isTYEJpkB4UN5zMoajeIiw3wnx1aWAUeKGgp3TUUgZjkd9D3y8+gYOBZN6a6pVJvcNWVno/scDvCxmhrPCOwUd/NHd01kLxbVCIjiPZGyTBZqj3KYrSwD+h3RFX5HOIPfERxXZOQhIFLLLKcwLQq/IxoDjsVpnP+OKNXGK/uN6GX5vTlim/CZO6mGqZ3M+xhKls3z86+OIbJ3oP9YDaTwOyLeAUAMvZcjcaTfYxks5Z2fcQ+dL6lqbCk1P7s1dW79I/nJXp0RS3bnJe7l+CFHfcRRgahAVCAqEBWICkQFogJRgahAVCAqEBWICsR/ZZD/s0cFYuPBzzJHpzIQRa4Bx2AehlU+QpwnrpNCiiMW9b2SxuAtOrfBkpB3zi+5m398PP8jo4Tc/H083bblN8rW8E0sPDWnKOvDKh8hzhPXOT2pZMGP5m5J45015oG8BX7Jvfz28fyPjJKejVxUd/NHQEh3FrgGlGc8SrV388R1UiQgsqC5W1I7f7Tmgbxzfsl35H9klApA3M0fDZrlCb7ENaA81yuqvZ8nrpNmxHRuaO6W1M4frHkg75xf8h35HxmlORD382OPID+UydkSMdcAbIePVT5CnCeuk/aIwwgg1kuK921wV/NA3jm/5F7+0/h4/kdGCbkJCPq7Lb8BW0PZOUtcA8pz4ugm2/LEddJd05yjUSwpXCoIJ2zNW+CX3Ml/MI/nf2iU+BP1+LDINsnzG7A1QhMLXAPKY4d1PkKUJ66TgJhzNIoltfNbWvF5P5F/eDz/Q6PEn3hpGtzG/EbYGtzECtfAocgqH8HnSesEEGsMUS0ZOn+/lWSg3Hfnf2yUkJuBcG5jftzj3hwTnda4BnRHrboU9/KkdRIQpFDR3C0JnQz63D2Qd84vuZf/0fqV07J1lPTsGp8kz3+XLZFzDbbkSess/bIul9zKwZh/++78j43Sr7X1bUd9xPFDjgpEBaIeFYgKRD0qEBWIelQgKhD1qEBUWOpRgfj2I7ANNnMhbkjfkpdSRfOCPFyiupG/dTZIafo7kL7HjGcBhRA6i3fZ8FZbrI5Rzt38izf87qUXlEZ8noEF5edt0Ft4eJsvTelGvJ+nGs33UsEPMcG3upkLYVa0K+Y8COc1vLkQqVfk72EOUnpglgSe0uf1Rwoh1hXUMcq5jYs0/FfSS73XPIONHEJJj7kfRZ5GrFp+L1VcWxEQ27gNosGxMa9V94o60qOAGr70KO5D+jseS7ao6aHOXB2jnFsjWNxLX2uRrHKlNnBWBVhihRFxIXFcibupC0Dc5RuIBsfGvFY8W9GMUCBCaWJ+0Bn6e72GGRGpdiwB4dUxyrkx0Ad7L32tRbLKldrAWXGGJsojfqgP9n6qjH1gG2zjQnQjNDi25cUeQSr2pOZBaySpV3SZesWHRekPVrugfDPVDn6Tm86KhsaQq2OUc8vSc72XXlAa8Xlg0bwNKJVQv9KUbsS+yJfkNee9zFPBD4nYBtu4ELQmIn1TXloXLSSAdEZoZJNQ+kPm0wfCibgCz4KdlAcTz4hUHaOcWwb6eC+9pDSieT5sib2hSiV2xtPgXcOKeMYx573MU8EPMYFtsI0LYWSz2ZiXUpkHEe0RM/WKbLMu8SywuA0zIGy0WRdyJ5v1SrpbyZNs1pEiCICYq4IkQz3jvcxTwQ8xgW3wALfBbs6rd01OGQ0l9YrCXVNB96Mx0lLhrmkxtwz0vfS1FhWImWqJER2rAk8jvi9aT1V+iAlsg61cCPyO2JZXV0Sw5ojRUFKvGKR0+B0x41kYVQiJf0fE6hgLuSWmy730Qu99HvyOKLE39HdEt/pLobvzOwL8kOSIv341z0JL/Yp6xZo6xnrue+mfr2M95X7qH1PkqI84fshRgahA1KMCUYGoRwWiAlGPCsRfdFQgKhB12H5uexWIH9LeDzrm6hKsSTzgpcTAudCouB0/b6Snnw7f6R9rfJxa+KtC7caYLIat8586/6kZFQiKck0sEHg3jA0t4/molLehSn7yOz3T/1ee+f7tQMRPzK1qagTOhfXdFteGDCi7ZSIgzGCMhsm2HohwJgai8X+t5OA47DTMLgABl41lCwMQTQSE46gnjfnLgcjVJeC4GOzRxq7+CAj4TTwQZgaEmQFhVoEIGdhtJH4zGKf59YIoAwHHlNJ8/lIgcnUJBeKgQBybdCwHEnToxulfkVDvrhRoPgDRmQN/R8h5Q+mm48/kDjmMUVmEtSfVfv487REExCCDPlBtGpaJZgQ7mRy3xlbDsRKAoL5Q3X8lEDk/AT6ryWV+Fc7FNQFiWsumuTI4izhEjbjF4aDXpenI16bVOXYEi4dWdmbzWCtljZDPTqLKPwHhDtMuoWEQsDs5LnkSvy9HojjKzLMc02sAEPDzHf/O5WnOT6BrklgYNLTEuTDJ0sTbqI85RF1X52gMhBMgIPnP0vzkOb5ZuCBvVlruJjcjRduyPIwdK9lQi7ocYnuYUm7Wx3I48WvoMiNumBHkbNQoFu7vBiLlGfCMwNKUAcEKAh6InJ4WgCCXuTUaBKMhwhzNBBFVUCCmUqMZAcSUc8TNgQIhDAmu3Xq2IFgiYrVNl6a/fEaUeAbXKw0mA+EaAYAHnYfXehpNWJokcJPOCMM3P9HSdBjd0Ur0LSvwcEkrQPDS5OSujckEfNlzCSrpmsALOYrVxzkQf+keMecnEDMP3RXOhXD4EEOElhVS1uhG3U+mDZg3a0kfwJKjbbSRv92VOQvT30a4C9agLJec/k47wIjNGhwH/I4AELydTyURBVKYEVfEgumuwoKwCJqEefTXHrHpyzoaGqps6UjT5yv1lgF65Je1e/D8X318/pGD/h7+PiAeBej/FIi/o70KxA9prwLxQ9qrQPyQ9ioQP6S9CsQPaa8C8UPaq0D8kPYqED+kvZ9z/A8ai5htTtE9IQAAAABJRU5ErkJggg==","mimeType":"image/png","cols":196,"rows":71,"chars":7554,"font":"8x13","variant":"bw","lineRepeat":1,"detail":"original"}],"totalChars":132799,"truncatedChars":0,"text":"# User ¶█Complete the assignment below, thoroughly:█# Target█Brain loops in crates/spt-daemon including project index, registry, dispatch, stream subscribers, and state scans. Non-goal: broker PTY ownership.█# Change█Find loops capable of reading ~64 MiB/s or polling at sub-second cadence. Check whether any loop repeatedly scans repository/transcript/log files under active sessions.█# Acceptance█Return ranked mechanisms with exact cadence, files read, citations, and live discriminating probes.█# Assistant ¶█_**Planning read-only investigation**_█# Tool call ¶█//Reading diagnosis workflow█read(path=\"skill://diagnose\", selector=\"1-220\")█<out>█\u000e---█name: diagnose█description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce -> minimise -> hypothesise -> instrument -> fix -> regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.█---█# Diagnose█A discipline for hard bugs. Skip phases only when explicitly justified.█When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.█## Phase 1 - Build a feedback loop█**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause - bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.█Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**█### Ways to construct one - try them in roughly this order█1. **Failing test** at whatever seam reaches the bug - unit, integration, e2e.█2. **Curl / HTTP script** against a running dev server [...5116ch elided...] # Phase 6 - Cleanup + post-mortem█Required before declaring done:█- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)█- [ ] Regression test passes (or absence of seam is documented)█- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)█- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)█- [ ] The hypothesis that turned out correct is stated in the commit / PR message - so the next debugger learns█**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before - you have more information now than when you started.█\u000f█</out>█# Assistant ¶█_**Locating crates and daemon code**_█# Tool call ¶█//Mapping daemon sources█glob(path=\"crates/spt-daemon/**/*.{rs,toml}\", hidden=true, gitignore=true, limit=500)█<out>█\u000e# crates/spt-daemon/█Cargo.toml█## src/█nethost.rs█dispatch.rs█broker.rs█attach.rs█brain.rs█msg.rs█seedproofx.rs█registryhost.rs█lib.rs█endpoint.rs█sync.rs█projwriter.rs█brainproc.rs█applyhost.rs█docshost.rs█daemon.rs█config.rs█update.rs█relcache.rs█release.rs█notif.rs█relay.rs█propagate.rs█livehost.rs█inject.rs█conn.rs█autostart.rs█stderrlog.rs█lifecycle.rs█digest.rs█pairhost.rs█xfer.rs█shellwake.rs█shellchan.rs█seedmap.rs█resting.rs█resthost.rs█linkhost.rs█effect.rs█adapter_update.rs█wan.rs█tunnelhub.rs█transport.rs█translation.rs█test_home.rs█shellhost.rs█shelldisc.rs█service.rs█serveprobe.rs█rollback_compat.rs█reconcile.rs█reap.rs█psyrelay.rs█presence.rs█notifsync.rs█machineid.rs█harnesshost.rs█grants.rs█frame.rs█drivehub.rs█digesthub.rs█deelevate.rs█deadline.rs█crc_swap.rs█consent.rs█codec.rs█access.rs█### pump/█mod.rs█health.rs█update.rs█sync.rs█registry.rs█notif.rs█### bin/█xlate_choreo_fixture.rs█## tests/█inject_control_wedge.rs█attach.rs█redispatch_stall.rs█false_promote.rs█conn_blackhole_lifecycle.rs█broker.rs█brain_decouple.rs█mesh_recovery.rs█redispatch.rs█input_ack_deadlock.rs█docs_server_e2e.rs█propagate.rs█pumpdeadline.rs█pump.rs█net_worker_starve.rs█brain_resume_conn_deadlock.rs█two_origin_spanning.rs█wake_single_flight.rs█endpoint_survival.rs█xfer.rs█wanmsg.rs█twohost.rs█replicate.rs█sync.rs█notifsync.rs█netstream.rs█netbroker.rs█idempotent.rs█dispatch.rs█daemon_e2e.rs█access.rs█shellchan.rs█seedproofx.rs█rosterprop.rs█resume.rs█reseed.rs█psyche_residency_expectation_e2e.rs█psyche_event_turn_e2e.rs█psyche_context_file_e2e.rs█presence.rs█pairjoin.rs█mesh.rs█legacy_resident_sweep_e2e.rs█handoff.rs█driven_by_selfheal.rs█digest.rs█daemon_lifecycle_real_brain.rs█control_stamp_lifetime.rs█budget.rs█brain_swap.rs█### fixtures/█dispatch_fixture.rs\u000f█</out>█# Tool call ¶█//Finding polling loops█grep(pattern=\"\\\\b(loop|while|interval|sleep|tick|poll|scan|refresh)\\\\b\", path=\"crates/spt-daemon\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## Cargo.toml█5|publish = false█*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) - the `spt` binary is a thin CLI over it. B0 lands the crate + the version...█7|█8|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for█9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /█## src/█### applyhost.rs█34|//! `apply_brain_only` handoff, the regression that re-attached a CLI-side█*35|//! subscriber while the live brain kept running the OLD code.█36|//!█37|//! New code runs **immediately** - the supervised respawn lands on the new█38|//! executable, no manual bounce, no logon. Readiness-gated promotion +█...█134| // failed readiness on this node and auto-rolled-back is quarantined - never█*135| // re-applied, or the self-heal becomes a self-harm loo [...48942ch elided...] 85| const NUL: u16 = 0;█...█213| let mut buf = vec![0u8; buf_len as usize];█*214| loop {█215| let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };█216| let mut result: *mut libc::passwd = std::ptr::null_mut();█217| let rc = unsafe {█...█332| let mut buf = vec![0u8; buf_len as usize];█*333| loop {█334| let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };█335| let mut result: *mut libc::passwd = std::ptr::null_mut();█336| let rc = unsafe {█...█716| let mut n = 0usize;█*717| while !(*p.add(n) == 0 && *p.add(n + 1) == 0) {█718| n += 1;█719| }█[Showing lines 1-958 of 1175 (49.9KB limit). Use :959 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Finding filesystem reads█grep(pattern=\"read_dir|read_to_string|File::open|OpenOptions|BufReader|metadata\\\\(|canonicalize|walkdir|WalkDir|fs::read|tokio::fs|std::fs\", path=\"crates/spt-daemon\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## adapter_update.rs█389| let src = tmp.path().join(format!(\"{name}-src\"));█*390| std::fs::create_dir_all(&src).unwrap();█*391| std::fs::write(src.join(\"manifest.toml\"), body).unwrap();█392| src█393| };█394| let plain = seed(█## applyhost.rs█261| let aside = swap_aside_path(exe_path, version);█*262| let _ = std::fs::remove_file(&aside); // stale prior apply of this version█*263| std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;█264| if let Err(e) = land_artifact(exe_path, artifact) {█*265| let _ = std::fs::rename(&aside, exe_path); // roll back - never half-swapped█266| return Err(format!(\"land new binary (rolled back): {e}\"));█267| }█268| // The binary IS swapped from here on - but DO NOT claim `applied` yet█...█444| use std::os::unix::fs::PermissionsExt;█*445| std::fs::set_permissions(exe_path, std::fs::Permissions::from_mode(0o755))?;█446| }█447| Ok(())█448|}█...█625| .collect();█*626| std::fs::write(█627| &keys,█628| serde_json::json!({\"keys\": {\"k\": [...31736ch elided...] /C exit 0\";█1079| #[cfg(unix)]█1080| let spawn = \"true\";█*1081| std::fs::write(█1082| src.join(\"manifest.toml\"),█1083| format!(█1084| \"[adapter]\\nname = \\\"Gated\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\█## bin/█### xlate_choreo_fixture.rs█36|█*37|use std::fs::OpenOptions;█38|use std::io::{self, BufRead, Write};█39|█40|/// The fixed, recognizable payload token the choreography types into the PTY.█...█62| if let Some(path) = &stdin_log {█*63| if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {█64| let _ = writeln!(f, \"{trimmed}\");█65| }█66| }█Showing files 1-20 of 73+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding task spawns█grep(pattern=\"tokio::spawn|spawn_blocking|thread::spawn|JoinSet|FuturesUnordered|watch::|broadcast::|mpsc::|Notify\", path=\"crates/spt-daemon\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## applyhost.rs█513| let serve = Arc::clone(&broker);█*514| let handle = std::thread::spawn(move || {█515| let _ = serve.serve();█516| });█517| ServedBroker {█## autostart.rs█132| fn replay_bounded(budget: Duration) {█*133| let (tx, rx) = mpsc::channel();█*134| let handle = std::thread::spawn(move || {█135| replay_startup_endpoints(\"bogus-broker-socket-never-dialed\");█136| let _ = tx.send(());█137| });█## brain.rs█25|use std::io;█*26|use std::sync::mpsc::{self, Receiver, RecvTimeoutError};█27|use std::thread::{self, JoinHandle};█28|use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};█29|█...█232| let (mut recv, send) = stream.split();█*233| let (tx, frames) = mpsc::channel();█234| let reader = thread::Builder::new()█235| .name(\"pump-ipc-reader\".to_string())█236| .spawn(move || {█...█1843| use std::sync::atomic::{AtomicU32, Ordering};█*1844| use std::sync::mpsc::{sync_channel, SyncSender};█1845|█1846| // [unit->REQ-PUMP-DIAL-FASTFAIL] the peer-reply read budget is DECOUPLED from█1847| // the 30 [...18060ch elided...] 528| watcher_run(&owlery, &owner, &shell_id, &tokens, &params, &stop, || {█529| resolve_wake(&owlery, &owner, &shell_id, &adapter_name, &shell)█530| });█...█553|pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {█*554| std::thread::spawn(move || {█555| let set = Arc::new(WakeSet::new());█556| let params = WakeParams::default();█557| while !stop.load(Ordering::SeqCst) {█## pump/█### mod.rs█1250|) -> thread::JoinHandle<()> {█*1251| thread::spawn(move || {█1252| let paths = PumpPaths::production();█1253| supervise_pump(&stop, SUPERVISE_BACKOFF_BASE, || {█1254| run_peer_pump(█Showing files 1-20 of 64+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding project registry dispatch█grep(pattern=\"project|registry|dispatch|subscriber|stream|state|transcript|log\", path=\"crates/spt-daemon/src\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## access.rs█4|//! order: subnet *visibility* (is the endpoint routable here at all? -█*5|//! `spt_store::visibility`, applied inside registry resolution) -> the█6|//! **endpoint access whitelist** (may *your node* remotely reach it? - THIS█7|//! module) -> *capability grants* (may this agent do this high-risk thing? -█8|//! [`crate::consent`], default-deny). The whitelist is the outer gate before█...█10|//!█*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)█12|//!█13|//! - **Origin-node gate:** the subject is the node the inbound interaction█14|//! *originates from* (where the operator is acting) - never the sender█15|//! endpoint's identity or home. The subject value MUST be the QUIC█*16|//! handshake-proven remote node id from the broker's conn/stream table,█17|//! never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).█18|//! - **Outbound** -> any visible node: the whitelist never restricts who an█19|//! endpoint talks *to*.█...█22|//! (\"established/related\").█*23|//! Classification reads the durable [`RecentOutbound`] log█24|//! ([`classify_inbound`]); senders feed it via [`note_outbound`].█25|//! - **Unsoli [...49026ch elided...] 4|//!█*5|//! ## On-demand projection, not a byte feed█6|//!█7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser█*8|//! on every output chunk. The digest is now a **projection of the same normalized█9|//! history the echo-commune consumes** - so there is nothing to feed: a snapshot█*10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's█*11|//! records via [`project_endpoint_digest`], and the hub publishes the result.█12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces█*13|//! read the logs.█14|//!█15|//! ## Sources (one per endpoint)█16|//!█17|//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or█[Showing lines 1-933 of 1154 (50.0KB limit). Use :934 to continue]\u000f█</out>█# Assistant ¶█_**Investigating project index and brainproc dependencies**_█# Tool call ¶█//Reading brain loop wiring█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"138-333\")█<out>█\u000e█/// The `spt daemon brain` child entry. Connect to the broker over the existing█/// socket IPC, signal readiness, host the brain-side logic loops, and idle -█/// holding the connection and probing broker liveness each heartbeat. A dropped█/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)█/// returns `Err`, exiting the process so the broker-side supervisor respawns it█/// from the (possibly swapped) binary.█///█/// **D2-1 - net consumers run here.** The inbound dispatcher + outbound peer█/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no█/// shared `Arc<Broker>`), so they belong with the restartable brain, not the█/// broker - a brain respawn re-spawns them from the swapped binary. They are█/// meaningful only once the broker-owned NetHost is up, so we gate on the█/// existing `net-status` verb (which also serves as the broker-liveness probe)█/// and start them exactly once, on the first `enabled` reply. That single gate█/// also covers the boot-race self-heal (REQ-DAEMON-9): the broker retries the█/// NetHost bind in the background and `net-status` flips `enabled` when it█/// attaches, at which point the brain starts the [...10093ch elided...] tatus` reply rather than the broker's identity load.█// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█// [impl->REQ-DAEMON-9]█fn spawn_net_consumers(node_hex: &str) {█let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));█let stop = Arc::new(AtomicBool::new(false));█let _ = crate::dispatch::spawn_dispatcher(█broker_socket_name(),█Arc::clone(&registry),█Arc::clone(&stop),█);█let cfg = crate::config::DaemonConfig::load();█if cfg.peer_pump {█let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);█}█}█/// Connect to the broker, retrying briefly while it finishes binding.█fn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {█...█}█[Showing lines 137-336 of 2116. Use :337 to continue]\u000f█</out>█# Tool call ¶█//Reading project writer loop█read(path=\"crates/spt-daemon/src/projwriter.rs\", selector=\"1-760\")█<out>█\u000e//! The materialized project-index WRITER - the daemon's derived-state█//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).█//!█//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of█//! `$SPT_HOME/index/project-index.json` (format + read path owned by█//! `spt_store::projindex`, W1). One brain-hosted thread:█//!█//! - loads the persisted index at start and is READY WITHOUT WARM - the boot█//! reconcile runs in the background; a cold daemon never blocks on it;█//! - reconciles with BATCHED work, `O(P + B + F + C)`: ONE branch enumeration█//! per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE█//! tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per█//! distinct normalized cwd (a stamped cross-cycle cache; ordinary commits█//! in a project repo touch neither the path nor `.git/config`, so they are█//! a NO-OP by construction - ADR-0037 §3);█//! - publishes atomically via `projindex::write_index_at` and preserves█//! last-known-good on ANY failure (a failed cycle changes nothing on disk);█//! - drains + coalesces the `projinval` event queue (multiple invalidations ->█//! one refresh) with a debounce window, keyed on the [...30144ch elided...] ntext in `project` (a p-* membership row).█fn membership(&self, project: &str, id: &str) {█let cs = self.store();█let file = cs.project_context_path(project, id).unwrap();█std::fs::write(&file, format!(\"{id} in {project}\")).unwrap();█cs.commit_project(project, &format!(\"{id} slice\")).unwrap().unwrap();█}█/// A plain (non-git) project dir whose folder name becomes the id.█fn project_dir(&self, name: &str) -> PathBuf {█let dir = self.home.join(\"workdirs\").join(name);█std::fs::create_dir_all(&dir).unwrap();█dir█}█}█fn global() -> Coalesced {█Coalesced { global: true, ..Coalesced::default() }█}█...█}█[Showing lines 1-763 of 1270. Use :764 to continue]\u000f█</out>█# Tool call ¶█//Reading dispatcher loop█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"1-260,400-570,1080-1220\")█<out>█\u000e//! Inbound net-stream dispatch (D9-1) - the daemon-side acceptor that turns█//! the hand-wired serve loops of D5-D8 into production behavior.█//!█//! Every E2E test so far drove the serve half manually (a probe brain calling█//! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against█//! a known stream). In production nothing did - a peer could dial and write█//! and the bytes sat in the broker's stream log unread. This module is the█//! missing consumer: a daemon-hosted loop that watches the broker's stream█//! table for **peer-initiated** streams, classifies each by its first NDJSON█//! record, and hands it to the right serve loop on its own worker thread.█//!█//! ## Direction (the gotcha-#9 fact, now first-class)█//!█//! The stream table holds locally-opened outbound streams too. The dispatcher█//! claims only rows with `initiated_locally == false` - serving our own█//! requester stream would deadlock the requester against ourselves. The flag█//! is broker-recorded at registration (`open_stream` vs the accept loop),█//! never inferred.█//!█//! ## Classification (first-record demux)█//!█//! Streams are single-purpose; the opener's first record names the protoc [...24026ch elided...] d against the canonical broker█/// socket - the daemon-boot entry ([`crate::daemon::Daemon::run`]).█pub fn spawn_dispatcher(█broker_name: String,█registry: Arc<RegistryHost>,█stop: Arc<AtomicBool>,█) -> thread::JoinHandle<()> {█thread::spawn(move || {█let paths = DispatchPaths::production();█if let Err(e) =█run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)█{█eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");█}█})█}█#[cfg(test)]█mod tests {█use super::*;█use serde_json::json;█// [unit->REQ-NET-1] the demux table: every family's first record█// classifies correctly, including the `request` kind collision.█#[test]█fn classifies_every_family_by_first_line_shape() {█...█}█...█}\u000f█</out>█# Tool call ¶█//Reading live reconcile loops█read(path=\"crates/spt-daemon/src/livehost.rs\", selector=\"1-180,570-700,900-1050\")█<out>█\u000e//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) - re-host the█//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.█//!█//! This is the production caller `run_pulse_loop` lacked: the brain instantiates█//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced█//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of█//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from█//! disk): same shape - enumerate Self perches, resolve each manifest from its█//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run█//! one bounded reconcile sweep at boot and every tick. Where the shell waker█//! hosts **offline** shells, this hosts **online** live endpoints - `status` is█//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the█//! online ones re-hosts exactly what was live before a brain (re)start, and█//! never resurrects a gracefully-departed (offline) agent.█//!█//! **Online-only is staged behind the interim.** The interim `spt api listen`█//! process still first-hosts its live agents today; those perches are█//! `status`-ABSENT (not \"online\") [...22488ch elided...] fflined), the surviving sessions at█// a brain restart (live agents stay, only the dead are offlined). The█// controllable==Some(true) gate inside keeps relay/legacy perches exempt.█// [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]█// [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]█if let Some(live) = query_live_session_endpoints() {█reconcile_hosted_liveness(&owlery, &live);█}█reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);█// Sleep in slices so a stop lands promptly.█let mut left = LIVE_RECONCILE_INTERVAL_MS;█while left > 0 && !stop.load(Ordering::SeqCst) {█let step = left.min(100);█...█}█}█})█}\u000f█</out>█# Tool call ¶█//Reading wake scan loop█read(path=\"crates/spt-daemon/src/shellwake.rs\", selector=\"1-180,450-580\")█<out>█\u000e//! Shell wake-watcher hosting (M5-D4b - CONTEXT §Shell sleep/wake): the█//! offline half of the online/offline mutual exclusivity. While a shell█//! instance is **offline** and its manifest declares a `wake_command`, the█//! daemon runs that template as a long-running **wake-watcher** child whose█//! sole job is to fire a wake; while the shell binary runs, no watcher does█//! ([`reconcile_once`] flips between them - spt-core owns the flip).█//!█//! **Exit-opcode supervision:** the watcher exiting with [`WAKE_OPCODE`] =>█//! the wake resolution ([`resolve_wake`]) brings the shell online. Any other█//! exit is a crash => respawn with exponential backoff ([`backoff_ms`]) and█//! **give-up** after [`WakeParams::give_up_after`] consecutive crashes (a█//! durable [`WAKER_GAVE_UP_FILE`] marker the next shell activity - any█//! (re)launch - clears; crash-bug safety, the watcher can never crash-loop█//! forever). One watcher per offline instance: the in-process [`WakeSet`]█//! entry plus the perch's [`WAKER_PID_FILE`] (which also lets a freshly█//! booted daemon kill a dead daemon's orphaned watcher before adopting).█//!█//! **Scheduling (KH 7.4):** every watcher gets its own supervision thr [...10618ch elided...] owlery_dir();█let adapters_dir = spt_store::perch::adapters_dir();█let registered = spt_runtime::registry::registered(&adapters_dir);█reconcile_once(&owlery, &registered, &adapters_dir, &set, &params);█// Sleep in slices so a stop lands promptly.█let mut left = RECONCILE_INTERVAL_MS;█while left > 0 && !stop.load(Ordering::SeqCst) {█let step = left.min(100);█std::thread::sleep(Duration::from_millis(step));█left -= step;█}█}█})█}█#[cfg(test)]█mod tests {█use super::*;█use spt_store::shellinfo::{spawn_record, SHELL_STATUS_ONLINE};█fn fast_params() -> WakeParams {█WakeParams {█backoff_base_ms: 10,█...█}█}█...█}\u000f█</out>█# Tool call ¶█//Tracing project index APIs█grep(pattern=\"ProjectIndex|project_index|IndexWriter|reconcile_project|index writer|fingerprint\", path=\"crates\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/█### src/█#### brainproc.rs█247|█*248| // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the█249| // brain hosts the SOLE single-flight writer - load persisted index, boot█250| // reconcile in the background (ready-without-warm is preserved because█251| // write_ready already fired above), then drain/debounce/coalesce the█...█253| // held for symmetry, the thread dies with the brain, and the next brain█*254| // rehydrates from the persisted index (warm start, fingerprint-gated).█255| // [impl->REQ-PROJECT-INDEX-WRITER]█256| let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));█257|█#### broker.rs█5328| // payload's HEAD prefix must appear in the PTY echo. A swallowed head (only a tail█*5329| // echoes) FAILS verify - the exact head-loss fingerprint. An empty payload (a█5330| // keys-only sequence, nothing echoable) is vacuously verified. RED-FIRST: make█5331| // `echo_verified` always return true -> the swallowed-head assert flips.█5332| #[test]█#### projwriter.rs█*1|//! The materialized project-index WRITER - the daemon's derived-state█2|//! maintenance duty [...18661ch elided...] | // The observability surface rides `daemon status --json` (index presence█190| // alone is not health), and the batched counters are on it.█...█200| let pi = status_json█*201| .get(\"project_index\")█*202| .expect(\"status carries the project_index observability block\");█203| assert_eq!(pi[\"source_generation\"], cold.source_generation.as_str());█204| assert_eq!(pi[\"endpoints\"], 3);█205| assert_eq!(█...█303| // (4b) CONTEXT: a committed slice gives ep2 membership in `beta` - the█*304| // store's own commit funnel fires the Global nudge; the fingerprint moves.█305| let slice = cs.project_context_path(\"beta\", \"ep2\").unwrap();█306| std::fs::write(&slice, \"ep2 in beta\").unwrap();█307| cs.commit_project(\"beta\", \"ep2 slice\").unwrap().unwrap();\u000f█</out>█# Assistant ¶█_**Investigating pulse_tick and lifecycle scanning**█**Examining echo_commune and event projections**_█# Tool call ¶█//Locating pulse internals█grep(pattern=\"fn run_pulse_loop|fn pulse_tick|echo|commune|digest|history|read_to_string|read\\\\(\", path=\"crates/spt-daemon/src/lifecycle.rs\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e 4|//! In the no-daemon interim model the long-lived `spt api listen` process hosted█*5|//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo█6|//! fires, graceful signoff) directly in the agent's session. Consolidation█7|//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the█8|//! brain runs it as scheduled in-process loops, with a durable, configurable█...█11|//! a re-host, not a rewrite: the heavy lifting stays in the already-pure█*12|//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],█13|//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is█14|//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 - the█15|//! resident `spawn_psyche` seam retired); the ordering invariants█*16|//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live█17|//! where they always have.█18|// [impl->REQ-DAEMON-1]█19|█...█26|use spt_live::{█*27| download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,█*28| sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,█29|};█30|use spt_runtime::{Manifest, ManifestRuntim [...37198ch elided...] ost█2457| .rest_event(Some(\"sid-1\"), crate::resting::RestEvent::Detach)█2458| .expect(\"apply ok\")█...█2460| assert!(█*2461| report.echo_fired,█*2462| \"active->dormant fires the transition echo\"█2463| );█*2464| assert!(commune.exists(), \"the echo wrote a commune before the flip\");█*2465| std::fs::remove_file(&commune).unwrap();█2466|█*2467| // dormant -> suspended: already resting - no second echo.█2468| let report = host█2469| .rest_event(Some(\"sid-1\"), crate::resting::RestEvent::Suspend)█2470| .expect(\"apply ok\")█2471| .expect(\"edge\");█*2472| assert!(!report.echo_fired, \"resting->resting fires no echo\");\u000f█</out>█# Tool call ¶█//Locating brain heartbeat█grep(pattern=\"BRAIN_HEARTBEAT|DEFAULT_PULSE_PERIOD|pulse_period\", path=\"crates/spt-daemon/src; crates/spt-live/src; crates/spt-store/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### brainproc.rs█62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.█*63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);█64|█65|/// Why the broker spawned this brain - the Q4 update-vs-crash discriminator the█66|/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the█...█259| loop {█*260| thread::sleep(BRAIN_HEARTBEAT);█261| // `net-status` is both the net-capability probe and the broker-liveness█262| // probe: an `Err` is the broker gone (or a future planned█263| // snapshot+self-exit, Q3) - exit for a supervised respawn.█### config.rs█4|//! In the no-daemon interim model the pulse cadence was a hard-coded█*5|//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)█6|//! folds the pulse loop into the brain as a *scheduled* in-process loop whose█7|//! period is **durable and configurable**, sourced from a small JSON file under█8|//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when█...█21|/// override it per machine.█*22|pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);█23|█2 [...7866ch elided...] 2637| };█2638| let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");█### livehost.rs█1100| DaemonConfig {█*1101| pulse_period: Duration::from_millis(15),█1102| ..DaemonConfig::default()█1103| }█1104| }█## spt-live/src/█### pulse.rs█30|/// per-endpoint override) is the M3 daemon's; this is the interim constant.█*31|pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);█32|█33|/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`█34|/// is threaded into the drop-file precedence guard (6.5).█...█43| PulseConfig {█*44| period: DEFAULT_PULSE_PERIOD,█45| protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,█46| }█47| }\u000f█</out>█# Tool call ¶█//Reading invalidation queue█read(path=\"crates/spt-store/src/projinval.rs\", selector=\"1-220\")█<out>█\u000e//! Project-index invalidation events - the store-anchored nudge queue between█//! mutation sites and the daemon's single-flight index writer (ADR-0037 §3,█//! REQ-PROJECT-INDEX-INVALIDATION).█//!█//! Mutation seams (session append, context-store commit, rename, fork, purge)█//! run in MANY processes - CLI verbs, adapter hooks, the daemon brain itself -█//! and the brain deliberately does not drain its broker connection█//! (brainproc's non-draining hazard), so a socket push is the wrong carrier.█//! Instead every seam drops a tiny event file under█//! `$SPT_HOME/index/invalidations/`; the writer polls the directory cheaply█//! (one `read_dir` - pure file I/O, no git), DRAINS all pending events, and█//! coalesces them into one refresh (multiple invalidations -> one reconcile).█//! Daemon-offline nudges simply accumulate and are consumed at the next brain█//! start - the same recover-by-reading-state stance as the rest of the store.█//!█//! Nudging is **fire-and-forget best-effort**: an unwritable queue must never█//! fail the mutation that triggered it (the periodic reconcile backstop█//! catches anything a lost nudge missed).█// [impl->REQ-PROJECT-INDEX-INVALIDATION]█use std::path [...7180ch elided...] r().cloned().collect::<Vec<_>>(),█vec![\"a\".to_string(), \"b\".to_string()]█);█assert_eq!(one.cwds.len(), 1);█assert!(!one.is_empty());█assert!(coalesce(Vec::new()).is_empty());█}█// [unit->REQ-PROJECT-INDEX-INVALIDATION] tolerant drain: a torn/garbage█// event file is skipped AND removed (never wedges the queue), parseable█// siblings still deliver.█#[test]█fn garbage_event_is_skipped_and_removed() {█let tmp = tempdir().unwrap();█let dir = tmp.path().join(\"invalidations\");█std::fs::create_dir_all(&dir).unwrap();█std::fs::write(dir.join(\"000-torn.json\"), \"{\\\"scope\\\":\").unwrap();█nudge_at(&dir, &InvalScope::Global).unwrap();█...█}█}█[Showing lines 1-223 of 229. Use :224 to continue]\u000f█</out>█# Tool call ¶█//Reading branch git scans█read(path=\"crates/spt-store/src/branchstore.rs\", selector=\"230-310,480-540\")█<out>█\u000e pub fn branches(&self) -> std::io::Result<Vec<String>> {█...█}█/// Every local branch name ordered **newest->oldest by tip committer date** -█/// the recency order a reader projects history in (REQ-PICKER-2). Same source█/// as [`branches`](Self::branches), just `--sort=-committerdate`; ties hold█/// git's stable secondary order.█// [impl->REQ-PICKER-2]█pub fn branches_by_recency(&self) -> std::io::Result<Vec<String>> {█let out = run_git_ok(█&[█\"-C\",█&self.dir_arg(),█\"for-each-ref\",█\"--sort=-committerdate\",█\"--format=%(refname:short)\",█\"refs/heads/\",█],█None,█None,█)?;█Ok(String::from_utf8_lossy(&out.stdout)█.lines()█.map(str::trim)█.filter(|l| !l.is_empty())█.map(str::to_string)█.collect())█}█/// Every local branch with its tip commit id, ordered **newest->oldest by█/// tip committer date** - the index writer's ONE branch enumeration█/// (ADR-0037 §2: recency order AND the tip fingerprint substrate in a█/// [...3299ch elided...] mit id.█// [impl->REQ-NET-3]█pub fn merge_commit_in_worktree(█&self,█worktree: &Path,█other_parent: &str,█message: &str,█) -> std::io::Result<String> {█run_git_ok(&[\"add\", \"-A\", \"--\", \".\"], Some(worktree), None)?;█let tree = run_git_ok(&[\"write-tree\"], Some(worktree), None)?.stdout_trimmed();█let head = run_git_ok(&[\"rev-parse\", \"HEAD\"], Some(worktree), None)?.stdout_trimmed();█let merge = run_git_ok(█&[█\"commit-tree\",█&tree,█\"-p\",█&head,█\"-p\",█other_parent,█\"-m\",█message,█],█Some(worktree),█None,█)?█.stdout_trimmed();█...█}\u000f█</out>█# Tool call ¶█//Locating session ledger reads█grep(pattern=\"pub fn read_all|SESSIONS|sessions.json|SessionEntry\", path=\"crates/spt-store/src/sessions.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 58|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]█*59|pub struct SessionEntry {█60| /// RFC3339-UTC instant (see [`crate::timefmt`]).█61| pub ts: String,█62| /// The harness session id that became active.█...█98|/// line is skipped, the rest still load (the degrade-don't-crash store stance).█*99|pub fn read_all(perch: &Path) -> Vec<SessionEntry> {█100| let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {█101| return Vec::new();█102| };█...█109|/// The last `k` rows (oldest->newest within the tail). `k == 0` => empty.█*110|pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {█111| if k == 0 {█112| return Vec::new();█113| }█...█118|█*119|/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]█120|/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps█121|/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**█122|/// `read_all` index (not the slice index) so it stays comparable across sessions.█...█124|// [impl->REQ-DIGEST-CURSOR]█*125|pub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntr [...967ch elided...] let row = SessionEntry {█310| ts: \"t\".to_string(),█311| session_id: \"s\".to_string(),█312| trigger: SessionTrigger::Boot,█...█321| );█*322| let back: SessionEntry = serde_json::from_str(&json).unwrap();█323| assert_eq!(row, back, \"a no-cwd row round-trips identically\");█324| }█325|█...█354| fn adapter_field_roundtrips_and_survives_append() {█*355| let none_row = SessionEntry {█356| ts: \"t\".to_string(),█357| session_id: \"s\".to_string(),█358| trigger: SessionTrigger::Boot,█...█374| &perch,█*375| &SessionEntry {█376| ts: \"t2\".to_string(),█377| session_id: \"s2\".to_string(),█378| trigger: SessionTrigger::Clear,\u000f█</out>█# Tool call ¶█//Finding repeated daemon scans█grep(pattern=\"while !stop|loop \\\\{|thread::sleep|recv_timeout|read_dir|read_to_string|std::fs::read\\\\(\", path=\"crates/spt-daemon/src\", selector=null, case=true, gitignore=true, skip=20)█<out>█\u000e# crates/spt-daemon/src/█## linkhost.rs█455| let mut decoder = ShellLinkDecoder::new();█*456| loop {█457| match brain.read_event()? {█458| BrokerEvent::NetStreamData {█459| stream_id: sid,█...█684| let mut decoder = ShellLinkDecoder::new();█*685| loop {█686| match brain.read_event()? {█687| BrokerEvent::NetStreamData {█688| stream_id: sid,█## livehost.rs█297|fn read_resume_pid(perch: &Path) -> Option<u32> {█*298| std::fs::read_to_string(perch.join(RESUME_PID_FILE))█299| .ok()█300| .and_then(|s| s.trim().parse::<u32>().ok())█301| .filter(|&p| p != 0)█...█918| let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);█*919| let Ok(entries) = std::fs::read_dir(&perch) else {█920| continue;█921| };█922| for entry in entries.flatten() {█...█1004| }█*1005| while !stop.load(Ordering::SeqCst) {█1006| // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT█1007| // unset = normal production, untouched): skip the reconcile poll+body so an█1008| // int rig [...22790ch elided...] ::Relaxed) {█...█1233| while slept < backoff && !stop.load(Ordering::Relaxed) {█*1234| thread::sleep(TICK.min(backoff - slept));█1235| slept += TICK;█1236| }█1237| }█...█1809| write_heartbeat(&path);█*1810| let first: u64 = std::fs::read_to_string(&path)█1811| .expect(\"heartbeat written\")█1812| .trim()█1813| .parse()█...█1815| assert!(first > 0, \"a real wall-clock stamp\");█*1816| thread::sleep(Duration::from_millis(15));█1817| write_heartbeat(&path);█*1818| let second: u64 = std::fs::read_to_string(&path)█1819| .unwrap()█1820| .trim()█1821| .parse()█Showing files 21-40 of 48+. Use skip=40 for the next page, or narrow paths/pattern.\u000f█</out>█# Assistant ¶█_**Identifying exact lifecycle sections**_█# Tool call ¶█//Reading pulse tick scans█read(path=\"crates/spt-daemon/src/lifecycle.rs\", selector=\"460-740\")█<out>█\u000e█/// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),█/// or `None` when there is no pinned install dir / the file can't be read.█/// The W3c reload-on-dirty change detector. (W3c)█// [impl->REQ-ADAPTER-LIVE-UPDATE]█fn manifest_disk_hash(&self) -> Option<String> {█let dir = self.install_dir.as_ref()?;█let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;█Some(crate::release::sha256_hex(&bytes))█}█/// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,█/// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in█/// place - so a daemon-coordinated adapter update (W3d) that swapped the█/// install dir takes effect within one pulse, WITHOUT restarting the brain or█/// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`█/// child) are SEPARATE PROCESSES with no shared memory, so the apply cannot█/// push an `Arc` handle across - the brain PULLS the change from disk here█/// (corrects ADR-0025 step 3 + the W3 verdict's \"broker registers the brain's█/// handle\" caution). Re-parses the SAME bytes it hashed; W3b's [...13040ch elided...] inue;█}█// Pull any live-update manifest swap from disk before the tick so the█// tick (psyche turn / echo) runs against the fresh manifest. (W3c)█self.reload_manifest_if_changed(&mut manifest_hash);█match self.pulse_tick(session_id) {█Ok(report) => {█// F-030 W1: feed a fired turn's outcome to the failure budget.█if let Some(outcome) = &report.turn_outcome {█self.note_turn_outcome(&mut turn_strikes, outcome);█}█on_tick(&report);█}█Err(_) => { /* a transient tick error must not kill the loop */ }█}█...█}█}█[Showing lines 459-743 of 2890. Use :744 to continue]\u000f█</out>█# Tool call ¶█//Reading history fetch path█read(path=\"crates/spt-daemon/src/lifecycle.rs\", selector=\"740-885,930-1160\")█<out>█\u000e pub fn run_pulse_loop<F: FnMut(&TickReport)>(█...█) {█...█while !stop.load(Ordering::Relaxed) {█...█match self.pulse_tick(session_id) {█Ok(report) => {█...█on_tick(&report);█}█Err(_) => { /* a transient tick error must not kill the loop */ }█}█// Advance past the FIRED grid point AND past `now` (doyle amendment█// 7): strictly-after-fired keeps the double-fire guard (a same-ms tick█// steps exactly one grid); skip-past-now collapses a stall (an█// overrunning tick, or a suspend/resume that left `deadline` many█// grids behind) to ONE catch-up fire instead of a back-to-back storm.█deadline = anchor.advance_after_fire(deadline, now_ms());█}█}█/// Fetch history per the manifest `[history]` strategy (empty if none).█fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {█// W3c gate (i): snapshot the manifest (guard dropped before the bounded█// history fetch below), so a live-update refresh never waits on it.█let manifest = self.cell.manifest_snapshot();█[...19786ch elided...] f.id,█&project_id,█commune_file.as_deref(),█signoff_file.as_deref(),█)█}█/// Resolve a psyche-download drop FILE (`<dir>/<id>-<kind>.md`) from a raw manifest█/// drop dir against the endpoint's cwd - the same dir-resolution rule as█/// [`resolve_endpoint_drop_dir`] (absolute as-is; relative joined under the cwd;█/// relative-with-no-cwd -> `None`) and the same filename convention as `spt`'s█/// `resolve_filedrops`, so the mind seeded into the `{psyche_context_file}` sees the█/// SAME pending drops the `psyche-download` CLI would.█// [impl->REQ-PSYCHE-SID-CUSTODY]█fn psyche_drop_file(█&self,█raw_dir: Option<&Path>,█cwd: Option<&Path>,█kind: &str,█) -> Option<PathBuf> {█...█}\u000f█</out>█# Tool call ¶█//Tracing ingest drops implementation█grep(pattern=\"pub fn ingest_drops|fn ingest_drops|read_dir|read_to_string|File::open\", path=\"crates/spt-live/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-live/src/█## context.rs█76|pub fn read_marker(path: &Path) -> Option<Marker> {█*77| let s = std::fs::read_to_string(path).ok()?;█78| Marker::parse_content(&s)█79|}█80|█...█210| assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });█*211| let content = std::fs::read_to_string(&path).unwrap();█212| assert!(content.contains(\"DIRECT\") && !content.contains(\"STALE\"));█213| });█214| }█## echo.rs█220| assert_eq!(res.drop_path, dir.path().join(\"doyle-commune.md\"));█*221| let written = std::fs::read_to_string(&res.drop_path).unwrap();█222| // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.█223| assert!(█224| written.starts_with(\"Source: echo-commune\\n\"),█## history.rs█127| let log = resolve_perch_path(id, hint).join(\"history.log\");█*128| match std::fs::read_to_string(&log) {█129| Ok(s) => Ok(records(&s)),█130| // No log yet => a fresh perch with no recorded history.█131| Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),█## ingest.rs█201|// [impl->REQ-HAZAR [...6319ch elided...] ct-context).█...█110| let Some(path) = file else { return };█*111| let Ok(body) = std::fs::read_to_string(path) else { return };█112| // Strip the checkpoint sentinel before presenting (pre-synthesis strip point):█113| // the marker is spt-core control metadata, never agent context. The inter-marker█114| // text is kept. [impl->REQ-RESUME-CONTEXT-PULL]█## signoff.rs█264| );█*265| let written = std::fs::read_to_string(&live).unwrap();█266| assert!(█267| written.contains(\"Source: self-commune\"),█268| \"provenance: {written:?}\"█...█286| );█*287| assert!(std::fs::read_to_string(&proj)█288| .unwrap()█289| .contains(\"Source: self-commune\"));█290| });\u000f█</out>█# Tool call ¶█//Tracing history fetch implementation█grep(pattern=\"pub fn fetch_history|fn fetch_history|read_to_string|std::fs::read|File::open|BufReader\", path=\"crates/spt-live/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-live/src/█## context.rs█76|pub fn read_marker(path: &Path) -> Option<Marker> {█*77| let s = std::fs::read_to_string(path).ok()?;█78| Marker::parse_content(&s)█79|}█80|█...█210| assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });█*211| let content = std::fs::read_to_string(&path).unwrap();█212| assert!(content.contains(\"DIRECT\") && !content.contains(\"STALE\"));█213| });█214| }█## digest.rs█123| .map_err(DigestExtractError::Fill)?;█*124| let raw = std::fs::read(&path).map_err(DigestExtractError::Read)?;█125|█126| // The extractor sees the located path as `{source}` and the bytes on stdin.█127| let mut ekeys = keys.clone();█## echo.rs█220| assert_eq!(res.drop_path, dir.path().join(\"doyle-commune.md\"));█*221| let written = std::fs::read_to_string(&res.drop_path).unwrap();█222| // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.█223| assert!(█224| written.starts_with(\"Source: echo-commune\\n\"),█## history.rs█83|/// (a fresh perch), not an error.█*84|pub fn fetch_history(█85| history: [...6943ch elided...] ct-context).█...█110| let Some(path) = file else { return };█*111| let Ok(body) = std::fs::read_to_string(path) else { return };█112| // Strip the checkpoint sentinel before presenting (pre-synthesis strip point):█113| // the marker is spt-core control metadata, never agent context. The inter-marker█114| // text is kept. [impl->REQ-RESUME-CONTEXT-PULL]█## signoff.rs█264| );█*265| let written = std::fs::read_to_string(&live).unwrap();█266| assert!(█267| written.contains(\"Source: self-commune\"),█268| \"provenance: {written:?}\"█...█286| );█*287| assert!(std::fs::read_to_string(&proj)█288| .unwrap()█289| .contains(\"Source: self-commune\"));█290| });\u000f█</out>█# Tool call ¶█//Reading sessions ledger█read(path=\"crates/spt-store/src/sessions.rs\", selector=\"1-180\")█<out>█\u000e//! Per-endpoint **session ledger** - `<perch>/sessions.log` (ADR-0019 /█//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/█//! sessions.log`.█//!█//! A live agent that `/clear`s or `/compact`s keeps its perch identity but█//! rotates its harness `session_id` (a new transcript file). The single█//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*█//! the thread but cannot *span* it. This ledger records each rotation so the█//! digest can enumerate the **last K sessions** and bridge a boundary█//! (ADR-0019 §thread-spanning).█//!█//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is█//! **bounded** ([`MAX_LEDGER`]) - the sister project's seal-on-boot failure grew█//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates█//! the oldest rows out. A re-bind of the *same* session is not a new boundary and█//! is deduped.█// [impl->REQ-TERM-6]█use std::io;█use std::path::Path;█use serde::{Deserialize, Serialize};█use crate::atomic::atomic_write_string;█/// The ledger filename under a perch.█pub const LEDGER_FILE: &str = \"sessions.log\";█/// On-disk row cap. The ledger is a rolling tail - [...6045ch elided...] new row's ordinal is one past the highest existing (monotonic, persisted).█// With the backfill above every existing row is now Some, so `last()` carries█// the max; an empty ledger starts at 0.█let next = entries█.last()█.and_then(|e| e.ordinal)█.map(|o| o + 1)█.unwrap_or(0);█let mut entry = entry.clone();█entry.ordinal = Some(next);█entries.push(entry);█let start = entries.len().saturating_sub(MAX_LEDGER);█let body: String = entries[start..]█.iter()█.map(|e| serde_json::to_string(e).unwrap_or_default())█.map(|l| l + \"\\n\")█.collect();█std::fs::create_dir_all(perch)?;█atomic_write_string(&perch.join(LEDGER_FILE), &body)?;█Ok(true)█}█[Showing lines 1-183 of 478. Use :184 to continue]\u000f█</out>█# Tool call ¶█//Reading pump cadence and work█read(path=\"crates/spt-daemon/src/pump/mod.rs\", selector=\"1-170,520-750\")█<out>█\u000e//! Outbound peer pump (D9-1-2) - the production trigger loops every D5-D8█//! slice deferred (\"rides D9 with the daemon lifecycle\").█//!█//! One daemon-hosted loop - *the* peer pump - is a pure scheduler + per-peer█//! fan-out over four [`PumpWorker`] legs. The pump owns *when* (each worker's█//! cadence + its wake markers, fed to the pure [`due`] kernel) and *toward█//! whom* (every roster member of every non-detached member subnet); each worker█//! owns *what* (its node-local `pre_round` and its per-peer `peer_step`), a thin█//! call over an already-shipped, idempotent driver. The four legs, in fan-out█//! order, each in its own module:█//!█//! - **registry push** ([`registry`], [`RegistryHost::advertise_local`] -> one█//! feed stream per peer): the node re-advertises its own rows; peers█//! reconverge within one cadence (replay-is-recovery, the D4d/D8 push█//! posture). Mesh-D6 widened the push target from the directly-paired set to█//! ALL roster members - a wider DIRECT fan-out, never a relay (REQ-MESH-3).█//! Own rows only - a relayed third-party row would carry an origin the█//! receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10), so every row still█//! arri [...18985ch elided...] flags, Instant::now());█}█Ok(())█}█/// Compute each worker's due flag for this tick (the pure scheduler step):█/// poll EVERY worker's wake first - even a not-otherwise-due leg, so a marker█/// is never stranded - then the [`due`] kernel over its RAM-only last-ran█/// instant. Returns the per-worker flags (parallel to `workers`) and whether█/// ANY leg is due (an all-idle tick sleeps without loading a round).█fn compute_due(█workers: &mut [&mut dyn PumpWorker],█last: &[Option<Instant>],█now: Instant,█) -> (Vec<bool>, bool) {█let mut flags = vec![false; workers.len()];█let mut any = false;█for i in 0..workers.len() {█let wake = workers[i].poll_wake();█flags[i] = due(last[i], wake, now, workers[i].cadence());█any |= flags[i];█}█...█}\u000f█</out>█# Assistant ¶█_**Analyzing session echo gate behavior**█**Inspecting registry and project index scanning**█**Planning live read-only process probes**█**Planning code inspection for read pump workers**_█# Tool call ¶█//Reading registry pump scans█read(path=\"crates/spt-daemon/src/pump/registry.rs\", selector=\"1-260\")█<out>█\u000e//! The registry pump worker (pump-seam P2) - the first leg cut behind the█//! [`super::PumpWorker`] seam.█//!█//! `poll_wake` consumes the event-driven advertisement marker (force a round█//! NOW on an endpoint online/offline transition); `pre_round` runs the█//! node-local sweep (repair-evictions -> silent-peer eviction -> due seed█//! rotations -> re-advertise) ONCE per due round and holds the produced adverts█//! as worker state; `peer_step` pushes this node's own rows for the given█//! subnet over the peer's feed stream. Construction-captured deps: the shared█//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the█//! two registry cadence knobs.█use std::io;█use std::path::PathBuf;█use std::sync::Arc;█use std::time::Duration;█use spt_net::net::replicate::RegistryFeedRecord;█use spt_store::subnet::SubnetStore;█use crate::registryhost::RegistryHost;█use super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};█/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,█/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the█/// prior generation one-deep for the re-seed grace) and clear its pending entry.█/ [...9948ch elided...] 4] `pre_round` runs the full node-local sweep once per due█// round: a scheduled seed rotation fires (epoch bumps) and adverts are█// recomputed each call (a fresh epoch per advertisement - the once-per-round█// re-stamp). The per-step ORDER (evict -> rotate -> advertise) is preserved█// verbatim from the pre-seam inline loop; each step keeps its own█// registryhost-side unit coverage.█#[test]█fn pre_round_sweeps_and_refreshes_adverts_each_round() {█with_home(|home| {█// A served subnet plus one advertisable Self perch.█let owlery = home.join(\"owlery\");█let id = \"adv-ag\";█let p = spt_store::perch::resolve_perch_path_in(█...█);█...█});█}█}█[Showing lines 1-263 of 334. Use :264 to continue]\u000f█</out>█# Tool call ¶█//Reading notif pump reads█read(path=\"crates/spt-daemon/src/pump/notif.rs\", selector=\"1-240\")█<out>█\u000e//! The notif pump worker (pump-seam P3) - the full semilattice spool push.█//!█//! `peer_step` emits this node's notif feed for the subnet█//! ([`emit_notif_feed`]) and pushes it over the peer's feed stream; the█//! receiver's join dedups. No node-local `pre_round` and no wake edge - a█//! plain cadenced push. Construction-captured dep: the notif spool DB path.█use std::io;█use std::path::PathBuf;█use std::time::Duration;█use spt_store::notif::NotifStore;█use crate::notifsync::emit_notif_feed;█use super::{PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};█/// The notif leg as a [`PumpWorker`]: the full semilattice spool per subnet█/// ([`emit_notif_feed`]); the receiver's join dedups.█pub(super) struct NotifWorker {█/// The notif spool DB ([`NotifStore::open_at`]).█notif_db: PathBuf,█/// This worker's scheduling period (`cadence.notif`).█cadence: Duration,█}█impl NotifWorker {█/// Construction capture (§Frozen-4): the notif spool DB path + the cadence.█pub(super) fn new(paths: &PumpPaths, cadence: &PumpCadence) -> NotifWorker {█NotifWorker {█notif_db: paths.notif_db.clone(),█cadence: cadence.notif,█}█}█}█impl PumpWorker for NotifWorker {█fn cadence(&self) -> Duration {█self.cadence█}█/// Emit this node's notif feed for `subnet` (the full semilattice spool)█/// and push it fire-and-forget; the receiver's join dedups.█// [impl->REQ-NOTIF-1]█fn peer_step(█&mut self,█io: &mut PeerIo,█subnet: &str,█_peer_hex: &str,█_ctx: &RoundCtx,█) -> io::Result<()> {█let store =█NotifStore::open_at(&self.notif_db).map_err(|e| io::Error::other(e.to_string()))?;█let feed = emit_notif_feed(&store, subnet).map_err(io::Error::other)?;█if !feed.is_empty() {█io.push_feed(&feed)?;█}█Ok(())█}█}█\u000f█</out>█# Tool call ¶█//Reading sync pump reads█read(path=\"crates/spt-daemon/src/pump/sync.rs\", selector=\"1-280\")█<out>█\u000e//! The sync pump worker (pump-seam P3) - anti-entropy pull for the minds.█//!█//! `poll_wake` consumes the wake-edge freshness-pull marker (a dormant->active█//! transition forces a pull NOW); `peer_step` builds the want-set - the union█//! of locally-tracked allowed branches ([`select_refs`]) and registry-derived█//! `a-<id>` bootstrap refs (endpoints known in the shared subnet this node█//! never held locally), both gated by the same [`crate::sync::SyncPolicy`] the█//! responder enforces (symmetric scoping) - then pulls via [`request_sync`]█//! over the shell's open-op counter and reports conflicts through the█//! construction-captured [`PumpHooks`]. Construction-captured deps (§Frozen-4):█//! the registry [`Arc`] (snapshot bootstrap refs), the store/scratch/marker█//! paths, and the hooks.█use std::io;█use std::path::PathBuf;█use std::sync::Arc;█use std::time::Duration;█use spt_store::contextstore::ContextStore;█use crate::registryhost::RegistryHost;█use crate::sync::{request_sync, select_refs};█use super::{PeerIo, PumpCadence, PumpHooks, PumpPaths, PumpWorker, RoundCtx};█/// The sync leg as a [`PumpWorker`]: anti-entropy pull for the minds.█pub(super) struct SyncWorker {█[...2392ch elided...] s = ContextStore::open_or_init_in(&self.ctx_root)?;█let mut want = select_refs(&ctx.policy, &cs, peer_hex)?;█// Registry-derived bootstrap refs: minds named in the shared subnet█// that this node never held locally.█if let Some(snap) = self.registry.snapshot(subnet) {█for id in snap.endpoint_ids() {█let r = format!(\"a-{id}\");█if !want.contains(&r) && ctx.policy.allows(&r, peer_hex) {█want.push(r);█}█}█}█if !want.is_empty() {█let open_op = io.open_op()?;█let report = request_sync(&mut *io.brain, io.conn_id, &want, open_op, &cs, &self.scratch)?;█(self.hooks.on_pull)(peer_hex, &report);█}█Ok(())█}█}█\u000f█</out>█# Tool call ¶█//Reading update pump reads█read(path=\"crates/spt-daemon/src/pump/update.rs\", selector=\"1-220\")█<out>█\u000e//! The update pump worker (pump-seam P4) - peer-propagated self-update.█//!█//! `peer_step` runs the REQ-UPD-1/4/6 leg under the PRODUCTION gate: the█//! VerifyPolicy floor arithmetic (keys + channel pin from `release-keys.json`,█//! version floor = the staged release as the rollback gate, wall clock for█//! expiry only), the [`request_update`] pull, and - on a fresh stage - the█//! consent-notif production minted from the registry's CANONICAL epoch█//! ([`RegistryHost::with_epoch`], the construction-captured Arc per §Frozen-4),█//! plus the `record_last_outcome` convergence writes. No wake edge, no█//! `pre_round`. Construction-captured deps: the registry Arc, the worker's█//! path slices, and the `full_auto_update` consent knob.█use std::io;█use std::path::PathBuf;█use std::sync::Arc;█use std::time::Duration;█use spt_store::notif::NotifStore;█use spt_store::visibility::VisibilityStore;█use crate::consent::{decide, most_recently_active};█use crate::notif::{produce_consent_notif, NotifSurfacePolicy};█use crate::propagate::{request_update, UpdatePullOutcome};█use crate::registryhost::RegistryHost;█use crate::relcache::ReleaseCache;█use crate::release::VerifyPolicy;█use crate::up [...5912ch elided...] subnet,█&surface,█&self.owlery,█now_ms(),█)█});█if let Err(e) = produced {█eprintln!(\"UPDATE_NOTIF_FAIL:{e}\");█}█}█UpdatePullOutcome::UpToDate => {}█UpdatePullOutcome::Rejected(reason) => {█// Loud + typed, never silent (ADR-0004 §D).█eprintln!(\"UPDATE_REJECTED:{peer_hex}:{reason}\");█// Convergence fact (M8 decision 19) - `Rejected{reason}` in the█// table.█// [impl->REQ-UPD-6]█let _ = cache.record_last_outcome(&format!(\"rejected:{reason}\"));█}█}█Ok(())█}█}█\u000f█</out>█# Tool call ¶█//Locating pump path loads█grep(pattern=\"struct RoundCtx|impl RoundCtx|struct PumpPaths|impl PumpPaths|fn poll_wake|fn pre_round|fn peer_step\", path=\"crates/spt-daemon/src/pump\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/pump/█## mod.rs█221|/// roots so two nodes can share one test process.█*222|pub struct PumpPaths {█223| /// The perch tree (registry advertisement scan).█224| pub owlery: PathBuf,█225| /// The context store root ([`ContextStore::open_or_init_in`]).█...█268|█*269|impl PumpPaths {█270| /// The canonical SPT_HOME layout.█271| pub fn production() -> PumpPaths {█272| let home = perch::spt_home();█...█378| /// wake claims exactly one forced round. Default: no wake edge.█*379| fn poll_wake(&mut self) -> bool {█380| false█381| }█382|█...█384| /// Default: nothing.█*385| fn pre_round(&mut self, _ctx: &RoundCtx) {}█386|█387| /// The per-peer step for one `(subnet, peer)` under this round's context. A█388| /// failure aborts the remaining due workers for THIS peer and drops its█389| /// conn (redial next tick) - abort-on-first-fail, current behaviour.█*390| fn peer_step(█391| &mut self,█392| io: &mut PeerIo,█393| subnet: &str,█...█404|/// construction-captured by each worker).█*405|struct RoundCtx {█406| /// `subnet.json` - the member subnets iterated for fan-out.█407 [...2203ch elided...] let dir = tempfile::tempdir().unwrap();█230| let marker = dir.path().join(\"advertise-now\");█231| let mut w = RegistryWorker {█...█257| #[test]█*258| fn pre_round_sweeps_and_refreshes_adverts_each_round() {█259| with_home(|home| {█260| // A served subnet plus one advertisable Self perch.█261| let owlery = home.join(\"owlery\");█## sync.rs█74| // [impl->REQ-INST-3]█*75| fn poll_wake(&mut self) -> bool {█76| crate::resting::take_freshness_pull(&self.pull_marker)█77| }█78|█...█85| // [impl->REQ-INST-5]█*86| fn peer_step(█87| &mut self,█88| io: &mut PeerIo,█89| subnet: &str,█## update.rs█84|█*85| fn peer_step(█86| &mut self,█87| io: &mut PeerIo,█88| subnet: &str,\u000f█</out>█# Tool call ¶█//Finding registry scan internals█grep(pattern=\"fn advertise_local|pub fn advertise_local|list_self_perch_ids|read_info|read_dir\", path=\"crates/spt-daemon/src/registryhost.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 372| // [impl->REQ-INST-7]█*373| pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {█374| let subnets = SubnetStore::load();█375| let mut vis = VisibilityStore::load();█376| // First-join home adoption (D9-5, REQ-INST-15): within one█...█412| let live_ids: std::collections::BTreeSet<String> =█*413| perch::list_self_perch_ids(owlery).into_iter().collect();█414|█*415| for id in perch::list_self_perch_ids(owlery) {█416| let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);█417| // Advertisement follows the resting-state machine (D9-2,█418| // REQ-INST-3): a live perch with a resting record advertises its█...█430| // [impl->REQ-INST-14]█*431| let info = spt_store::info::read_info(&perch_path);█432| let resources = info█433| .as_ref()█434| .and_then(|i| i.resources.clone())█...█1932| #[test]█*1933| fn advertise_local_scans_and_respects_visibility() {█1934| with_home(|home| {█1935| let owlery = home.join(\"owlery\");█1936| for id in [\"vis-ag\", \"hid-ag\"] {█...█2172| #[test]█*2173| fn advertise_local_stamps_the_node_label() {█2174| with_home(|home| {█2175| let owlery = home.join(\"owlery\");█2176| let p = perch::resolve_perch_path_in(&owlery, \"lab-ag\", ParentHint::Infer);\u000f█</out>█# Assistant ¶█_**Identifying needed RoundCtx and pump paths**_█# Tool call ¶█//Reading pump shared state scans█read(path=\"crates/spt-daemon/src/pump/mod.rs\", selector=\"210-440\")█<out>█\u000e/// discovery do the work ([`production_resolver`]); hermetic tests inject a█/// concrete address map (loopback endpoints run no discovery).█pub type PeerResolver = Box<dyn Fn(&str) -> Option<serde_json::Value> + Send>;█/// The production resolver: an id-only iroh address - mDNS on the LAN, n0█/// DNS on the WAN (`addr_for_node_hex`).█pub fn production_resolver() -> PeerResolver {█Box::new(|hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()))█}█/// Where this node's pump-side stores live. Production = the canonical█/// SPT_HOME layout; the loopback E2E gives its in-process \"node\" distinct█/// roots so two nodes can share one test process.█pub struct PumpPaths {█/// The perch tree (registry advertisement scan).█pub owlery: PathBuf,█/// The context store root ([`ContextStore::open_or_init_in`]).█pub ctx_root: PathBuf,█/// The notif spool DB ([`NotifStore::open_at`]).█pub notif_db: PathBuf,█/// Transient pull scratch.█pub scratch: PathBuf,█/// `subnet.json` ([`SubnetStore::load_from`]).█pub subnets: PathBuf,█/// `visibility.json` ([`VisibilityStore::load_from`]).█pub vis: PathBuf,█/// The pump's durable open- [...8374ch elided...] _or_else(|| PathBuf::from(\"attachment.json\")),█);█// THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster█// (all members), not the directly-paired set. The one roster loaded█// here feeds both the push (who we dial) and the sync gate (who we█// authorize), so they read one store.█let roster_path = paths█.subnets█.parent()█.map(|p| p.join(\"roster.json\"))█.unwrap_or_else(|| PathBuf::from(\"roster.json\"));█let policy = SyncPolicy {█subnets: subnets█.subnets█.iter()█.map(|s| (s.name.clone(), s.hide_new_endpoints))█.collect(),█...█};█...█}█}█[Showing lines 209-443 of 1982. Use :444 to continue]\u000f█</out>█# Tool call ¶█//Reading registry advertisement█read(path=\"crates/spt-daemon/src/registryhost.rs\", selector=\"350-480,760-850\")█<out>█\u000eimpl RegistryHost {█...█fn stamp_admit(&self) {█self.last_admit_ms█.store(wall_ms(), std::sync::atomic::Ordering::Relaxed);█}█/// Wall-clock ms of the last admitted inbound registry/label feed, if any█/// this process lifetime (ADR-0039 Decision 5 status field).█// [impl->REQ-PUMP-STAGE-TRUTH]█pub fn last_admit_ms(&self) -> Option<u64> {█match self█.last_admit_ms█.load(std::sync::atomic::Ordering::Relaxed)█{█0 => None,█ms => Some(ms),█}█}█/// Scan the owlery and (re-)advertise every self perch into every member█/// subnet it is visible in, each row stamped with a fresh epoch. Returns█/// the updates to push to peers (the node's own rows only - peers' rows█/// reconverge from their own re-advertisement, no transitive gossip in█/// v1: trust is pairwise per subnet, a relayed third-party row would█/// carry an origin the receiver can't gate honestly).█// [impl->REQ-INST-7]█pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {█let subnets = SubnetStore::load();█let mut vis = VisibilityStore::load();█[...9859ch elided...] ) and NEVER `Offline` (a live█/// node never self-gossips Offline; node-down is only ever remote-inferred via█/// eviction, and `RestState` has no Offline).█/// - **Corrupt** (info.json present but destroyed - a hard-reset NUL zero-fill):█/// `is_perch_alive` now reads false for a corrupt record and `is_perch_unbound`█/// reads false too (unparseable => no status), so a corrupt perch lands in the█/// cold arm -> `Suspended`, NEVER Active/Dormant. Without this the daemon█/// self-gossiped a wiped endpoint as ONLINE forever█/// (REQ-HAZARD-CORRUPT-PERCH-COHERENCE, counter-39 bug #2).█// [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]█// [impl->REQ-HAZARD-CORRUPT-PERCH-COHERENCE]█// [impl->REQ-EFFECTIVE-INSTANCE-STATE]█fn advertised_status(perch_path: &Path) -> Status {█...█}\u000f█</out>█# Tool call ¶█//Tracing sync ref selection█grep(pattern=\"pub fn select_refs|fn select_refs|create_bundle|bundle|branches\\\\(|refs|read_dir|std::fs::read\", path=\"crates/spt-daemon/src/sync.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e*1|//! Context bundle sync over broker streams (M4-D6c, ADR-0013, REQ-NET-3) -█2|//! the serve/request drivers for the [`SyncRecord`] protocol, the server-side█3|//! ref gate, and the post-pull reconcile wiring.█4|//!█...█7|//! current tips; the responder's [`serve_sync`] gates every ref, builds an█*8|//! incremental bundle ([`BranchStore::create_bundle`]), and streams it back as█*9|//! positional chunks. The requester fetches the bundle into quarantine and█10|//! rules per file with the vector driver ([`spt_store::syncmerge`]); surfaced█11|//! conflicts feed the Psyche reconcile when this node is the elected█12|//! reconciler ([`reconcile_after_sync`]).█...█15|//!█*16|//! The requester names refs, but naming is not entitlement: [`SyncPolicy`]█17|//! re-checks every requested ref on the responder under the█18|//! **handshake-proven** origin node (never payload bytes, KNOWN-HAZARDS 7.5):█19|//!█...█28|//! per-file p- filtering is post-v1 (CONTEXT-MEMORY §Deferred).█*29|//! - Anything else (machinery refs, `refs/spt-sync/...`) is never served.█30|//!█31|//! ## Wire discipline + recovery█32|//!█...█35|//! There is deliberately **no resume protocol**: a torn pull (strea [...9022ch elided...] may hold plus hosted p- projects.█619| #[test]█*620| fn select_refs_scopes_two_tiers() {█621| let tmp = tempfile::tempdir().unwrap();█622| let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();█623| cs.live_context_path(\"doyle\").unwrap();█...█630|█*631| let refs = select_refs(&p, &cs, \"peer1\").unwrap();█*632| assert!(refs.contains(&\"a-doyle\".to_string()));█*633| assert!(refs.contains(&\"p-spt-core\".to_string()));█634| assert!(█*635| !refs.iter().any(|r| r == \"a-ghost\"),█*636| \"unsynced mind never requested: {refs:?}\"█637| );█638|█639| assert!(█*640| select_refs(&p, &cs, \"stranger\").unwrap().is_empty(),█641| \"untrusted peer gets nothing\"█642| );█643| }\u000f█</out>█# Tool call ¶█//Tracing notif feed storage█grep(pattern=\"pub fn emit_notif_feed|fn emit_notif_feed|fn list|pub fn list|read_to_string|std::fs::read\", path=\"crates/spt-daemon/src/notifsync.rs; crates/spt-store/src/notif.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### notifsync.rs█82|// [impl->REQ-NOTIF-1]█*83|pub fn emit_notif_feed(store: &NotifStore, subnet: &str) -> Result<Vec<u8>, String> {█84| let mut wire = Vec::new();█85| for row in store.list(subnet).map_err(|e| format!(\"notif list: {e}\"))? {█86| wire.extend_from_slice(&NotifRecord::Row { row }.encode_line());█## spt-store/src/█### notif.rs█252| /// Every row in `subnet`, oldest produce first (created_ms, id tiebreak).█*253| pub fn list(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {█254| self.select_rows(█255| \"subnet = ?1 ORDER BY created_ms ASC, notif_id ASC\",█256| params![subnet],█...█419| #[test]█*420| fn listing_is_subnet_scoped() {█421| let dir = tempdir().unwrap();█422| let s = store(dir.path());█423| let mut e = epochs(dir.path());\u000f█</out>█# Tool call ¶█//Reading history implementation█read(path=\"crates/spt-live/src/history.rs\", selector=\"1-155\")█<out>█\u000e//! History subsystem (M2b T1) - transcript access behind the `[history]`█//! manifest section. Feeds the echo-commune (T3) and the resume briefs (T7).█//!█//! Three strategies, one normalized output ([`HistoryRecord`] lines):█//! - **`fetcher`** - spt-core *pulls*: run the adapter's `fetcher` command█//! bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT); its stdout is already-normalized█//! history.█//! - **`locate_normalize`** - spt-core *locates* the raw transcript█//! (`locate_template`), reads it, then pipes it through `normalize_command`█//! bounded (raw transcript on stdin -> normalized stdout).█//! - **`native`** - Path-B store: the adapter *pushed* records via█//! `api history-log`; spt-core reads the perch's `history.log` (the T7/M2a█//! sink in `spt::api::reporting::append_history`).█//!█//! A record is one non-empty line of normalized output. spt-core treats each as█//! **opaque** - the adapter owns the per-line schema (MANIFEST §Principle); the█//! history subsystem only delimits, never parses.█//█// Clean-room over the M2a seams (the sister's transcript access is `claude -p`█// /`.claude/`-coupled). The fetcher/normalize command lines are the adapter's█// opaque temp [...4653ch elided...] }█}█}█/// A bounded command's stdout, or `NonZero` carrying its stderr if it failed.█fn ok_or_nonzero(out: spt_runtime::BoundedOutput) -> Result<String, HistoryError> {█if out.success() {█Ok(out.stdout)█} else {█Err(HistoryError::NonZero {█code: out.status_code,█stderr: out.stderr,█})█}█}█/// Split normalized output into records: one per non-empty line, trailing█/// whitespace trimmed. The native store is already newline-delimited; fetchers█/// and normalizers emit the same shape.█fn records(s: &str) -> Vec<HistoryRecord> {█s.lines()█.map(str::trim_end)█.filter(|l| !l.is_empty())█.map(|l| HistoryRecord { raw: l.to_string() })█.collect()█}█[Showing lines 1-158 of 352. Use :159 to continue]\u000f█</out>█# Tool call ¶█//Reading drop ingest implementation█read(path=\"crates/spt-live/src/ingest.rs\", selector=\"180-280\")█<out>█\u000e/// `id`: route each into the two-tier context store (precedence-guarded, llm█/// source) and **delete** it (spt-core single-writer; the mind never deletes).█///█/// A drop is deleted whether its writes `Written` or `Suppressed` - both mean█/// \"consumed\" (a suppressed drop was a stale snapshot, correctly discarded). A█/// read/write *error* leaves the drop in place to retry on the next pass.█///█/// **Commit-before-delete (F-032, KNOWN-HAZARDS 7.40):** a non-empty█/// `<project-context>` slice with an EMPTY `project_id` (the endpoint's anchor█/// cwd unresolved/owlery-internal at ingest time) was never committed anywhere█/// - deleting the drop would black-hole it. That slice is instead **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 the█/// stale-signoff sweep on the next listener start): the live slice (just█/// committed) is dropped from the pending so a retry never re-commits it, and█/// the next ingest with a resolvable `project_id` commits + deletes. The█/// rewrite is idempotent (the pending form re-parses to itself - no churn on█/// steady retries), and a NEWER com [...3447ch elided...] // A SIGNOFF source is consumed as a drop once its deferred slice is█// safely carried by the commune-suffix pending: the signoff file█// itself must never linger (write-once-per-generation sentinel -█// the sweep rationale), and the live slice was committed above.█if kind == DropKind::Signoff {█std::fs::remove_file(&drop_path)?;█}█out.push(Ingested {█kind,█writes,█drop_path,█preserved: true,█});█continue;█}█// [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop█// after consuming it; the mind never does.█...█}█...█}█[Showing lines 179-283 of 889. Use :284 to continue]\u000f█</out>█# Assistant ¶█_**Planning livehost adapter reads inspection**_█# Tool call ¶█//Reading live host scan details█read(path=\"crates/spt-daemon/src/livehost.rs\", selector=\"120-250,1000-1058\")█<out>█\u000eimpl LiveSet {█...█fn stop_host(&self, id: &str) {█let handle = {█...█map.remove(id)█};█if let Some(h) = handle {█h.stop.store(true, Ordering::SeqCst);█let _ = h.thread.join();█// F-030 W3: nothing detached to reap - the per-event psyche driver spawns█// no resident child, so joining the driver thread + de-tabling above is the█// whole un-host (the resident-psyche handle-reap + orphan-reap retired with█// the residency machinery). Un-hosting a genuinely-offline endpoint is legit.█}█}█}█/// One reconcile sweep - the brain's live host-set invariant holder, run at boot█/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the█/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):█///█/// - **un-host** (tabled ∧ ¬online) - stop the driver + de-table (exit guard: a█/// signed-off / removed endpoint must not keep pulsing - thread + dead-agent leak).█/// - **host** (online ∧ ¬tabled) - spawn the Psyche + start the driver.█/// - **leave-alone** (online ∧ tabled).█///█/// `status` is daemon-authoritative (CONTEXT:19 [...8603ch elided...] lined). The█// controllable==Some(true) gate inside keeps relay/legacy perches exempt.█// [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]█// [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]█if let Some(live) = query_live_session_endpoints() {█reconcile_hosted_liveness(&owlery, &live);█}█reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);█// Sleep in slices so a stop lands promptly.█let mut left = LIVE_RECONCILE_INTERVAL_MS;█while left > 0 && !stop.load(Ordering::SeqCst) {█let step = left.min(100);█std::thread::sleep(Duration::from_millis(step));█left -= step;█}█}█})█}█#[cfg(test)]\u000f█</out>█# Tool call ¶█//Reading brain heartbeat constants█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"35-75\")█<out>█\u000euse crate::endpoint::{brain_ready_path, broker_socket_name};█use crate::relcache::{AppliedRecord, ReleaseCache};█/// Supervisor backoff floor after a brain death (mirrors the peer-pump█/// supervisor, M8 decision 23): a healthy run resets to this; a fast death█/// doubles, capped at [`SUPERVISE_BACKOFF_CAP`].█const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(2);█/// Backoff ceiling - a persistently-crashing brain stays visible and bounded,█/// never a hot respawn loop.█const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);█/// A run at least this long counts as healthy and resets the backoff floor.█/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate█/// brain has this long to signal `ready` for its generation before the window█/// elapses (the \"up but useless\" alive-never-ready rollback path).█const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);█/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that█/// **exits before** signaling ready is respawned this many consecutive times█/// (a transient first-boot hiccup should not abandon a good binary) before the█/// supervisor auto-rolls-back to the last-know [...681ch elided...] - the Q4 update-vs-crash discriminator the█/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the█/// broker *observes* every respawn, so it alone is authoritative (strictly more█/// reliable than a brain->brain frame, which retires in D4).█///█/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value - a█/// reason a *future* broker adds that this brain predates - defaults to `Cold`,█/// never a parse failure that would brick the brain before it rehydrates.█// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]█#[derive(Clone, Copy, Debug, PartialEq, Eq)]█pub enum StartReason {█/// The broker's first brain spawn this broker lifetime.█Cold,█/// A supervised respawn after an unexpected brain exit.█...█}█[Showing lines 34-78 of 2116. Use :79 to continue]\u000f█</out>█# Tool call ¶█//Reading dispatch loop tail█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"430-590\")█<out>█\u000epub fn run_dispatch_loop(█broker_name: &str,█paths: &DispatchPaths,█registry: Arc<RegistryHost>,█stop: &AtomicBool,█poll: Duration,█) -> io::Result<()> {█let mut brain = connect(broker_name)?;█// Claim lifecycle (ADR-0038 C): in-flight / terminal / retryable -█// replaces the pre-fix one-shot `HashSet` whose burned claims permanently█// abandoned a stream on the first transient failure.█let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));█// Dispatcher generation id (Amendment fix 9): every telemetry record this█// loop emits carries it, so per-generation attach/poison censuses line up.█let generation = now_ms();█// Global circuit breaker (Amendment fix 5) + its current window, shared█// with workers so a seat-blocked retry paces on it.█let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));█// Bounded worker pool (Amendment fix 7): a cold table (daemon refresh█// with a large backlog) claims at most this many rows per poll - the█// rest wait for the next tick, batching the cold enumeration instead of█// spawning one thread per row. [...6193ch elided...] outcome_transition(&outcome, attempts, Instant::now(), breaker_window),█);█});█}█thread::sleep(poll);█}█Ok(())█}█/// Bounded redispatch worker pool (ADR-0038 Amendment fix 7): a cold stream█/// table claims at most this many rows per poll tick.█const MAX_DISPATCH_WORKERS: usize = 8;█/// PURE pool gate (Amendment fix 7): may the loop claim another row right█/// now? The cold-enumeration break rides this - a refresh with a large█/// backlog claims at most [`MAX_DISPATCH_WORKERS`] rows per poll, the rest█/// wait for the next tick (batched, never one thread per row).█// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]█fn pool_has_room(active: usize) -> bool {█active < MAX_DISPATCH_WORKERS█}█[Showing lines 429-593 of 1558. Use :594 to continue]\u000f█</out>█# Tool call ¶█//Reading nethost stream subscribers█read(path=\"crates/spt-daemon/src/nethost.rs\", selector=\"140-250,770-830,1000-1090,1270-1300\")█<out>█\u000eenum SeatFail {█...█}█/// One installed subscriber behind a bounded writer queue (ADR-0038 Amendment█/// fixes 2+3, REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE): frames ENQUEUE under the█/// log mutex; the conn I/O - the deadline-bounded `SharedSend::write` that can█/// take 15s against a wedged conn - runs on this seat's OWN writer thread,█/// OUTSIDE every lock. The field stall was exactly the old shape's serial█/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).█///█/// Writer discipline: the thread HALTS at the FIRST failed write (no█/// discarded-errors iteration) and marks the seat poisoned; the next enqueue█/// sees it and the caller removes the seat. Dropping the seat drops the queue█/// sender - the writer drains what it holds and exits.█// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]█struct SubscriberSeat {█sub: SharedSend,█tx: std::sync::mpsc::SyncSender<Envelope>,█/// Writer thread exited (failure or clean drain) - the seat is fully gone█/// and a replacement may install (the no-reinstall-until-gone rule,█/// REQ-DISPATCH-FALLBACK-CIRCUIT).█done: Arc<std::sync::atomic::AtomicBool>,█/// The writer died on a WRITE FAILURE (poison), not [...10844ch elided...] the reactor-liveness canary. Stamps█// epoch-ms every ~25ms FROM the 2-worker runtime, so a starved runtime█// (workers monopolized by non-yielding dead-peer connects) shows as a█// stale stamp. Throwaway probe - no REQ, gated by nothing (cheap store).█let net_canary_tick_ms = Arc::new(AtomicU64::new(epoch_ms()));█let canary = Arc::clone(&net_canary_tick_ms);█runtime.spawn(async move {█let mut ivl = tokio::time::interval(Duration::from_millis(25));█loop {█ivl.tick().await;█canary.store(epoch_ms(), Ordering::Relaxed);█}█});█Ok(NetHost {█runtime,█endpoint,█shared,█dial_ops: Mutex::new(HashMap::new()),█...█})\u000f█</out>█# Tool call ¶█//Finding transcript readers in daemon█grep(pattern=\"fetch_history|history\\\\(|project_endpoint_digest|digest|sessions::read_all|history.log|transcript\", path=\"crates/spt-daemon/src\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## adapter_update.rs█43| pub signed: SignedRelease,█*44| /// The pulled artifact bytes (hashed + matched against the signed digest).█45| pub bytes: Vec<u8>,█46|}█47|█...█72|pub enum AdapterUpdateOutcome {█*73| /// `file_pull`: the payload is authentic (adapter key) + intact (digest) and█74| /// ready to apply. Carries the verified metadata.█75| Verified(ReleaseMetadata),█76| /// `delegated` + attested: run the adapter's own updater (the trusted command).█...█104| };█*105| // Authentic (adapter key) + intact (digest) - the two halves of█106| // adapter content signing.█107| match verify_signature(&payload.signed, &key) {█108| Ok(meta) => match verify_artifact(&meta, &payload.bytes) {█...█305|█*306| // [unit->REQ-UPD-5] tampered bytes (digest mismatch) are rejected even with a█307| // valid signature over the metadata.█308| #[test]█309| fn file_pull_tampered_bytes_rejected() {█## brain.rs█895| /// Attach this (freshly-connected) brain to an **existing** broker session's█*896| /// output from `from_seq`, e.g. a dedicated digest-feed subscriber tha [...24396ch elided...] .mk, prover, t)█346| });█347| let mp = self.prev.as_ref().is_some_and(|p| {█348| peer_proofs█349| .iter()█*350| .any(|(e, t)| *e == p.epoch && p.transcript.verify(&p.mk, prover, t))█351| });█352| (mc, mp)█353| }█...█369| mk: MembershipKey::derive(seed, name, epoch),█*370| transcript: SeedProofTranscript {█371| subnet_id: name.to_string(),█372| seed_epoch: epoch,█373| dialer_pub,█## shellhost.rs█28|use hmac::{Hmac, Mac};█*29|use sha2::{Digest, Sha256};█30|█31|use spt_runtime::manifest::{AdapterKind, CommandReceipt, Manifest, Shell};█32|use spt_runtime::registry::AdapterRecord;█Showing files 1-20 of 22+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding subsecond disk reads█grep(pattern=\"from_millis\\\\((25|40|50|100|200|250|500)\\\\)|POLL_MS|TICK\", path=\"crates/spt-daemon/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## attach.rs█101|/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.█*102|const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);█103|█104|/// The CONTROLLER fell behind the live ring further than the ring holds - the█105|/// dropped frames are GONE (`tail - delivered_through > cap_chunks`), so an█## brainproc.rs█56|/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.█*57|const TICK: Duration = Duration::from_millis(200);█58|█59|/// How long the brain child retries connecting to the broker socket before█60|/// giving up (the broker may still be binding - the child is freshly spawned).█...█62|/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.█*63|const BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);█64|█65|/// Why the broker spawned this brain - the Q4 update-vs-crash discriminator the█66|/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the█...█624|█*625|/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands█626|/// promptly mid-backoff (saturating - never an underflow panic).█627|fn sleep_backoff(bac [...8742ch elided...] off && !stop.load(Ordering::Relaxed) {█*1234| thread::sleep(TICK.min(backoff - slept));█*1235| slept += TICK;█1236| }█1237| }█1238|}█...█1286| // And it becomes due once enough has elapsed.█*1287| let earlier = now - Duration::from_millis(50);█1288| assert!(due(Some(earlier), false, now, Duration::from_millis(10)));█*1289| assert!(!due(Some(earlier), false, now, Duration::from_millis(500)));█1290| }█1291|█1292| // [unit->REQ-CONV-2] [unit->REQ-INST-3] the wake edge (a worker's█...█1308| // Wake on an already-elapsed leg is due (the same single round).█*1309| let earlier = now - Duration::from_millis(50);█1310| assert!(due(Some(earlier), true, now, Duration::from_millis(10)));█1311| }█1312|\u000f█</out>█# Assistant ¶█_**Inspecting project ID derivation from git**_█# Tool call ¶█//Reading project derivation IO█read(path=\"crates/spt-store/src/project.rs\", selector=\"1-240\")█<out>█\u000e//! `project_id` derivation (STORAGE §project_id) - the key that makes█//! project-context sync match across machines.█//!█//! Derived **from the project's git remote URL** when it has one: stable and█//! path-independent, so the same project at `/home/x/proj` and█//! `C:\\Users\\x\\proj` resolves to the same id (which is what lets D6's█//! `p-<project_id>` branches line up across nodes). A git repo without a█//! remote falls back to its toplevel **folder name**; a non-git dir falls back█//! to its own folder name (documented limitation: folder names are not█//! guaranteed path-independent across machines).█//!█//! The emitted id is a slug in the endpoint-id charset (4.6) - lowercase█//! `[a-z0-9-]` - so it is valid as a branch-name component and a directory█//! name everywhere, and two URL spellings of one remote (SSH vs HTTPS,█//! trailing `.git`, case) normalize to one id.█// [impl->REQ-STORE-1]█use std::path::Path;█use std::time::Duration;█use crate::gitrun::run_git;█/// Budget for the (local, fast) `git remote get-url` / `rev-parse` reads.█const PROJECT_GIT_TIMEOUT: Duration = Duration::from_secs(10);█/// Slug a remote URL (or any name) into the project-id charset: lowercase, [...6734ch elided...] oj-x\");█std::fs::create_dir_all(root.join(\"nested\")).unwrap();█run_git_ok(&[\"init\", &root.to_string_lossy()], None, None).unwrap();█// No remote -> display = toplevel folder.█let (id, disp) = project_id_and_display_for_dir(&root.join(\"nested\"));█assert_eq!((id.as_str(), disp.as_str()), (\"proj-x\", \"proj-x\"));█// With a remote -> id slugged, display = URL tail (case preserved).█run_git_ok(█&[\"-C\", &root.to_string_lossy(), \"remote\", \"add\", \"origin\", \"git@example.com:Team/CoolRepo.git\"],█None,█None,█)█.unwrap();█let (id, disp) = project_id_and_display_for_dir(&root.join(\"nested\"));█assert_eq!((id.as_str(), disp.as_str()), (\"example-com-team-coolrepo\", \"CoolRepo\"));█}█}█\u000f█</out>█# Tool call ¶█//Reading project ref derivation█read(path=\"crates/spt-store/src/projderive.rs\", selector=\"1-260\")█<out>█\u000e//! Project-attribution derivation kernel - the ONE implementation of the█//! precedence contract `session-cwd -> origin-cwd -> context-recency`█//! (ADR-0037 §4: behavioral parity is binding; rendered project IDs/display█//! names must not change).█//!█//! Lifted verbatim from the picker's `project_refs_from` (PROJECT-INDEX W2) so█//! the daemon's materialized-index writer and the legacy synchronous reader█//! (until the W3 cutover) share one kernel - parity by construction, not by█//! parallel maintenance. The picker wraps [`project_refs_from`] and drops the█//! [`SourceLeg`]; the writer persists it as `EndpointProject.source`█//! (observability, never rendered).█// [impl->REQ-PROJECT-INDEX-WRITER]█use std::path::Path;█use crate::sessions;█/// Which precedence leg decided an attribution (ADR-0037 §4). Serialized into█/// `EndpointProject.source` by the daemon writer; never rendered to users.█#[derive(Debug, Clone, Copy, PartialEq, Eq)]█pub enum SourceLeg {█/// Derived from the newest usable session-ledger cwd.█SessionCwd,█/// Derived from the endpoint's `info.cwd` origin (fresh endpoint, empty ledger).█OriginCwd,█/// A committed-context `p-*` branch membershi [...8251ch elided...] assert_eq!(█derive_calls.get(),█2,█\"derive runs once per DISTINCT non-excluded cwd\"█);█}█// [unit->REQ-PROJECT-INDEX-WRITER] path normalization: separators + case +█// trailing slash fold together (the cwd-identity dedup key), and the█// owlery-exclusion prefix test rides the same normalization.█#[test]█fn normalization_and_path_under() {█assert_eq!(normalize_path(Path::new(\"C:\\\\Users\\\\X\\\\Proj\\\\\")), \"c:/users/x/proj\");█assert!(path_under(█Path::new(\"C:\\\\home\\\\OWLERY\\\\a\\\\nested\"),█Path::new(\"c:/home/owlery\")█));█assert!(!path_under(Path::new(\"/home/owl\"), Path::new(\"/home/owlery\")));█assert!(!path_under(Path::new(\"/anything\"), Path::new(\"\")));█}█}█\u000f█</out>█# Tool call ¶█//Finding project function definitions█grep(pattern=\"fn project_id_and_display_for_dir|fn project_id_for_dir|fn git_root|read_to_string|canonicalize\", path=\"crates/spt-store/src/project.rs; crates/spt-store/src/projderive.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-store/src/█## project.rs█63|// [impl->REQ-STORE-1]█*64|pub fn project_id_for_dir(dir: &Path) -> String {█65| project_id_and_display_for_dir(dir).0█66|}█67|█...█92|// [impl->REQ-STORE-1] [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]█*93|pub fn project_id_and_display_for_dir(dir: &Path) -> (String, String) {█94| let dir_arg = dir.to_string_lossy().to_string();█95|█96| // 1. Remote URL (stable + path-independent): id = slug(url), display = URL tail.\u000f█</out>█# Tool call ¶█//Finding branch tree scan command█grep(pattern=\"pub fn ls_files_at|fn ls_files_at|ls-tree\", path=\"crates/spt-store/src/branchstore.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 345| // [impl->REQ-NET-3]█*346| pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {█347| let out = run_git_ok(█348| &[█349| \"-C\",█350| &self.dir_arg(),█*351| \"ls-tree\",█352| \"-r\",█353| \"--name-only\",█354| commitish,\u000f█</out>█# Tool call ¶█//Finding adapter registry scan█grep(pattern=\"pub fn registered|fn registered|read_dir|read_to_string|manifest.toml\", path=\"crates/spt-runtime/src/registry.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 25|//!█*26|//! Layout per adapter: `adapters/<name>/record.toml` (+ `manifest.toml` in█27|//! copy mode). Records write atomically (REQ-HAZARD-EBUSY-RENAME stance via█28|//! `spt_store::atomic`).█29|// [impl->REQ-INSTALL-4]█...█150|fn manifest_copy_file(adapters_dir: &Path, name: &str) -> PathBuf {█*151| adapters_dir.join(name).join(\"manifest.toml\")█152|}█153|█*154|/// Resolve the manifest file inside a source: `<dir>/manifest.toml`, or the█155|/// path itself when it already names a file.█156|fn source_manifest_file(source: &Path) -> PathBuf {█157| if source.is_dir() {█*158| source.join(\"manifest.toml\")█159| } else {█160| source.to_path_buf()█161| }█...█219| };█*220| match std::fs::read_to_string(&full) {█221| Ok(contents) => Some(Value::String(contents)),█222| Err(e) => {█223| eprintln!(█...█278| std::fs::create_dir_all(dst)?;█*279| for entry in std::fs::read_dir(src)? {█280| let entry = entry?;█281| let from = entry.path();█282| let to = dst.join(entry.file_name());█...█292|/// Register (or re-register) the adapter whose manifest lives at `source`█*293|/// (a directory conta [...2763ch elided...] adapters.join(\"equalfloor\").join(\"manifest.toml\").exists(),█1054| \"a copy-mode admit holds the manifest copy\"█1055| );█1056| }█...█1132| // Simulate the deferred/un-extracted state: the source manifest vanishes.█*1133| std::fs::remove_file(shell_src.join(\"manifest.toml\")).unwrap();█1134|█1135| // load_manifest -> actionable DeferredManifest naming the adapter, NOT Io.█1136| match load_manifest(&adapters, &rec) {█...█1155| /// Register the profiled shell adapter into a fresh `adapters/` dir.█*1156| fn registered_shell(tmp: &Path) -> PathBuf {█1157| let adapters = tmp.join(\"adapters\");█1158| let src = seed_source(tmp, \"ms-src\", SHELL_COPY_PROFILED);█1159| register(&adapters, &src, 1000).unwrap();\u000f█</out>█# Tool call ¶█//Reading pulse gate semantics█read(path=\"crates/spt-live/src/pulse.rs\", selector=\"1-120\")█<out>█\u000e//! pulse heartbeat (M2b T5) - the interim per-tick driver.█//!█//! In the no-daemon interim model, the long-running `api listen` process█//! (T8) hosts a pulse loop. Each tick does the lightweight, recurring work:█//! - **poll the drop dirs** -> fold any commune/signoff drops into context█//! (single-writer ingest->delete - T4);█//! - **fire the pending echo** -> if the echo-gate (`.more-done`, armed by█//! `api state idle`) is set, signal that an echo is due and **clear the gate**█//! so it fires *exactly once*;█//! - surface the **idle** flag for the caller's capsule/idle decisions.█//!█//! The pulse is deliberately thin - it decides *when* an echo is due and█//! guarantees once-only; the heavy echo run ([`crate::echo::run_echo_commune`])█//! is the caller's (T8), which holds the runtime + history + commune dir. Keep█//! all of this behind this one module so M3's daemon re-host (Psyche-as-loop,█//! daemon-owned period) is a localized swap, not a rewrite (mirrors M2a's█//! seed-file discipline).█//█// Interim only: the durable pulse loop + its global/per-endpoint period config█// belong to the M3 daemon (CONTEXT R-DAEMON-1). No dedicated REQ - the pulse is█// throwaway glue un [...2721ch elided...] te_dir_all(&perch).unwrap();█std::fs::write(resolve_echo_gate_file(id, ParentHint::Infer), \"\").unwrap();█}█#[test] // [unit->REQ-SEAM-PSYCHE] an armed echo-gate fires exactly once, then clears.█fn echo_gate_fires_exactly_once() {█with_home(|_| {█let drops = tempfile::tempdir().unwrap();█arm_gate(\"doyle\");█let t1 = tick(█\"doyle\",█\"proj-x\",█ParentHint::Infer,█drops.path(),█1000,█&PulseConfig::default(),█)█.expect(\"tick\");█assert!(t1.echo_due, \"armed gate should fire\");█// Second tick: gate cleared, no re-fire.█...█});█}█...█}█[Showing lines 1-123 of 253. Use :124 to continue]\u000f█</out>█# Assistant ¶█_**Inspecting info.json setters for project index nudges**█**Analyzing project invalidation frequency and impact**_█# Tool call ¶█//Tracing invalidation producers█grep(pattern=\"projinval::nudge|InvalScope::|nudge_at\", path=\"crates\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### projwriter.rs█1088| for i in 0..10 {█*1089| spt_store::projinval::nudge(&InvalScope::Endpoint {█1090| id: format!(\"b{}\", (i % 3) + 1),█1091| cwd: None,█1092| });█### sync.rs█453| if report.applied.iter().any(|a| a.branch.starts_with(\"p-\")) {█*454| spt_store::projinval::nudge(█*455| &spt_store::projinval::InvalScope::Global,█456| );█457| }█458| return Ok(report);█## spt-msg/src/█### ready.rs█137| // [impl->REQ-PROJECT-INDEX-INVALIDATION]█*138| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█139| id: id.to_string(),█140| cwd: rec.cwd.clone(),█141| });█## spt-store/src/█### contextstore.rs█155| if let Some(home) = self.root.parent() {█*156| let _ = crate::projinval::nudge_at(█157| &crate::projinval::invalidations_dir_under(home),█158| &scope,█159| [...3922ch elided...] _string(),█12691| cwd: None,█12692| });█#### api/█##### reporting.rs█135| // [impl->REQ-PROJECT-INDEX-INVALIDATION]█*136| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█137| id: id.to_string(),█138| cwd: cwd_for_nudge,█139| });█##### startup.rs█459| // [impl->REQ-PROJECT-INDEX-INVALIDATION]█*460| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█461| id: id.to_string(),█462| cwd: rec.cwd.clone(),█463| });█### tests/█#### projindex_writer_e2e.rs█293| .unwrap();█*294| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█295| id: \"ep3\".to_string(),█296| cwd: Some(dir_c.to_string_lossy().to_string()),█297| });\u000f█</out>█# Tool call ¶█//Tracing info writes█grep(pattern=\"fn set_last_active|pub fn set_last_active|fn write_info|pub fn write_info|projinval\", path=\"crates/spt-store/src/info.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 350|// [impl->REQ-UPD-4]█*351|pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {█352| mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))█353|}█354|█...█509|/// [`write_info`].█*510|fn write_info_unlocked(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {█511| let json = serde_json::to_string(info)█512| .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;█513| atomic_write_string_durable(&perch_path.join(\"info.json\"), &json)█...█532|// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]█*533|pub fn write_info(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {█534| let _lock = lock_perch_sentinel(perch_path)?;█535| write_info_unlocked(perch_path, info)█536|}█...█832| #[test]█*833| fn set_last_active_stamps_recency() {█834| let d = perch();█835| write_info(█836| d.path(),█...█1212| #[test]█*1213| fn write_info_racing_mutate_info_never_lost_update() {█1214| use std::sync::{Arc, Barrier};█1215|█1216| let d = perch();\u000f█</out>█# Tool call ¶█//Reading info mutation semantics█read(path=\"crates/spt-store/src/info.rs\", selector=\"1-220,300-460\")█<out>█\u000e//! `info.json` - the per-perch identity/status record at `<perch_path>/info.json`.█//!█//! Adapted from sister `types.rs` (`InfoJson` / `PidValue`) per ADR-0001:█//! - [`PidValue`] (untagged numeric-or-`\"BUSY\"`) is copied verbatim - the█//! `\"BUSY\"` sentinel is load-bearing (a busy poll-listener writes it; numeric█//! readers must treat it as \"no usable pid\").█//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)█//! tag string rather than the sister's `PerchState` enum (whose Spine/Touch█//! kinds the spt-core taxonomy drops - see T4).█//!█//! ## Torn-read tolerance (KNOWN-HAZARDS 1.2, REQ-HAZARD-INFO-JSON-TORN-READ)█//! A reader may hit the file mid-write. Two layers guarantee it never observes a█//! truncated record:█//! 1. **Writers go through [`write_info`]** -> [`crate::atomic::atomic_write_string_durable`]█//! (tmp + rename): a same-volume rename is atomic, so a concurrent reader█//! sees the *complete* old file or the *complete* new file, never a torn one.█//! 2. **Readers tolerate failure**: [`read_info`] / [`read_pid`] parse via a█//! bounded retry and return `None` on persistent failure - never panic.█use serde::{Deserialize, [...20058ch elided...] e first failure.█/// - `None` - clear it (a later successful host shows clean).█///█/// `status` is **never** touched - daemon liveness stays authoritative█/// (KH 2.5). Serialized via [`mutate_info`]; errors if the record is absent -█/// the daemon only stamps perches it hosts.█// [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]█pub fn set_psyche_host_error(perch_path: &Path, reason: Option<&str>) -> std::io::Result<()> {█mutate_info(perch_path, |rec| match reason {█Some(r) => {█let attempts = rec█.psyche_host_error█.as_ref()█.map(|e| e.attempts.saturating_add(1))█.unwrap_or(1);█rec.psyche_host_error = Some(PsycheHostError {█reason: r.to_string(),█...█});█}█...█})█}\u000f█</out>█# Tool call ¶█//Finding project invalidation events█grep(pattern=\"InvalScope::Endpoint|InvalScope::Global\", path=\"crates/spt-daemon/src; crates/spt-store/src; crates/spt-live/src; crates/spt/src\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/█## spt-daemon/src/█### projwriter.rs█1088| for i in 0..10 {█*1089| spt_store::projinval::nudge(&InvalScope::Endpoint {█1090| id: format!(\"b{}\", (i % 3) + 1),█1091| cwd: None,█1092| });█### sync.rs█454| spt_store::projinval::nudge(█*455| &spt_store::projinval::InvalScope::Global,█456| );█457| }█458| return Ok(report);█## spt-store/src/█### contextstore.rs█238| if committed.is_some() {█*239| self.nudge_index(crate::projinval::InvalScope::Global);█240| }█241| Ok(committed)█242| }█...█381| // membership view. [impl->REQ-PROJECT-INDEX-INVALIDATION]█*382| self.nudge_index(crate::projinval::InvalScope::Global);█*383| self.nudge_index(crate::projinval::InvalScope::Endpoint {█384| id: old_id.to_string(),█385| cwd: None,█386| });█*387| self.nudge_index(crate::projinval::InvalScope::Endpoint {█388| id: new_id.to_string(),█389| [...2264ch elided...] dir), \"garbage is consumed too, not left to wedge\");█227| }█228|}█## spt/src/█### cli.rs█12688| // [impl->REQ-PROJECT-INDEX-INVALIDATION]█*12689| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█12690| id: id.to_string(),█12691| cwd: None,█12692| });█### api/█#### reporting.rs█135| // [impl->REQ-PROJECT-INDEX-INVALIDATION]█*136| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█137| id: id.to_string(),█138| cwd: cwd_for_nudge,█139| });█#### startup.rs█459| // [impl->REQ-PROJECT-INDEX-INVALIDATION]█*460| spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {█461| id: id.to_string(),█462| cwd: rec.cwd.clone(),█463| });\u000f█</out>█# Assistant ¶█_**Investigating unexpected high I/O readings**█**Analyzing transcript read frequency and contextstore layout**_█# Tool call ¶█//Locating context store layout█grep(pattern=\"seed_git_dir|pub fn open_or_init_in|struct ContextStore|worktree|\\\\.seed\\\\.git\", path=\"crates/spt-store/src/contextstore.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 7|//! tracked/█*8|//! .seed.git bare BranchStore repo (the one true object DB)█*9|//! agents/<id>/ linked worktree of branch a-<id>█10|//! live-context.md per-agent live context -> syncs to ALL instances█*11|//! projects/<project_id>/ linked worktree of branch p-<project_id>█12|//! <id>/project-context.md per-(agent,project) -> same-project instances█13|//! ```█14|//!█15|//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle█*16|//! exchange ships); worktrees are the read/write view. Tier routing is the█17|//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind█18|//! everywhere, the `p-` branch holds every agent's context for one project█19|//! (cross-agent synthesis + query-routing ride that shape).█...█32|█*33|/// The per-agent live-context file inside an `a-<id>` worktree.█34|pub const LIVE_CONTEXT_FILE: &str = \"live-context.md\";█*35|/// The per-agent **live-role** file inside the same `a-<id>` worktree, beside█36|/// [`LIVE_CONTEXT_FILE`] (CONTEXT §live role). A durable statement of the█37|/// agent's broad purpose - replicates with the mind, renders FIRST [...10876ch elided...] arthur\"))█...█604|█*605| // p- arm: agent dir renamed inside the project worktree + committed.█606| assert_eq!(█607| cs.branch_store()█608| .read_at_tip(&project_branch(\"spt-core\"), \"arthur/project-context.md\")█...█688| // [unit->REQ-INST-15] delete-source removes EXACTLY the source: its a-█*689| // branch + worktree and its project slices go; the fork's mind and other█690| // agents' slices stay. Idempotent on an already-removed id.█691| #[test]█692| fn remove_endpoint_removes_exactly_the_source() {█...█713| !tmp.path().join(\"agents\").join(\"doyle\").exists(),█*714| \"worktree gone\"█715| );█716| assert!(█717| bs.read_at_tip(&project_branch(\"spt-core\"), \"doyle/project-context.md\")\u000f█</out>█# Tool call ¶█//Reading context store paths█read(path=\"crates/spt-store/src/contextstore.rs\", selector=\"1-120,170-260\")█<out>█\u000e//! The two-tier context store - `tracked/` realized over [`BranchStore`]█//! (M4-D6a; STORAGE §tracked/, ADR-0011, ADR-0013).█//!█//! Layout (the synced *mind*, physically separate from node-local `perches/`):█//!█//! ```text█//! tracked/█//! .seed.git bare BranchStore repo (the one true object DB)█//! agents/<id>/ linked worktree of branch a-<id>█//! live-context.md per-agent live context -> syncs to ALL instances█//! projects/<project_id>/ linked worktree of branch p-<project_id>█//! <id>/project-context.md per-(agent,project) -> same-project instances█//! ```█//!█//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle█//! exchange ships); worktrees are the read/write view. Tier routing is the█//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind█//! everywhere, the `p-` branch holds every agent's context for one project█//! (cross-agent synthesis + query-routing ride that shape).█//!█//! Writes here are **paths + commits only** - the precedence-marker guard█//! (6.5) stays in `spt-live::context`, which writes through the paths this█//! store hands out, then calls the c [...7394ch elided...] ** version as a tracked artifact beside the local█/// file - `.conflicts/<file>.<hash8>.md` inside the same worktree - and█/// checkpoint it. The local working file stays untouched (the active mind█/// is never clobbered by an unresolved conflict), and the artifact█/// replicates like any context file, so conflict state itself syncs.█/// Deterministic content-hash naming makes recording idempotent across█/// nodes - every holder of the same pair mints the same artifact.█// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]█pub fn record_conflict(█&self,█worktree: &Path,█file_rel: &str,█incoming: &[u8],█) -> std::io::Result<PathBuf> {█let dir = worktree.join(CONFLICT_DIR);█std::fs::create_dir_all(&dir)?;█...█}█...█}\u000f█</out>█# Tool call ¶█//Reading sync bundle streaming█read(path=\"crates/spt-daemon/src/sync.rs\", selector=\"180-300,310-445\")█<out>█\u000e/// `policy` **here**, fail-closed (REQ-INST-5: a peer must not be able to pull█/// a hidden/unsynced mind by naming it). `scratch_dir` holds the transient█/// bundle file.█// [impl->REQ-NET-3]█// [impl->REQ-INST-5]█// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]█pub fn serve_sync(█brain: &mut Brain,█stream_id: u64,█origin_node: &str,█net_from_seq: u64,█cs: &ContextStore,█policy: &SyncPolicy,█scratch_dir: &Path,█) -> io::Result<SyncServeOutcome> {█brain.net_stream_subscribe(stream_id, net_from_seq)?;█let mut decoder = SyncDecoder::new();█loop {█match brain.read_event()? {█BrokerEvent::NetStreamData {█stream_id: sid,█bytes,█..█} if sid == stream_id => {█let Some(rec) = decoder.push(&bytes).into_iter().next() else {█continue;█};█let SyncRecord::Request {█sync_id,█refs,█have_tips,█} = rec█else {█continue; // not a request - protocol noise, keep waiting█};█// The gate [...9334ch elided...] nch_store().fetch_bundle(&bundle)?;█let _ = std::fs::remove_file(&bundle);█for branch in &refs {█if let Some(tip) = tips.get(branch) {█report.applied.push(apply_fetched(cs, branch, tip)?);█}█}█// PROJECT-INDEX W3 (the W2-carried gap, ruled at█// cutover): a sync pull moves `p-*` tips on the RAW█// BranchStore - `apply_fetched`'s adopt leg can even█// CREATE a p-* branch (a membership change) - and█...█}█...█}█}█}█...█}█}█}\u000f█</out>█# Tool call ¶█//Reading branch tree scan█read(path=\"crates/spt-store/src/branchstore.rs\", selector=\"330-370\")█<out>█\u000e pub fn read_at(&self, commitish: &str, file: &str) -> std::io::Result<Option<Vec<u8>>> {█let out = run_git(█&[█...█&self.dir_arg(),█\"show\",█&format!(\"{commitish}:{file}\"),█],█None,█None,█GIT_TIMEOUT,█)?;█Ok(if out.success() {█Some(out.stdout)█} else {█None█})█}█/// Every tracked file path (worktree-relative) in the tree at `commitish`.█// [impl->REQ-NET-3]█pub fn ls_files_at(&self, commitish: &str) -> std::io::Result<Vec<String>> {█let out = run_git_ok(█&[█\"-C\",█&self.dir_arg(),█\"ls-tree\",█\"-r\",█\"--name-only\",█commitish,█],█None,█None,█)?;█Ok(String::from_utf8_lossy(&out.stdout)█.lines()█.map(str::to_string)█.filter(|l| !l.is_empty())█.collect())█}█/// Whether `commitish` names a commit this repo holds.█pub fn has_commit(&self, commitish: &str) -> std::io::Result<bool> {█let out = run_git(█&[█\"-C\",█&self.dir_arg(),█\"cat-file\",█\"-e\",█...█],█...█)?;█...█}█[Showing lines 329-373 of 959. Use :374 to continue]\u000f█</out>█# Tool call ¶█//Reading adapter registry reload█read(path=\"crates/spt-runtime/src/registry.rs\", selector=\"420-515\")█<out>█\u000epub fn deregister(adapters_dir: &Path, name: &str) -> Result<Option<String>, RegistryError> {█let mut record = load_record(adapters_dir, name)?;█record.active = false;█save_record(adapters_dir, &record)?;█let uninstall = load_manifest(adapters_dir, &record)█.ok()█.and_then(|m| m.update.and_then(|u| u.uninstall));█Ok(uninstall)█}█/// One adapter's record by name.█pub fn load_record(adapters_dir: &Path, name: &str) -> Result<AdapterRecord, RegistryError> {█let path = record_file(adapters_dir, name);█if !path.exists() {█return Err(RegistryError::NotRegistered(name.to_string()));█}█let s = std::fs::read_to_string(&path)?;█toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))█}█fn save_record(adapters_dir: &Path, record: &AdapterRecord) -> Result<(), RegistryError> {█let s = toml::to_string_pretty(record)█.map_err(|e| RegistryError::BadRecord(format!(\"{}: {e}\", record.name)))?;█atomic_write_string(&record_file(adapters_dir, &record.name), &s)?;█Ok(())█}█/// The record's manifest: the held copy in copy mode, the live source tree in█/// pointer mode (a delegated adapter's self-updat [...2440ch elided...] // Skip-diagnostics, never a silent drop (REQ-HAZARD-DEFERRED-MANIFEST):█// a pointer-mode adapter whose manifest isn't extracted yet (or any█// unreadable record) is omitted from the active set, but we say WHICH█// and WHY on stderr so it never vanishes without a trace (mirrors the█// [digest]/[strings] skip stance).█// [impl->REQ-HAZARD-DEFERRED-MANIFEST]█Err(e) => {█eprintln!(\"ADAPTER_SKIP:{} - {e}\", r.name);█None█}█})█.collect()█}█// ------------------------- Local profiles (M9 / REQ-MANIFEST-2) -------------█//█// A **local** profile is a node-local, user-authored sparse overlay registered█[Showing lines 419-518 of 1505. Use :519 to continue]\u000f█</out>█# Tool call ¶█//Reading notif DB query█read(path=\"crates/spt-store/src/notif.rs\", selector=\"220-275\")█<out>█\u000e pub fn mark_seen(&self, notif_id: &str, endpoint: &str) -> rusqlite::Result<bool> {█...█let res = (|| -> rusqlite::Result<bool> {█let Some(mut row) = self.read_row(notif_id)? else {█...█};█if !row.seen.insert(endpoint.to_string()) {█return Ok(false);█}█self.conn.execute(█\"UPDATE notifs SET seen = ?2 WHERE notif_id = ?1\",█params![notif_id, encode_seen(&row.seen)],█)?;█Ok(true)█})();█match &res {█Ok(_) => self.conn.execute_batch(\"COMMIT\")?,█Err(_) => self.conn.execute_batch(\"ROLLBACK\")?,█}█res█}█/// Advance `last_surfaced_ms` to `now_ms` if greater (max-merge - a lagging█/// clock never rewinds the suppression window). Returns whether it moved.█// [impl->REQ-NOTIF-1]█pub fn touch_surfaced(&self, notif_id: &str, now_ms: u64) -> rusqlite::Result<bool> {█let n = self.conn.execute(█\"UPDATE notifs SET last_surfaced_ms = ?2 WHERE notif_id = ?1 AND last_surfaced_ms < ?2\",█params![notif_id, now_ms as i64],█)?;█Ok(n > 0)█}█[...497ch elided...] ng set.█// [impl->REQ-NOTIF-1]█pub fn undismissed(&self, subnet: &str) -> rusqlite::Result<Vec<NotifRow>> {█self.select_rows(█\"subnet = ?1 AND dismissed = 0 ORDER BY created_ms ASC, notif_id ASC\",█params![subnet],█)█}█fn insert_row(&self, row: &NotifRow) -> rusqlite::Result<()> {█self.conn.execute(█\"INSERT INTO notifs (notif_id, subnet, kind, from_id, body, created_ms,█dismissed, seen, last_surfaced_ms)█VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)\",█params![█row.notif_id,█row.subnet,█row.kind,█row.from_id,█...█],█)?;█...█}█[Showing lines 219-278 of 566. Use :279 to continue]\u000f█</out>█# Assistant ¶█_**Analyzing frequent git process invocations**█**Tracing brain direct reads and dispatch patterns**█**Identifying subsecond file read loops**█**Investigating write_ready exe hashing**_█# Tool call ¶█//Tracing ready heartbeat reads█grep(pattern=\"fn write_ready|pub fn write_ready|current_exe_hash|exe_hash|sha256|std::fs::read\", path=\"crates/spt-daemon/src/brainproc.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 350|/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a█*351|/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads█352|/// the `pid` to confirm the brain is up and (on a change) that it respawned; the█353|/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) - the█354|/// supervisor that spawned generation N promotes only on a ready stamp **== N**,█355|/// so a stale stamp from a prior generation cannot falsely promote a brain that█*356|/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this█357|/// brain's own executable* - the \"which bytes are resident\" diagnostic: it proves█358|/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it█359|/// flips across a swap; the D7-4 field run asserts it matches the released█...█364|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█*365|fn write_ready(generation: u64) {█366| let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });█*367| // Best-effort: omit `exe_hash` if the self-read fails (additive - a reader█368| // tolerates its absence). Never fail the ready write over a diagn [...5005ch elided...] `exe_hash` flips).█1424| // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION]█1425| #[test]█*1426| fn appended_padding_flips_exe_hash() {█1427| let a = b\"the-fixture-A-bytes\".to_vec();█1428| let mut b = a.clone();█1429| b.extend_from_slice(&[0u8; 16]); // fixture B = A + appended padding█1430| assert_ne!(█*1431| crate::release::sha256_hex(&a),█*1432| crate::release::sha256_hex(&b),█*1433| \"appending padding must flip the exe_hash (the fixture-B swap proof)\"█1434| );█1435| }█1436|█...█1536| }█*1537| fn ready_exe_hash(&self) -> Option<String> {█1538| self.ready_hash.lock().unwrap().clone()█1539| }█1540| fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\u000f█</out>█# User ¶█Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `Main`.█Parent IRC message:█Fresh v0.36 evidence: repeated Registry-family stream subscriber poison. Examples stream 524/1161/1347/3053/3810: CONN_WRITE_POISONED after ~15s, DISPATCH_EV family=Registry endpoint=- elapsed 15.5-16.4s. Current broker ~550 threads; brain reads ~64 MiB/s. Investigate Registry stream lifecycle/replay specifically.█# Assistant ¶█_**Identifying frequent binary reads and planning registry investigation**_█# Tool call ¶█//Reporting brain I/O smoking gun█irc(op=\"send\", to=\"Main\", message=\"Brain 64 MiB/s is almost certainly self-hash loop: brainproc.rs:63 BRAIN_HEARTBEAT=500ms; run_brain loop :259-260 then write_ready(generation) :297 each heartbeat; write_ready :365-372 calls current_exe_hash every time; current_exe_hash :381-384 std::fs::read(current_exe) + SHA256. Thus read rate = 2 × spt.exe size/sec; a ~31.96 MiB binary predicts 63.9 MiB/s exactly. Source comment :379 wrongly says one exe read per brain start. Continuing Registry poison lifecycle.\", replyTo=null, await=false, from=null, timeoutMs=null, peek=null)█<out>█\u000eDelivered to 1 peer(s):█- Main: injected\u000f█</out>█# Assistant ¶█_**Investigating Registry subscriber poison**_█# Tool call ¶█//Locating registry serve worker█grep(pattern=\"serve_registry|RegistryDecoder|StreamFamily::Registry|net_stream_subscribe|RegistryApply\", path=\"crates/spt-daemon/src/dispatch.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 66|use spt_net::net::replicate::{█*67| FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,█68|};█69|use spt_net::net::wanmsg::{WanDecoder, WanReply};█70|use spt_store::contextstore::ContextStore;█...█141| } else if line.get(\"subnet\").is_some() && line.get(\"instance\").is_some() {█*142| StreamFamily::Registry█143| } else if line.get(\"subnet\").is_some()█144| && line.get(\"node\").is_some()█145| && line.get(\"label\").is_some()█...█149| // first line is a NodeLabelUpdate. Same serve arm, same gate.█*150| StreamFamily::Registry█151| } else {█152| StreamFamily::Unknown█153| }█...█663| let mut brain = connect(broker_name)?;█*664| brain.net_stream_subscribe(stream_id, floor_seq)?;█665| let mut buf: Vec<u8> = Vec::new();█666| loop {█667| match brain.read_event()? {█...█830| StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),█*831| StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),█832| StreamFamily: [...1165ch elided...] , \"epoch\": 4}});█*1249| assert_eq!(classify_first_line(&reg), StreamFamily::Registry);█1250| }█1251|█1252| // [unit->REQ-NET-1] unclassifiable shapes are Unknown - dropped loud,█...█1512| (StreamFamily::Sync, json!({\"endpoint_id\": \"x\", \"endpoint\": \"x\"}), None),█*1513| (StreamFamily::Registry, json!({\"target\": \"x\"}), None),█1514| // The right family with a blank/missing field -> None, not \"\".█1515| (StreamFamily::Attach, json!({\"endpoint_id\": \"\"}), None),█1516| (StreamFamily::Attach, json!({\"session_id\": 3}), None),█...█1543| StreamFamily::WanMsg,█*1544| StreamFamily::Registry,█1545| StreamFamily::Xfer,█1546| StreamFamily::Rest,█1547| StreamFamily::ShellLink,\u000f█</out>█# Tool call ¶█//Reading dispatcher worker lifecycle█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"590-950\")█<out>█\u000e/// wait for the next tick (batched, never one thread per row).█// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]█fn pool_has_room(active: usize) -> bool {█active < MAX_DISPATCH_WORKERS█}█/// Connect a fresh brain IPC client (retrying through broker startup).█fn connect(broker_name: &str) -> io::Result<Brain> {█let mut last = io::Error::other(\"broker never answered\");█for _ in 0..300 {█match Brain::cold_start(broker_name, now_ms()) {█Ok(b) => return Ok(b),█Err(e) => last = e,█}█thread::sleep(Duration::from_millis(10));█}█Err(last)█}█fn now_ms() -> u64 {█std::time::SystemTime::now()█.duration_since(std::time::UNIX_EPOCH)█.map(|d| d.as_millis() as u64)█.unwrap_or(0)█}█/// The stream's first NDJSON line, restart-durable (ADR-0038 B,█/// REQ-STREAM-OPENER-DURABLE): read the broker-pinned opener fact first -█/// it survives any ring roll - and fall back to the ring peek only when the█/// broker predates the verb (KH-2.3 N-1 window). `Ok(None)` = unclassifiable█/// (terminal); `Err` = transient (the claim requeues bounded).█fn first_line(█broker_name: &str,█stream_id: u64,█floor_seq: u6 [...14418ch elided...] hitelist gates AND whose shell█// tree resolves (owner exclusivity = resolution scope); the█// action runs against the LOCAL owlery - perch, manifest, spool,█// and binary all live here, the shell's node.█// [impl->REQ-SHELL-2]█match crate::linkhost::serve_shell_link(&mut brain, stream_id, origin, &paths.owlery) {█Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),█Err(e) => DispatchOutcome::Failed(e.to_string()),█}█}█StreamFamily::ServeProbe => {█// No gate (serveprobe module docs): the answer is public█// serve-state for a named subnet, read fresh from this node's█...█}█...█};█...█}█[Showing lines 589-953 of 1558. Use :954 to continue]\u000f█</out>█# Tool call ¶█//Tracing stream subscription handler█grep(pattern=\"KIND_NET_STREAM_SUBSCRIBE|NetStreamSubscribeReq|subscribe_stream|SubscriberSeat::install|subscriber busy|replay halted\", path=\"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## broker.rs█57| ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,█*58| BrokerImageReply, StallEvictsReply, NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,█59| NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,█60| Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,█61| KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_DIAL_SUBMIT,█...█66| KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,█*67| KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,█68| KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,█69| [...2757ch elided...] ing the dispatcher classifies█2630| // SeatBlocked), a HEALTHY p\u000erior is DISPLACED as before (the█2631| // newer-brain-takes-over contract a blanket no-reinstall broke - it█2632| // TIMEOUTed the applyhost brain-swap tests), and a fully-GONE prior█...█2653| assert_eq!(err.kind(), io::ErrorKind::WouldBlock);█*2654| assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");█2655|█2656| // Fully gone -> the replacement installs.█2657| log.subscriber.as_ref().unwrap().done.store(true, Ordering::Release);█...█2840| assert_eq!(err.kind(), io::ErrorKind::WouldBlock);█*2841| assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");█2842| }█2843|█2844| // [unit->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] the lease gate at the send\u000f█</out>█# Tool call ¶█//Tracing stream finish delivery█grep(pattern=\"fn finish|finish\\\\(|NetStreamEof|eof_envelope|finished\", path=\"crates/spt-daemon/src/nethost.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 49|use crate::msg::{█*50| net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,█51| NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,█52|};█53|█...█354| lease: Arc<StreamLease>,█*355| /// Read side finished cleanly (peer called finish / conn ended).█*356| finished: bool,█357| /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` => not█358| /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must█359| /// NOT drop bytes - CONTEXT:262 \"tunneled protocols cannot drop frames, so█...█382| lease: Arc::new(StreamLease::default()),█*383| finished: false,█384| retentive_cap: 0,█385| opener: OpenerCapture::Buffering(Vec::new()),█386| }█...█478| /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,█*479| /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so█*480| /// the poller sees EOF and closes. Empty bytes with `finished=false` => nothing█481| /// pending yet.█482| fn drain(&mut self) -> (Vec<u8>, bool) {█483| let mut [...6061ch elided...] REDISPATCH-FINISHED-RETIRE] a dead CONNECTION retires its█...█2603| // attach only fails): eof delivery through a poisoned seat removes it█*2604| // and cancels the lease, and the log still records finished.█2605| #[test]█*2606| fn finish_halts_and_removes_a_poisoned_seat() {█2607| let (sub, client, _recv) = seat_socket_pair(Duration::from_secs(5));█2608| let mut log = StreamLog::new(8, 64);█2609| log.begin_attach(Arc::clone(&sub), 0).expect(\"attach\");█...█2619|█*2620| log.finish();█*2621| assert!(log.finished, \"the read side still records its clean end\");█2622| assert!(log.subscriber.is_none(), \"halt-and-remove at the finish site\");█2623| assert!(lease.is_canceled(), \"poison at finish cancels the lease too\");█2624| }\u000f█</out>█# Tool call ¶█//Tracing registry feed sender█grep(pattern=\"push_feed|net_stream_send|finish|peer_step\", path=\"crates/spt-daemon/src/pump/registry.rs; crates/spt-daemon/src/pump/mod.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/pump/█## registry.rs█7|//! rotations -> re-advertise) ONCE per due round and holds the produced adverts█*8|//! as worker state; `peer_step` pushes this node's own rows for the given█9|//! subnet over the peer's feed stream. Construction-captured deps: the shared█10|//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the█11|//! two registry cadence knobs.█...█82| evict_after: Duration,█*83| /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.█84| adverts: Vec<RegistryFeedRecord>,█85|}█86|█...█123|█*124| /// The node-local sweep, run ONCE per due round before any `peer_step`█125| /// (§Frozen-7, order preserved verbatim):█126| fn pre_round(&mut self, _ctx: &RoundCtx) {█127| // Consume the re-pair eviction marker FIRST (M8 decision 13,█...█153| // Re-advertise this node's own rows; held as worker state for the█*154| // per-subnet `peer_step` filter below.█155| self.adverts = self.registry.advertise_local(&self.owlery);█156| }█157|█...█160| /// stream per feed.█*161| fn peer_step(█162| &mut self,█163| io: &mut PeerIo,█164| su [...4865ch elided...] due worker's `peer_step` for a CONNECTED peer across all subnets it█811|/// targets this round (one conn serves them all), applying [`peer_leg_outcome`]█812|/// after each - an ordinary failure drops the conn + backs the peer off and stops█813|/// this peer's remaining legs; a carrier desync bubbles to a supervised restart.█...█836| };█*837| workers[i].peer_step(&mut io, sub_name, peer_hex, ctx)█838| });█839| peer_leg_outcome(outcome, peer_hex, conns, sched, now)?;█840| if !conns.contains_key(peer_hex) {█...█1116|/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,█*1117|/// and finish it (the receiver's dispatcher classifies + applies). One█1118|/// stream per feed: streams are single-purpose by the demux contract.\u000f█</out>█# Assistant ¶█_**Requesting additional registry functions**_█# Tool call ¶█//Reading registry receive lifecycle█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"970-1045\")█<out>█\u000e█/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into█/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧█/// origin trusted in that subnet), until the peer finishes.█///█/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer█/// instance of an id observed transitioning to `Active` - its node's daemon█/// just gave it a driver, so a *locally hosted* instance of the same id█/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`█/// dormant). The rest edge's transition echo is gate-armed█/// ([`crate::resting::arm_transition_echo`]) - the endpoint's own pulse█/// loop runs the bounded summarizer call, never this worker (the KH 7.4█/// non-blocking posture: N flips in one feed are N gate files, not N LLM█/// calls serialized here).█// [impl->REQ-INST-7]█fn serve_registry_feed(█brain: &mut Brain,█stream_id: u64,█origin: &str,█registry: &RegistryHost,█) -> DispatchOutcome {█let policy = RegistryGatePolicy::load();█if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {█return DispatchOutcome::Failed(e.to_string());█}█let mut decoder = RegistryDecoder::new();█let [...1717ch elided...] e::resting::daemon_rest_event(█&id,█crate::resting::RestEvent::AttentionShift,█None,█) {█eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");█}█}█}█}█Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {█return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));█}█Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),█Ok(_) => {}█Err(e) => return DispatchOutcome::Failed(e.to_string()),█}█}█}█[Showing lines 969-1048 of 1558. Use :1049 to continue]\u000f█</out>█# Tool call ¶█//Reading stream log subscribe█read(path=\"crates/spt-daemon/src/nethost.rs\", selector=\"340-580,1935-1975\")█<out>█\u000estruct StreamLog {█...█/// `read_chunk` (or split one), so `next_seq` can be < the number of logical█/// chunks the sender issued for the SAME bytes. Correctness (byte-exact +█/// exactly-once) holds via these transport seqs + the recv cursor regardless;█/// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows█/// coalescing flake) and must gate on BYTES instead.█next_seq: u64,█ring: VecDeque<(u64, Vec<u8>)>,█cap_chunks: usize,█/// The installed subscriber, behind its bounded writer queue (ADR-0038█/// Amendment fixes 2+3): producers ENQUEUE here; conn I/O never runs█/// under this log's mutex.█subscriber: Option<SubscriberSeat>,█/// The stream's serve lease (Amendment fix 4): canceled on subscriber█/// POISON so the paired serve/wire worker's sends refuse.█lease: Arc<StreamLease>,█/// Read side finished cleanly (peer called finish / conn ended).█finished: bool,█/// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` => not█/// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must█/// NOT drop bytes - CONTEXT:262 \"tunneled protocols cannot drop fra [...11610ch elided...] )█})?;█Arc::clone(&e.log)█};█// Install the seat WITH its replay snapshot in one atomic critical█// section (ADR-0038 Amendment fix 3, gate follow-up): the snapshot█// handoff is a memory move; the seat's writer thread performs every█// conn write outside the mutex, replay first, live after - so a█// producer appending concurrently can never slot a live frame into█// the middle of the replay (the \"got seq 134 want 6\" re-serve red).█// A replay write failure poisons the seat writer-side; the next█// producer enqueue halt-and-removes it (the T1 discipline).█// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]█log.lock().unwrap().begin_attach(Arc::clone(&sub), from_seq)?;█...█}\u000f█</out>█# Tool call ¶█//Reading feed stream completion█read(path=\"crates/spt-daemon/src/pump/mod.rs\", selector=\"480-510,1105-1145\")█<out>█\u000e/// epoch-lease double-mint and is forbidden.█struct PeerIo<'a> {█/// The one brain IPC handle.█brain: &'a mut Brain,█/// The established peer conn id for this step.█conn_id: u64,█/// The pump's sole durable open-op counter. Workers mint open-ops only█/// through [`PeerIo::open_op`] (the sync + update legs); a worker-owned█/// `EpochSource` over this counter would be the KH epoch-lease double-mint.█ops: &'a mut EpochSource,█}█impl PeerIo<'_> {█/// Open a fresh stream and write one whole feed fire-and-forget (the█/// push-feed discipline - `op_id: None`, finished on send).█fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {█push_feed(self.brain, self.conn_id, feed)█}█/// Mint the next durable open-op from the pump's sole counter (exactly-once█/// open identity - a restart can never reuse one), tagged with the `pump`█/// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,█/// so its ops namespace under `pump` and never collide with cli/rc/shell ops.█fn open_op(&mut self) -> io::Result<MintedOp> {█Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))█}█}█/// Run the [...1147ch elided...] ying through broker█/// startup): the carrier is split at construction with the per-call deadline█/// armed ([`Brain::cold_start_pump`]), so the pump's reads are bounded from the█/// first tick (REQ-HAZARD-PUMP-IPC-DEADLINE).█// [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]█fn connect(broker_name: &str) -> io::Result<Brain> {█let mut last = io::Error::other(\"broker never answered\");█for _ in 0..300 {█match Brain::cold_start_pump(broker_name, now_ms(), PUMP_PEER_IO_TIMEOUT) {█Ok(b) => return Ok(b),█Err(e) => last = e,█}█thread::sleep(Duration::from_millis(10));█}█Err(last)█}█fn now_ms() -> u64 {█std::time::SystemTime::now()█.duration_since(std::time::UNIX_EPOCH)█.map(|d| d.as_millis() as u64)█.unwrap_or(0)█}\u000f█</out>█# Tool call ¶█//Reading broker unsubscribe cleanup█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"3210-3275\")█<out>█\u000e loop {█...█match env.kind.as_str() {█...█KIND_SESSIONS => {█...█// STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert█// each live session's control/viewer stamps to broker truth,█// divergence-gated + OFF the log lock. Closes the stamp-before-bind█// loss for a picker-created endpoint (its spawn stamp was swallowed█// pre-bind); converges within one reconcile-poll window after bind.█// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]█for s in &snaps {█converge_perch_stamps(█&s.endpoint,█s.controller_by.as_deref(),█s.has_controller,█s.viewers,█);█}█let reply = SessionsReply {█sessions: snaps█.into_iter()█.map(|s| SessionInfo {█session_id: s.id,█endpoint: s.endpoint [...1673ch elided...] 12 kin): `detach_if` re-stamps info.json█// (`viewer_count`/`driven_by`) and emits a lifecycle record - file +█// conn I/O that must never run under the shared sessions table lock,█// or one slow perch write stalls every spawn/attach/kill dispatch.█// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]█let logs: Vec<Arc<Mutex<OutputLog>>> = {█let sessions = recover(&self.sessions);█my_subs█.iter()█.filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))█.collect()█};█for log in &logs {█recover_log(log).detach_if(&send);█}█// Same for net streams: the streams + their rings persist (D4b).█[Showing lines 3209-3278 of 6298. Use :3279 to continue]\u000f█</out>█# Tool call ¶█//Finding unsubscribe net streams█grep(pattern=\"unsubscribe_stream|my_stream_subs|detach_stream|drop_subscriber|KIND_UNSUBSCRIBE\", path=\"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## broker.rs█68| KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,█*69| KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,█70|};█71|use crate::nethost::{NetHost, NET_EFFECT_SESSION};█72|use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};█...█3051| let mut my_subs: Vec<u64> = Vec::new();█*3052| let mut my_stream_subs: Vec<u64> = Vec::new();█3053| let mut my_presence_sub = false;█3054|█3055| loop {█...█3069| },█*3070| KIND_UNSUBSCRIBE => {█3071| if let Err(msg) = self.dispatch_unsubscribe(env, &send) {█3072| send_error(&send, &msg);█3073| }█...█3245| KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {█*3246| Ok(id) => my_stream_subs.push(id),█3247| Err(msg) => send_error(&send, &msg),█3248| },█3249| KIND_NET_PRESENCE_SUBSCRIBE => {█...█3279| if let Some(host) = self.net.get() {█*3280| for id in &my_stream_subs {█*3281| host.detach_stream_sub(*id, &send);█3282| }█3283| // And presence: the liveness log + its ring persist (D4c).█3284| if my_presence_sub {█## nethost.rs█1980| /// disconnect cleanup; the stream + its ring persist).█*1981| pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {█1982| let log = {█1983| let streams = self.shared.streams.lock().unwrap();█1984| streams.get(&stream_id).map(|e| Arc::clone(&e.log))\u000f█</out>","textHead":"# User ¶█Complete the assignment below, thoroughly:█# Target█Brain loops in crates/spt-daemon including project index, registry, dispatch, stream subscribers, and state scans. Non-goal: broker PTY ownership.█# Change█Find loops capable of reading ~64 MiB/s or polling at sub-second cadence. Check whether any loop repeatedly scans repository/transcript/log files under active sessions.█# Acceptance█Return ranked mechanisms with exact cadence, files read, citations, and live discriminating probes.█# Assistant ¶█_**Planning read-only investigation**_█# Tool call ¶█//Reading diagnosis workflow█read(path=\"skill://diagnose\", selector=\"1-220\")█<out>█\u000e---█name: diagnose█description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce -> minimise -> hypothesise -> instrument -> fix -> regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.█---█# Diagnose█A discipline for hard bugs. Skip phases only when explicitly justified.█When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.█## Phase 1 - Build a feedback loop█**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause - bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.█Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**█### Ways to construct one - try them in roughly this order█1. **Failing test** at whatever seam reaches the bug - unit, integration, e2e.█2. **Curl / HTTP script** against a running dev server [...5116ch elided...] # Phase 6 - Cleanup + post-mortem█Required before declaring done:█- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)█- [ ] Regression test passes (or absence of seam is documented)█- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)█- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)█- [ ] The hypothesis that turned out correct is stated in the commit / PR message - so the next debugger learns█**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before - you have more information now than when you started.█\u000f█</out>█# Assistant ¶█_**Locating crates and daemon code**_█# Tool call ¶█//Mapping daemon sources█glob(path=\"crates/spt-daemon/**/*.{rs,toml}\", hidden=true, gitignore=true, limit=500)█<out>█\u000e# crates/spt-daemon/█Cargo.toml█## src/█nethost.rs█dispatch.rs█broker.rs█attach.rs█brain.rs█msg.rs█seedproofx.rs█registryhost.rs█lib.rs█endpoint.rs█sync.rs█projwriter.rs█brainproc.rs█applyhost.rs█docshost.rs█daemon.rs█config.rs█update.rs█relcache.rs█release.rs█notif.rs█relay.rs█propagate.rs█livehost.rs█inject.rs█conn.rs█autostart.rs█stderrlog.rs█lifecycle.rs█digest.rs█pairhost.rs█xfer.rs█shellwake.rs█shellchan.rs█seedmap.rs█resting.rs█resthost.rs█linkhost.rs█effect.rs█adapter_update.rs█wan.rs█tunnelhub.rs█transport.rs█translation.rs█test_home.rs█shellhost.rs█shelldisc.rs█service.rs█serveprobe.rs█rollback_compat.rs█reconcile.rs█reap.rs█psyrelay.rs█presence.rs█notifsync.rs█machineid.rs█harnesshost.rs█grants.rs█frame.rs█drivehub.rs█digesthub.rs█deelevate.rs█deadline.rs█crc_swap.rs█consent.rs█codec.rs█access.rs█### pump/█mod.rs█health.rs█update.rs█sync.rs█registry.rs█notif.rs█### bin/█xlate_choreo_fixture.rs█## tests/█inject_control_wedge.rs█attach.rs█redispatch_stall.rs█false_promote.rs█conn_blackhole_lifecycle.rs█broker.rs█brain_decouple.rs█mesh_recovery.rs█redispatch.rs█input_ack_deadlock.rs█docs_server_e2e.rs█propagate.rs█pumpdeadline.rs█pump.rs█net_worker_starve.rs█brain_resume_conn_deadlock.rs█two_origin_spanning.rs█wake_single_flight.rs█endpoint_survival.rs█xfer.rs█wanmsg.rs█twohost.rs█replicate.rs█sync.rs█notifsync.rs█netstream.rs█netbroker.rs█idempotent.rs█dispatch.rs█daemon_e2e.rs█access.rs█shellchan.rs█seedproofx.rs█rosterprop.rs█resume.rs█reseed.rs█psyche_residency_expectation_e2e.rs█psyche_event_turn_e2e.rs█psyche_context_file_e2e.rs█presence.rs█pairjoin.rs█mesh.rs█legacy_resident_sweep_e2e.rs█handoff.rs█driven_by_selfheal.rs█digest.rs█daemon_lifecycle_real_brain.rs█control_stamp_lifetime.rs█budget.rs█brain_swap.rs█### fixtures/█dispatch_fixture.rs\u000f█</out>█# Tool call ¶█//Finding polling loops█grep(pattern=\"\\\\b(loop|while|interval|sleep|tick|poll|scan|refresh)\\\\b\", path=\"crates/spt-daemon\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/█## Cargo.toml█5|publish = false█*6|description = \"spt-core broker/brain daemon (M3b): the per-machine supervisor process. A stable broker kernel holds the un-transferable resources (PTY masters from spt-term, harness children, sockets) while a restartable brain holds all logic; a versioned local IPC lets a newer brain talk to an older broker so a self-update swaps the brain with zero endpoint interruption (ADR-0004). Internal supervisor (NOT public SDK, R-ARCH-2) - the `spt` binary is a thin CLI over it. B0 lands the crate + the version...█7|█8|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for█9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /█## src/█### applyhost.rs█34|//! `apply_brain_only` handoff, the regression that re-attached a CLI-side█*35|//! subscriber while the live brain kept running the OLD code.█36|//!█37|//! New code runs **immediately** - the supervised respawn lands on the new█38|//! executable, no manual bounce, no logon. Readiness-gated promotion +█...█134| // failed readiness on this node and auto-rolled-back is quarantined - never█*135| // re-applied, or the self-heal becomes a self-harm loo [...48942ch elided...] 85| const NUL: u16 = 0;█...█213| let mut buf = vec![0u8; buf_len as usize];█*214| loop {█215| let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };█216| let mut result: *mut libc::passwd = std::ptr::null_mut();█217| let rc = unsafe {█...█332| let mut buf = vec![0u8; buf_len as usize];█*333| loop {█334| let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };█335| let mut result: *mut libc::passwd = std::ptr::null_mut();█336| let rc = unsafe {█...█716| let mut n = 0usize;█*717| while !(*p.add(n) == 0 && *p.add(n + 1) == 0) {█718| n += 1;█719| }█[Showing lines 1-958 of 1175 (49.9KB limit). Use :959 to continue. Some lines truncated to 512 chars]\u000f█</out>█# Tool call ¶█//Finding filesystem reads█grep(pattern=\"read_dir|read_to_string|File::open|OpenOptions|BufReader|metadata\\\\(|canonicalize|walkdir|WalkDir|fs::read|tokio::fs|std::fs\", path=\"crates/spt-daemon\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## adapter_update.rs█389| let src = tmp.path().join(format!(\"{name}-src\"));█*390| std::fs::create_dir_all(&src).unwrap();█*391| std::fs::write(src.join(\"manifest.toml\"), body).unwrap();█392| src█393| };█394| let plain = seed(█## applyhost.rs█261| let aside = swap_aside_path(exe_path, version);█*262| let _ = std::fs::remove_file(&aside); // stale prior apply of this version█*263| std::fs::rename(exe_path, &aside).map_err(|e| format!(\"step current binary aside: {e}\"))?;█264| if let Err(e) = land_artifact(exe_path, artifact) {█*265| let _ = std::fs::rename(&aside, exe_path); // roll back - never half-swapped█266| return Err(format!(\"land new binary (rolled back): {e}\"));█267| }█268| // The binary IS swapped from here on - but DO NOT claim `applied` yet█...█444| use std::os::unix::fs::PermissionsExt;█*445| std::fs::set_permissions(exe_path, std::fs::Permissions::from_mode(0o755))?;█446| }█447| Ok(())█448|}█...█625| .collect();█*626| std::fs::write(█627| &keys,█628| serde_json::json!({\"keys\": {\"k\": [...31736ch elided...] /C exit 0\";█1079| #[cfg(unix)]█1080| let spawn = \"true\";█*1081| std::fs::write(█1082| src.join(\"manifest.toml\"),█1083| format!(█1084| \"[adapter]\\nname = \\\"Gated\\\"\\nkind = \\\"shell\\\"\\nversion = \\\"1\\\"\\n\\█## bin/█### xlate_choreo_fixture.rs█36|█*37|use std::fs::OpenOptions;█38|use std::io::{self, BufRead, Write};█39|█40|/// The fixed, recognizable payload token the choreography types into the PTY.█...█62| if let Some(path) = &stdin_log {█*63| if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {█64| let _ = writeln!(f, \"{trimmed}\");█65| }█66| }█Showing files 1-20 of 73+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding task spawns█grep(pattern=\"tokio::spawn|spawn_blocking|thread::spawn|JoinSet|FuturesUnordered|watch::|broadcast::|mpsc::|Notify\", path=\"crates/spt-daemon\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## applyhost.rs█513| let serve = Arc::clone(&broker);█*514| let handle = std::thread::spawn(move || {█515| let _ = serve.serve();█516| });█517| ServedBroker {█## autostart.rs█132| fn replay_bounded(budget: Duration) {█*133| let (tx, rx) = mpsc::channel();█*134| let handle = std::thread::spawn(move || {█135| replay_startup_endpoints(\"bogus-broker-socket-never-dialed\");█136| let _ = tx.send(());█137| });█## brain.rs█25|use std::io;█*26|use std::sync::mpsc::{self, Receiver, RecvTimeoutError};█27|use std::thread::{self, JoinHandle};█28|use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};█29|█...█232| let (mut recv, send) = stream.split();█*233| let (tx, frames) = mpsc::channel();█234| let reader = thread::Builder::new()█235| .name(\"pump-ipc-reader\".to_string())█236| .spawn(move || {█...█1843| use std::sync::atomic::{AtomicU32, Ordering};█*1844| use std::sync::mpsc::{sync_channel, SyncSender};█1845|█1846| // [unit->REQ-PUMP-DIAL-FASTFAIL] the peer-reply read budget is DECOUPLED from█1847| // the 30 [...18060ch elided...] 528| watcher_run(&owlery, &owner, &shell_id, &tokens, &params, &stop, || {█529| resolve_wake(&owlery, &owner, &shell_id, &adapter_name, &shell)█530| });█...█553|pub fn spawn_wake_host(stop: Arc<AtomicBool>) -> JoinHandle<()> {█*554| std::thread::spawn(move || {█555| let set = Arc::new(WakeSet::new());█556| let params = WakeParams::default();█557| while !stop.load(Ordering::SeqCst) {█## pump/█### mod.rs█1250|) -> thread::JoinHandle<()> {█*1251| thread::spawn(move || {█1252| let paths = PumpPaths::production();█1253| supervise_pump(&stop, SUPERVISE_BACKOFF_BASE, || {█1254| run_peer_pump(█Showing files 1-20 of 64+. Use skip=20 for the next page, or narrow paths/pattern.\u000f█</out>█# Tool call ¶█//Finding project registry dispatch█grep(pattern=\"project|registry|dispatch|subscriber|stream|state|transcript|log\", path=\"crates/spt-daemon/src\", selector=null, case=false, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## access.rs█4|//! order: subnet *visibility* (is the endpoint routable here at all? -█*5|//! `spt_store::visibility`, applied inside registry resolution) -> the█6|//! **endpoint access whitelist** (may *your node* remotely reach it? - THIS█7|//! module) -> *capability grants* (may this agent do this high-risk thing? -█8|//! [`crate::consent`], default-deny). The whitelist is the outer gate before█...█10|//!█*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)█12|//!█13|//! - **Origin-node gate:** the subject is the node the inbound interaction█14|//! *originates from* (where the operator is acting) - never the sender█15|//! endpoint's identity or home. The subject value MUST be the QUIC█*16|//! handshake-proven remote node id from the broker's conn/stream table,█17|//! never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).█18|//! - **Outbound** -> any visible node: the whitelist never restricts who an█19|//! endpoint talks *to*.█...█22|//! (\"established/related\").█*23|//! Classification reads the durable [`RecentOutbound`] log█24|//! ([`classify_inbound`]); senders feed it via [`note_outbound`].█25|//! - **Unsoli [...49026ch elided...] 4|//!█*5|//! ## On-demand projection, not a byte feed█6|//!█7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser█*8|//! on every output chunk. The digest is now a **projection of the same normalized█9|//! history the echo-commune consumes** - so there is nothing to feed: a snapshot█*10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's█*11|//! records via [`project_endpoint_digest`], and the hub publishes the result.█12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces█*13|//! read the logs.█14|//!█15|//! ## Sources (one per endpoint)█16|//!█17|//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or█[Showing lines 1-933 of 1154 (50.0KB limit). Use :934 to continue]\u000f█</out>█# Assistant ¶█_**Investigating project index and brainproc dependencies**_█# Tool call ¶█//Reading brain loop wiring█read(path=\"crates/spt-daemon/src/brainproc.rs\", selector=\"138-333\")█<out>█\u000e█/// The `spt daemon brain` child entry. Connect to the broker over the existing█/// socket IPC, signal readiness, host the brain-side logic loops, and idle -█/// holding the connection and probing broker liveness each heartbeat. A dropped█/// connection (broker gone, or a future planned snapshot+self-exit, ADR-0018 Q3)█/// returns `Err`, exiting the process so the broker-side supervisor respawns it█/// from the (possibly swapped) binary.█///█/// **D2-1 - net consumers run here.** The inbound dispatcher + outbound peer█/// pump are pure IPC clients (each `Brain::cold_start`s its own connection; no█/// shared `Arc<Broker>`), so they belong with the restartable brain, not the█/// broker - a brain respawn re-spawns them from the swapped binary. They are█/// meaningful only once the broker-owned NetHost","textTail":"\u000erior is DISPLACED as before (the█2631| // newer-brain-takes-over contract a blanket no-reinstall broke - it█2632| // TIMEOUTed the applyhost brain-swap tests), and a fully-GONE prior█...█2653| assert_eq!(err.kind(), io::ErrorKind::WouldBlock);█*2654| assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");█2655|█2656| // Fully gone -> the replacement installs.█2657| log.subscriber.as_ref().unwrap().done.store(true, Ordering::Release);█...█2840| assert_eq!(err.kind(), io::ErrorKind::WouldBlock);█*2841| assert!(err.to_string().contains(\"subscriber busy\"), \"{err}\");█2842| }█2843|█2844| // [unit->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] the lease gate at the send\u000f█</out>█# Tool call ¶█//Tracing stream finish delivery█grep(pattern=\"fn finish|finish\\\\(|NetStreamEof|eof_envelope|finished\", path=\"crates/spt-daemon/src/nethost.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e 49|use crate::msg::{█*50| net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,█51| NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DIAL_FAILED, PRESENCE_DISCONNECTED,█52|};█53|█...█354| lease: Arc<StreamLease>,█*355| /// Read side finished cleanly (peer called finish / conn ended).█*356| finished: bool,█357| /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` => not█358| /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must█359| /// NOT drop bytes - CONTEXT:262 \"tunneled protocols cannot drop frames, so█...█382| lease: Arc::new(StreamLease::default()),█*383| finished: false,█384| retentive_cap: 0,█385| opener: OpenerCapture::Buffering(Vec::new()),█386| }█...█478| /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,█*479| /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so█*480| /// the poller sees EOF and closes. Empty bytes with `finished=false` => nothing█481| /// pending yet.█482| fn drain(&mut self) -> (Vec<u8>, bool) {█483| let mut [...6061ch elided...] REDISPATCH-FINISHED-RETIRE] a dead CONNECTION retires its█...█2603| // attach only fails): eof delivery through a poisoned seat removes it█*2604| // and cancels the lease, and the log still records finished.█2605| #[test]█*2606| fn finish_halts_and_removes_a_poisoned_seat() {█2607| let (sub, client, _recv) = seat_socket_pair(Duration::from_secs(5));█2608| let mut log = StreamLog::new(8, 64);█2609| log.begin_attach(Arc::clone(&sub), 0).expect(\"attach\");█...█2619|█*2620| log.finish();█*2621| assert!(log.finished, \"the read side still records its clean end\");█2622| assert!(log.subscriber.is_none(), \"halt-and-remove at the finish site\");█2623| assert!(lease.is_canceled(), \"poison at finish cancels the lease too\");█2624| }\u000f█</out>█# Tool call ¶█//Tracing registry feed sender█grep(pattern=\"push_feed|net_stream_send|finish|peer_step\", path=\"crates/spt-daemon/src/pump/registry.rs; crates/spt-daemon/src/pump/mod.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/pump/█## registry.rs█7|//! rotations -> re-advertise) ONCE per due round and holds the produced adverts█*8|//! as worker state; `peer_step` pushes this node's own rows for the given█9|//! subnet over the peer's feed stream. Construction-captured deps: the shared█10|//! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the█11|//! two registry cadence knobs.█...█82| evict_after: Duration,█*83| /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.█84| adverts: Vec<RegistryFeedRecord>,█85|}█86|█...█123|█*124| /// The node-local sweep, run ONCE per due round before any `peer_step`█125| /// (§Frozen-7, order preserved verbatim):█126| fn pre_round(&mut self, _ctx: &RoundCtx) {█127| // Consume the re-pair eviction marker FIRST (M8 decision 13,█...█153| // Re-advertise this node's own rows; held as worker state for the█*154| // per-subnet `peer_step` filter below.█155| self.adverts = self.registry.advertise_local(&self.owlery);█156| }█157|█...█160| /// stream per feed.█*161| fn peer_step(█162| &mut self,█163| io: &mut PeerIo,█164| su [...4865ch elided...] due worker's `peer_step` for a CONNECTED peer across all subnets it█811|/// targets this round (one conn serves them all), applying [`peer_leg_outcome`]█812|/// after each - an ordinary failure drops the conn + backs the peer off and stops█813|/// this peer's remaining legs; a carrier desync bubbles to a supervised restart.█...█836| };█*837| workers[i].peer_step(&mut io, sub_name, peer_hex, ctx)█838| });█839| peer_leg_outcome(outcome, peer_hex, conns, sched, now)?;█840| if !conns.contains_key(peer_hex) {█...█1116|/// Open a fresh stream on `conn_id`, write one whole feed fire-and-forget,█*1117|/// and finish it (the receiver's dispatcher classifies + applies). One█1118|/// stream per feed: streams are single-purpose by the demux contract.\u000f█</out>█# Assistant ¶█_**Requesting additional registry functions**_█# Tool call ¶█//Reading registry receive lifecycle█read(path=\"crates/spt-daemon/src/dispatch.rs\", selector=\"970-1045\")█<out>█\u000e█/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into█/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧█/// origin trusted in that subnet), until the peer finishes.█///█/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer█/// instance of an id observed transitioning to `Active` - its node's daemon█/// just gave it a driver, so a *locally hosted* instance of the same id█/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`█/// dormant). The rest edge's transition echo is gate-armed█/// ([`crate::resting::arm_transition_echo`]) - the endpoint's own pulse█/// loop runs the bounded summarizer call, never this worker (the KH 7.4█/// non-blocking posture: N flips in one feed are N gate files, not N LLM█/// calls serialized here).█// [impl->REQ-INST-7]█fn serve_registry_feed(█brain: &mut Brain,█stream_id: u64,█origin: &str,█registry: &RegistryHost,█) -> DispatchOutcome {█let policy = RegistryGatePolicy::load();█if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {█return DispatchOutcome::Failed(e.to_string());█}█let mut decoder = RegistryDecoder::new();█let [...1717ch elided...] e::resting::daemon_rest_event(█&id,█crate::resting::RestEvent::AttentionShift,█None,█) {█eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");█}█}█}█}█Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {█return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));█}█Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),█Ok(_) => {}█Err(e) => return DispatchOutcome::Failed(e.to_string()),█}█}█}█[Showing lines 969-1048 of 1558. Use :1049 to continue]\u000f█</out>█# Tool call ¶█//Reading stream log subscribe█read(path=\"crates/spt-daemon/src/nethost.rs\", selector=\"340-580,1935-1975\")█<out>█\u000estruct StreamLog {█...█/// `read_chunk` (or split one), so `next_seq` can be < the number of logical█/// chunks the sender issued for the SAME bytes. Correctness (byte-exact +█/// exactly-once) holds via these transport seqs + the recv cursor regardless;█/// any test asserting `next_seq == logical-chunk-count` is wrong (the Windows█/// coalescing flake) and must gate on BYTES instead.█next_seq: u64,█ring: VecDeque<(u64, Vec<u8>)>,█cap_chunks: usize,█/// The installed subscriber, behind its bounded writer queue (ADR-0038█/// Amendment fixes 2+3): producers ENQUEUE here; conn I/O never runs█/// under this log's mutex.█subscriber: Option<SubscriberSeat>,█/// The stream's serve lease (Amendment fix 4): canceled on subscriber█/// POISON so the paired serve/wire worker's sends refuse.█lease: Arc<StreamLease>,█/// Read side finished cleanly (peer called finish / conn ended).█finished: bool,█/// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` => not█/// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must█/// NOT drop bytes - CONTEXT:262 \"tunneled protocols cannot drop fra [...11610ch elided...] )█})?;█Arc::clone(&e.log)█};█// Install the seat WITH its replay snapshot in one atomic critical█// section (ADR-0038 Amendment fix 3, gate follow-up): the snapshot█// handoff is a memory move; the seat's writer thread performs every█// conn write outside the mutex, replay first, live after - so a█// producer appending concurrently can never slot a live frame into█// the middle of the replay (the \"got seq 134 want 6\" re-serve red).█// A replay write failure poisons the seat writer-side; the next█// producer enqueue halt-and-removes it (the T1 discipline).█// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]█log.lock().unwrap().begin_attach(Arc::clone(&sub), from_seq)?;█...█}\u000f█</out>█# Tool call ¶█//Reading feed stream completion█read(path=\"crates/spt-daemon/src/pump/mod.rs\", selector=\"480-510,1105-1145\")█<out>█\u000e/// epoch-lease double-mint and is forbidden.█struct PeerIo<'a> {█/// The one brain IPC handle.█brain: &'a mut Brain,█/// The established peer conn id for this step.█conn_id: u64,█/// The pump's sole durable open-op counter. Workers mint open-ops only█/// through [`PeerIo::open_op`] (the sync + update legs); a worker-owned█/// `EpochSource` over this counter would be the KH epoch-lease double-mint.█ops: &'a mut EpochSource,█}█impl PeerIo<'_> {█/// Open a fresh stream and write one whole feed fire-and-forget (the█/// push-feed discipline - `op_id: None`, finished on send).█fn push_feed(&mut self, feed: &[u8]) -> io::Result<()> {█push_feed(self.brain, self.conn_id, feed)█}█/// Mint the next durable open-op from the pump's sole counter (exactly-once█/// open identity - a restart can never reuse one), tagged with the `pump`█/// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,█/// so its ops namespace under `pump` and never collide with cli/rc/shell ops.█fn open_op(&mut self) -> io::Result<MintedOp> {█Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))█}█}█/// Run the [...1147ch elided...] ying through broker█/// startup): the carrier is split at construction with the per-call deadline█/// armed ([`Brain::cold_start_pump`]), so the pump's reads are bounded from the█/// first tick (REQ-HAZARD-PUMP-IPC-DEADLINE).█// [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]█fn connect(broker_name: &str) -> io::Result<Brain> {█let mut last = io::Error::other(\"broker never answered\");█for _ in 0..300 {█match Brain::cold_start_pump(broker_name, now_ms(), PUMP_PEER_IO_TIMEOUT) {█Ok(b) => return Ok(b),█Err(e) => last = e,█}█thread::sleep(Duration::from_millis(10));█}█Err(last)█}█fn now_ms() -> u64 {█std::time::SystemTime::now()█.duration_since(std::time::UNIX_EPOCH)█.map(|d| d.as_millis() as u64)█.unwrap_or(0)█}\u000f█</out>█# Tool call ¶█//Reading broker unsubscribe cleanup█read(path=\"crates/spt-daemon/src/broker.rs\", selector=\"3210-3275\")█<out>█\u000e loop {█...█match env.kind.as_str() {█...█KIND_SESSIONS => {█...█// STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert█// each live session's control/viewer stamps to broker truth,█// divergence-gated + OFF the log lock. Closes the stamp-before-bind█// loss for a picker-created endpoint (its spawn stamp was swallowed█// pre-bind); converges within one reconcile-poll window after bind.█// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]█for s in &snaps {█converge_perch_stamps(█&s.endpoint,█s.controller_by.as_deref(),█s.has_controller,█s.viewers,█);█}█let reply = SessionsReply {█sessions: snaps█.into_iter()█.map(|s| SessionInfo {█session_id: s.id,█endpoint: s.endpoint [...1673ch elided...] 12 kin): `detach_if` re-stamps info.json█// (`viewer_count`/`driven_by`) and emits a lifecycle record - file +█// conn I/O that must never run under the shared sessions table lock,█// or one slow perch write stalls every spawn/attach/kill dispatch.█// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]█let logs: Vec<Arc<Mutex<OutputLog>>> = {█let sessions = recover(&self.sessions);█my_subs█.iter()█.filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))█.collect()█};█for log in &logs {█recover_log(log).detach_if(&send);█}█// Same for net streams: the streams + their rings persist (D4b).█[Showing lines 3209-3278 of 6298. Use :3279 to continue]\u000f█</out>█# Tool call ¶█//Finding unsubscribe net streams█grep(pattern=\"unsubscribe_stream|my_stream_subs|detach_stream|drop_subscriber|KIND_UNSUBSCRIBE\", path=\"crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/nethost.rs\", selector=null, case=true, gitignore=true, skip=null)█<out>█\u000e# crates/spt-daemon/src/█## broker.rs█68| KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,█*69| KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,█70|};█71|use crate::nethost::{NetHost, NET_EFFECT_SESSION};█72|use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};█...█3051| let mut my_subs: Vec<u64> = Vec::new();█*3052| let mut my_stream_subs: Vec<u64> = Vec::new();█3053| let mut my_presence_sub = false;█3054|█3055| loop {█...█3069| },█*3070| KIND_UNSUBSCRIBE => {█3071| if let Err(msg) = self.dispatch_unsubscribe(env, &send) {█3072| send_error(&send, &msg);█3073| }█...█3245| KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {█*3246| Ok(id) => my_stream_subs.push(id),█3247| Err(msg) => send_error(&send, &msg),█3248| },█3249| KIND_NET_PRESENCE_SUBSCRIBE => {█...█3279| if let Some(host) = self.net.get() {█*3280| for id in &my_stream_subs {█*3281| host.detach_stream_sub(*id, &send);█3282| }█3283| // And presence: the liveness log + its ring persist (D4c).█3284| if my_presence_sub {█## nethost.rs█1980| /// disconnect cleanup; the stream + its ring persist).█*1981| pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {█1982| let log = {█1983| let streams = self.shared.streams.lock().unwrap();█1984| streams.get(&stream_id).map(|e| Arc::clone(&e.log))\u000f█</out>"}}}
{"type":"message","id":"80682ca5","parentId":"d0e5cde3","timestamp":"2026-07-17T07:01:00.463Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting executable hash and size**"},{"type":"toolCall","id":"call_nYyxokunHrwB5yhXoxjFMsOG|fc_0e15f89f5b7b4062016a59d32bc6b881949b4b1d087d07cdf4","name":"glob","arguments":{"i":"Locating deployed binary","path":"**/spt.exe","hidden":true,"gitignore":false,"limit":50}},{"type":"toolCall","id":"call_LOkHljhdXaWoUprPqICWIzHO|fc_0e15f89f5b7b4062016a59d32bc6dc81949aec35d9edbd6f93","name":"read","arguments":{"i":"Reading brain ready loop","path":"crates/spt-daemon/src/brainproc.rs","selector":"330-410,580-675"}},{"type":"toolCall","id":"call_e980i9JPS6i6sSK6cSlhYSiR|fc_0e15f89f5b7b4062016a59d32bc700819486c9dfd282d4a124","name":"read","arguments":{"i":"Reading brain hash cache","path":"crates/spt-daemon/src/brain.rs","selector":"1525-1550"}},{"type":"toolCall","id":"call_i6v52LO6EgpUb3sQTT5CSSuB|fc_0e15f89f5b7b4062016a59d32bc70c8194beb34cd8ae68560c","name":"read","arguments":{"i":"Reading subscribe implementation","path":"crates/spt-daemon/src/brain.rs","selector":"1570-1610"}},{"type":"toolCall","id":"call_zxXCz7ToRXx9HlGdtdCFicuH|fc_0e15f89f5b7b4062016a59d32bc7148194b9dd5ff6c9847ca8","name":"read","arguments":{"i":"Reading dispatcher poll constants","path":"crates/spt-daemon/src/dispatch.rs","selector":"440-590,640-730"}},{"type":"toolCall","id":"call_gJteT9LSKRplaXCgwytZcU9x|fc_0e15f89f5b7b4062016a59d32bc7208194a071f6055232d2b8","name":"grep","arguments":{"i":"Finding current exe reads","pattern":"current_exe\\(|sha256_file\\(|ready_exe_hash\\(|write_ready\\(","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":55645,"output":318,"cacheRead":0,"cacheWrite":0,"totalTokens":55963,"reasoningTokens":41,"cost":{"input":0.278225,"output":0.00954,"cacheRead":0,"cacheWrite":0,"total":0.287765}},"stopReason":"toolUse","timestamp":1784271650510,"responseId":"resp_0e15f89f5b7b4062016a59d3234be881949ae8fde2968d324c","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d325b2fc81948ee4ac2a27e02c8d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdMruEek_-d4Fo7Lc9MAm21j47B4DI8ajBCDaWlvW-K2kEyT6HVvaaeGgto0mGAdubw-u-zMgOrBCzaXIkBpMcNtWfu8uEsetlhUJucoIIStRRrKNKN2KgYytQRWPdLZp3DpWIb-7g0FoJiOi2KQH1N5hJ3gjghRQF01BQne5TU_Cze3FBHc2aGIhnz5mbPJR1Kxkcb1a9aMQT_mGLUIxuY7h32SJ2S_ivsfzqwV5mCnjNTh5WucYAzhgYgODifn2Py6LCznJ_4gOkx8pr6VGM1mFFIeYsYXUIfyGPqozs__HicXl0u2JsPKFIFU_gKbL39b_5EHSUWgk_1rpRXwhwFvmhtP_Mvae4nVkb8ldeSJ9y1Cb41iqKmvAaALrnXBcgG55ebfW7HDT0wfO83SDW4yL3Znz0vjmZHNZvObE7uZxq10WzPHy5urNxbpc4kgdjLLPrfNsG7xSL5TxVqqAlSifpaIx2bAORv4fLRFRG8NJXWlaFQxuR4A1SymSp3ilZi9QKLdkWhth-uJb1qvapZsXKmD9M5c7GlCW3b0u7l9dviUNw6eF7T9gmr1p8NxP0pltelcw-4Mp-jQYL9trG6knm2_8Teum9hxSSgg8EI3jqeBEgkqt-VV3M-MPXm2epoMUJcBo36Ug-ygX3xnHevPIzB5q9mXhONrUqWouhu2_o7gJRFLatzhvlUR9tPzowXP4my8lXkg25ZdhJC89xZPmpXSFS3ESWizYGkJg1RBZMmE3mgxKRuj9qerOHm8CqmURNFLeIlsoGHccjMDjKdRNU_MbH2RXuN8QAqq8E7DPmcBhD1YCHlqkRkhlBT4Lj1bNQhg570x9eAs-5P9Owo8l89VHdgqIMs0_1aLx2Br3JlPvYPROpw_qaS1rHmXIFpj4LriFv7QXcdhoR_hdxy_yN0lBj6eNcwgM9MxXzIEjWEsdZoqPX0W1Ihj1aDJR7wgGVKbbj_abSC55vtnhjRW460MyqvvfS2wQcfkf27JobSjhzvpGR0ZOHj8muI3IAvkF5NpZdIJxFuREF0KvaxDBWOukYzi_c-xCnnICXtUczebK16lMx2mdSieFCH-Zu_GeYPuBYiptNKXK8igpSLXIfUv0QJBJR6NOwE2Rdd5lNvrZeNR_oN22yAH4xSpEvllj1GgBZEa032DKcUD7KFChA==","summary":[{"type":"summary_text","text":"**Inspecting executable hash and size**"}]},{"id":"fc_0e15f89f5b7b4062016a59d32bc6b881949b4b1d087d07cdf4","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating deployed binary\",\"path\":\"**/spt.exe\",\"hidden\":true,\"gitignore\":false,\"limit\":50}","call_id":"call_nYyxokunHrwB5yhXoxjFMsOG","name":"glob"},{"id":"fc_0e15f89f5b7b4062016a59d32bc6dc81949aec35d9edbd6f93","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain ready loop\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"330-410,580-675\"}","call_id":"call_LOkHljhdXaWoUprPqICWIzHO","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d32bc700819486c9dfd282d4a124","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain hash cache\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1525-1550\"}","call_id":"call_e980i9JPS6i6sSK6cSlhYSiR","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d32bc70c8194beb34cd8ae68560c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading subscribe implementation\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"1570-1610\"}","call_id":"call_i6v52LO6EgpUb3sQTT5CSSuB","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d32bc7148194b9dd5ff6c9847ca8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher poll constants\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"440-590,640-730\"}","call_id":"call_zxXCz7ToRXx9HlGdtdCFicuH","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d32bc7208194a071f6055232d2b8","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding current exe reads\",\"pattern\":\"current_exe\\\\(|sha256_file\\\\(|ready_exe_hash\\\\(|write_ready\\\\(\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_gJteT9LSKRplaXCgwytZcU9x","name":"grep"}]},"duration":9952.114299997687,"ttft":3201.2826000005007,"contextSnapshot":{"promptTokens":55645,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nYyxokunHrwB5yhXoxjFMsOG|fc_0e15f89f5b7b4062016a59d32bc6b881949b4b1d087d07cdf4","toolName":"glob","startedAt":"2026-07-17T07:01:00.464Z","args":{"path":"**/spt.exe"},"intent":"Locating deployed binary"},"id":"76753e85","parentId":"80682ca5","timestamp":"2026-07-17T07:01:00.464Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_LOkHljhdXaWoUprPqICWIzHO|fc_0e15f89f5b7b4062016a59d32bc6dc81949aec35d9edbd6f93","toolName":"read","startedAt":"2026-07-17T07:01:00.464Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading brain ready loop"},"id":"15f50f58","parentId":"76753e85","timestamp":"2026-07-17T07:01:00.464Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_e980i9JPS6i6sSK6cSlhYSiR|fc_0e15f89f5b7b4062016a59d32bc700819486c9dfd282d4a124","toolName":"read","startedAt":"2026-07-17T07:01:00.464Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading brain hash cache"},"id":"22783a29","parentId":"15f50f58","timestamp":"2026-07-17T07:01:00.464Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_i6v52LO6EgpUb3sQTT5CSSuB|fc_0e15f89f5b7b4062016a59d32bc70c8194beb34cd8ae68560c","toolName":"read","startedAt":"2026-07-17T07:01:00.464Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading subscribe implementation"},"id":"0c62ed86","parentId":"22783a29","timestamp":"2026-07-17T07:01:00.464Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zxXCz7ToRXx9HlGdtdCFicuH|fc_0e15f89f5b7b4062016a59d32bc7148194b9dd5ff6c9847ca8","toolName":"read","startedAt":"2026-07-17T07:01:00.464Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher poll constants"},"id":"07f6cd5a","parentId":"0c62ed86","timestamp":"2026-07-17T07:01:00.464Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gJteT9LSKRplaXCgwytZcU9x|fc_0e15f89f5b7b4062016a59d32bc7208194a071f6055232d2b8","toolName":"grep","startedAt":"2026-07-17T07:01:00.464Z","args":{"path":"crates/spt-daemon/src"},"intent":"Finding current exe reads"},"id":"00f89deb","parentId":"07f6cd5a","timestamp":"2026-07-17T07:01:00.464Z"}
{"type":"message","id":"daa49985","parentId":"00f89deb","timestamp":"2026-07-17T07:01:00.491Z","message":{"role":"toolResult","toolCallId":"call_LOkHljhdXaWoUprPqICWIzHO|fc_0e15f89f5b7b4062016a59d32bc6dc81949aec35d9edbd6f93","toolName":"read","content":[{"type":"text","text":"fn spawn_net_consumers(node_hex: &str) {\n…\n    if cfg.peer_pump {\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n    }\n}\n\n/// Connect to the broker, retrying briefly while it finishes binding.\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n    let mut last: Option<io::Error> = None;\n    for _ in 0..tries {\n        match Brain::cold_start(name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => {\n                last = Some(e);\n                thread::sleep(Duration::from_millis(10));\n            }\n        }\n    }\n    Err(last.unwrap_or_else(|| io::Error::other(\"brain could not connect to broker\")))\n}\n\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\n/// flips across a swap; the D7-4 field run asserts it matches the released\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn write_ready(generation: u64) {\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\n    if let Some(hash) = current_exe_hash() {\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n    }\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n}\n\n/// Lowercase-hex SHA-256 of the running process's own executable file\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n/// read + hash per brain start — negligible, and it pays rent far beyond the\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\nfn current_exe_hash() -> Option<String> {\n    let exe = std::env::current_exe().ok()?;\n    let bytes = std::fs::read(exe).ok()?;\n    Some(crate::release::sha256_hex(&bytes))\n}\n\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_generation(body: &str) -> Option<u64> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"generation\")?\n        .as_u64()\n}\n\n/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\n/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\n/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\n/// this to prove a respawn ran the swapped binary's bytes.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"exe_hash\")?\n        .as_str()\n        .map(str::to_string)\n}\n…\nimpl TrialEnv for ProductionTrialEnv {\n…\n    fn old_gen_drained(&self) -> bool {\n…\n    }\n}\n\n/// Fire the loud, resurfacing auto-rollback notif on every member subnet\n/// (restoration D6-2). Best-effort: a notif failure must never derail the\n/// supervisor (the rollback already happened — the daemon is healthy on the old\n/// binary). Reuses the one producer front door so the row rides first-fire AND\n/// boundary-resurface (an undismissed rollback row re-surfaces until the\n/// operator dismisses it — \"loud, not fire-once\").\n///\n/// **Called only with the brain DEAD.** Both rollback paths reap the candidate\n/// before `record_rolled_back` (PreReadyExit = already exited; WindowElapsedAlive\n/// = the A11 kill+wait precedes `rollback`), so no brain-side pump can be\n/// advertising and the [`spt_store::epoch::EpochSource`] single-writer assumption\n/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n/// any future broker-side live-notif is cross-process serialization (a file\n/// lock), not relaxing this rule.\nfn fire_rollback_notif(quarantine_version: u64, running_version: u64) {\n    let Ok(store) = spt_store::notif::NotifStore::open() else {\n        eprintln!(\"ROLLBACK_NOTIF_DROP: notif store unavailable\");\n        return;\n    };\n    let mut epochs = spt_store::epoch::EpochSource::load();\n    let policy = crate::notif::NotifSurfacePolicy::load();\n    let node_hex = crate::presence::local_node_hex();\n    let owlery = spt_store::perch::owlery_dir();\n    for (subnet, _) in &policy.subnets {\n        if let Err(e) = crate::notif::produce_rollback_notif(\n            quarantine_version,\n            running_version,\n            &store,\n            &node_hex,\n            &mut epochs,\n            subnet,\n            &policy,\n            &owlery,\n            now_ms(),\n        ) {\n            eprintln!(\"ROLLBACK_NOTIF_FAIL:{subnet}:{e}\");\n        }\n    }\n}\n\n/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n/// promptly mid-backoff (saturating — never an underflow panic).\nfn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n    let mut slept = Duration::ZERO;\n    while slept < backoff && !stop.load(Ordering::Relaxed) {\n        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n        slept += TICK;\n    }\n}\n\n/// The verdict of one trial-candidate's readiness watch (D6-2). The latch is\n/// the unpromoted `AppliedPending` record (**A8**), not the start reason — so a\n/// pre-ready crash respawn (`reason=Crash`) and a reboot-mid-trial cold spawn\n/// (`reason=Cold`) are *both* trials.\nenum TrialStep {\n    /// The candidate stamped `brain.ready` for THIS generation within the\n    /// window — the swap is accepted. The same child stays alive and falls\n    /// through to normal supervision (a later crash is a normal respawn).\n    Promoted,\n    /// The candidate **exited before** signaling ready (a failed boot) — counts\n    /// toward the [`TRIAL_PREREADY_BUDGET`]. The child is already reaped.\n    PreReadyExit,\n    /// The window elapsed with the child **alive but not promotable** — either\n    /// never ready (\"up but useless\") OR ready-but-the-old-generation-never-drained\n    /// (REQ-UPDATE-PROMOTE-DRAINED: a persistent old-gen wedge past the window).\n    /// The caller MUST kill it before spawning the rollback binary (**A11** — never\n    /// two live brains).\n    WindowElapsedAlive,\n    /// `stop` was raised during the trial — the caller kills + returns.\n    Stopped,\n}\n\n/// Watch a freshly-spawned trial candidate for readiness within `window`\n/// (ADR-0018 Q7, D6-2). Polls in `TICK` slices (so a daemon stop stays prompt,\n/// A11/note-ii) for a `brain.ready` stamp **== this spawn's `generation`** — a\n/// stale stamp from another generation never satisfies the gate (the single\n/// load-bearing correctness invariant; the A9 clear-before-spawn closes the\n/// cross-lifetime exact-generation hole). Returns the moment readiness is seen\n/// **AND the old generation has drained**, the child exits pre-ready, the window\n/// elapses, or `stop` is raised.\n///\n/// **The DRAINED precondition (REQ-UPDATE-PROMOTE-DRAINED).** `brain.ready` alone is\n/// NOT a promote: the W2 stall-evict only BOUNDS the false-promote window to\n/// `BRAIN_WRITE_DEADLINE`, so a new brain can signal ready while the OLD generation's\n/// local controller conn is still wedged (the 22:47 apply \"promoted\" onto a frozen\n/// control plane). So readiness is LATCHED (`ready_seen` — a candidate that stamped\n/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n/// the candidate and rolls back (conservative: never a false-promote onto a wedge)."}],"details":{"displayContent":{"text":"fn spawn_net_consumers(node_hex: &str) {\n…\n    if cfg.peer_pump {\n        let _ = crate::pump::spawn_peer_pump(broker_socket_name(), registry, cfg, stop);\n    }\n}\n\n/// Connect to the broker, retrying briefly while it finishes binding.\nfn connect_retry(name: &str, tries: u32) -> io::Result<Brain> {\n    let mut last: Option<io::Error> = None;\n    for _ in 0..tries {\n        match Brain::cold_start(name, now_ms()) {\n            Ok(b) => return Ok(b),\n            Err(e) => {\n                last = Some(e);\n                thread::sleep(Duration::from_millis(10));\n            }\n        }\n    }\n    Err(last.unwrap_or_else(|| io::Error::other(\"brain could not connect to broker\")))\n}\n\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\n/// flips across a swap; the D7-4 field run asserts it matches the released\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn write_ready(generation: u64) {\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\n    if let Some(hash) = current_exe_hash() {\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n    }\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n}\n\n/// Lowercase-hex SHA-256 of the running process's own executable file\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n/// read + hash per brain start — negligible, and it pays rent far beyond the\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\nfn current_exe_hash() -> Option<String> {\n    let exe = std::env::current_exe().ok()?;\n    let bytes = std::fs::read(exe).ok()?;\n    Some(crate::release::sha256_hex(&bytes))\n}\n\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_generation(body: &str) -> Option<u64> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"generation\")?\n        .as_u64()\n}\n\n/// Parse the `exe_hash` breadcrumb out of a `brain.ready` body — pure (D7-1).\n/// `None` for an absent field / garbled body (a legacy N-1 stamp carries none, so\n/// `None` is the N-1-readable answer, not an error). The D7-1 survival E2E reads\n/// this to prove a respawn ran the swapped binary's bytes.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"exe_hash\")?\n        .as_str()\n        .map(str::to_string)\n}\n…\nimpl TrialEnv for ProductionTrialEnv {\n…\n    fn old_gen_drained(&self) -> bool {\n…\n    }\n}\n\n/// Fire the loud, resurfacing auto-rollback notif on every member subnet\n/// (restoration D6-2). Best-effort: a notif failure must never derail the\n/// supervisor (the rollback already happened — the daemon is healthy on the old\n/// binary). Reuses the one producer front door so the row rides first-fire AND\n/// boundary-resurface (an undismissed rollback row re-surfaces until the\n/// operator dismisses it — \"loud, not fire-once\").\n///\n/// **Called only with the brain DEAD.** Both rollback paths reap the candidate\n/// before `record_rolled_back` (PreReadyExit = already exited; WindowElapsedAlive\n/// = the A11 kill+wait precedes `rollback`), so no brain-side pump can be\n/// advertising and the [`spt_store::epoch::EpochSource`] single-writer assumption\n/// (epoch.rs) holds at this `EpochSource::load`. **Do NOT fire broker-side notifs\n/// while a brain is live:** concurrent `EpochSource` read-modify-write across the\n/// two processes mints duplicate epochs (KH 4.8 lost-update). The forward fix for\n/// any future broker-side live-notif is cross-process serialization (a file\n/// lock), not relaxing this rule.\nfn fire_rollback_notif(quarantine_version: u64, running_version: u64) {\n    let Ok(store) = spt_store::notif::NotifStore::open() else {\n        eprintln!(\"ROLLBACK_NOTIF_DROP: notif store unavailable\");\n        return;\n    };\n    let mut epochs = spt_store::epoch::EpochSource::load();\n    let policy = crate::notif::NotifSurfacePolicy::load();\n    let node_hex = crate::presence::local_node_hex();\n    let owlery = spt_store::perch::owlery_dir();\n    for (subnet, _) in &policy.subnets {\n        if let Err(e) = crate::notif::produce_rollback_notif(\n            quarantine_version,\n            running_version,\n            &store,\n            &node_hex,\n            &mut epochs,\n            subnet,\n            &policy,\n            &owlery,\n            now_ms(),\n        ) {\n            eprintln!(\"ROLLBACK_NOTIF_FAIL:{subnet}:{e}\");\n        }\n    }\n}\n\n/// Sleep out a capped backoff in `TICK` slices so a `stop` request lands\n/// promptly mid-backoff (saturating — never an underflow panic).\nfn sleep_backoff(backoff: Duration, stop: &AtomicBool) {\n    let mut slept = Duration::ZERO;\n    while slept < backoff && !stop.load(Ordering::Relaxed) {\n        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n        slept += TICK;\n    }\n}\n\n/// The verdict of one trial-candidate's readiness watch (D6-2). The latch is\n/// the unpromoted `AppliedPending` record (**A8**), not the start reason — so a\n/// pre-ready crash respawn (`reason=Crash`) and a reboot-mid-trial cold spawn\n/// (`reason=Cold`) are *both* trials.\nenum TrialStep {\n    /// The candidate stamped `brain.ready` for THIS generation within the\n    /// window — the swap is accepted. The same child stays alive and falls\n    /// through to normal supervision (a later crash is a normal respawn).\n    Promoted,\n    /// The candidate **exited before** signaling ready (a failed boot) — counts\n    /// toward the [`TRIAL_PREREADY_BUDGET`]. The child is already reaped.\n    PreReadyExit,\n    /// The window elapsed with the child **alive but not promotable** — either\n    /// never ready (\"up but useless\") OR ready-but-the-old-generation-never-drained\n    /// (REQ-UPDATE-PROMOTE-DRAINED: a persistent old-gen wedge past the window).\n    /// The caller MUST kill it before spawning the rollback binary (**A11** — never\n    /// two live brains).\n    WindowElapsedAlive,\n    /// `stop` was raised during the trial — the caller kills + returns.\n    Stopped,\n}\n\n/// Watch a freshly-spawned trial candidate for readiness within `window`\n/// (ADR-0018 Q7, D6-2). Polls in `TICK` slices (so a daemon stop stays prompt,\n/// A11/note-ii) for a `brain.ready` stamp **== this spawn's `generation`** — a\n/// stale stamp from another generation never satisfies the gate (the single\n/// load-bearing correctness invariant; the A9 clear-before-spawn closes the\n/// cross-lifetime exact-generation hole). Returns the moment readiness is seen\n/// **AND the old generation has drained**, the child exits pre-ready, the window\n/// elapses, or `stop` is raised.\n///\n/// **The DRAINED precondition (REQ-UPDATE-PROMOTE-DRAINED).** `brain.ready` alone is\n/// NOT a promote: the W2 stall-evict only BOUNDS the false-promote window to\n/// `BRAIN_WRITE_DEADLINE`, so a new brain can signal ready while the OLD generation's\n/// local controller conn is still wedged (the 22:47 apply \"promoted\" onto a frozen\n/// control plane). So readiness is LATCHED (`ready_seen` — a candidate that stamped\n/// ready then exited is not demoted, preserving the ready-then-exit-still-promotes\n/// semantic) and promotion waits for `env.old_gen_drained()` too. While ready-seen\n/// but not-yet-drained the watch keeps polling to `deadline`; if the drain never\n/// comes the window elapses to [`TrialStep::WindowElapsedAlive`] — the caller KILLS\n/// the candidate and rolls back (conservative: never a false-promote onto a wedge).","startLine":321,"lineNumbers":[321,null,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,null,534,null,572,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"}}},"isError":false,"timestamp":1784271660491}}
{"type":"message","id":"30581347","parentId":"daa49985","timestamp":"2026-07-17T07:01:00.491Z","message":{"role":"toolResult","toolCallId":"call_zxXCz7ToRXx9HlGdtdCFicuH|fc_0e15f89f5b7b4062016a59d32bc7148194b9dd5ff6c9847ca8","toolName":"read","content":[{"type":"text","text":"pub fn run_dispatch_loop(\n…\n) -> io::Result<()> {\n…\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now(), breaker_window),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Bounded redispatch worker pool (ADR-0038 Amendment fix 7): a cold stream\n/// table claims at most this many rows per poll tick.\nconst MAX_DISPATCH_WORKERS: usize = 8;\n\n/// PURE pool gate (Amendment fix 7): may the loop claim another row right\n/// now? The cold-enumeration break rides this — a refresh with a large\n/// backlog claims at most [`MAX_DISPATCH_WORKERS`] rows per poll, the rest\n/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n…\nfn first_line(\n…\n) -> io::Result<Option<serde_json::Value>> {\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n…\n        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n        // retry then manufactured a throwaway replay subscriber against a\n        // possibly-wedged conn (the v0.34 stall engine's first gear).\n        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n        Err(e) if is_unsupported_verb(&e) => {}\n        // Everything else is a transient: Failed -> classified bounded\n        // requeue, never a second replay subscriber.\n        Err(e) => return Err(e),\n    }\n    peek_first_line(broker_name, stream_id, floor_seq)\n}\n\n/// Peek the stream's first retained NDJSON line on a throwaway brain\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\n/// gap check.\nfn peek_first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n/// The endpoint identity a stream's opener names, where the family carries\n/// one (Amendment fix 9 — hertz's live catch on stream 7058 could not bind\n/// the stalled stream to an endpoint; this key is what makes the next catch\n/// a bound stream→endpoint proof). Families without an endpoint concept\n/// answer `None` honestly.\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn line_endpoint(family: StreamFamily, line: &serde_json::Value) -> Option<String> {\n    let key = match family {\n        StreamFamily::Attach => \"endpoint_id\",\n        StreamFamily::Rest | StreamFamily::Xfer => \"endpoint\",\n        StreamFamily::ShellLink => \"owner\",\n        StreamFamily::WanMsg => \"target\",\n        _ => return None,\n    };\n    line.get(key)\n        .and_then(|v| v.as_str())\n        .filter(|s| !s.is_empty())\n        .map(str::to_string)\n}\n\n/// A worker's report: the outcome plus the telemetry keys it resolved\n/// (family + endpoint, once the opener classified) — Amendment fix 9.\nstruct WorkerReport {\n    outcome: DispatchOutcome,\n    family: Option<StreamFamily>,\n    endpoint: Option<String>,\n}\n\nimpl WorkerReport {\n    fn bare(outcome: DispatchOutcome) -> WorkerReport {\n        WorkerReport {\n            outcome,\n            family: None,\n            endpoint: None,\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn worker(\n…\n) -> WorkerReport {\n…\n}"}],"details":{"displayContent":{"text":"pub fn run_dispatch_loop(\n…\n) -> io::Result<()> {\n…\n    let claims: Arc<Mutex<HashMap<u64, ClaimState>>> = Arc::new(Mutex::new(HashMap::new()));\n    // Dispatcher generation id (Amendment fix 9): every telemetry record this\n    // loop emits carries it, so per-generation attach/poison censuses line up.\n    let generation = now_ms();\n    // Global circuit breaker (Amendment fix 5) + its current window, shared\n    // with workers so a seat-blocked retry paces on it.\n    let breaker: Arc<Mutex<CircuitBreaker>> = Arc::new(Mutex::new(CircuitBreaker::default()));\n    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n    // with a large backlog) claims at most this many rows per poll — the\n    // rest wait for the next tick, batching the cold enumeration instead of\n    // spawning one thread per row. No claim lock is held across any I/O:\n    // enumeration runs lock-free, the claim map is touched in short scopes.\n    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    while !stop.load(Ordering::Relaxed) {\n        if breaker.lock().unwrap().open(Instant::now()) {\n            // Circuit open: a poisoned replay just happened — pause ALL\n            // claiming for the window instead of hammering the wedged conn\n            // with fresh replay subscribers (Amendment fix 5).\n            thread::sleep(poll);\n            continue;\n        }\n        let reply = brain.net_streams()?;\n        for s in reply.streams {\n            // Claim only peer-initiated rows with bytes (a QUIC stream is\n            // invisible until its first write anyway — gotcha #6 — but the\n            // next_seq guard also skips a row whose pump hasn't appended yet).\n            // Terminal rows never re-enter; a released (Retry) claim re-enters\n            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n            if s.initiated_locally || s.next_seq == 0 {\n                continue;\n            }\n            if !pool_has_room(active_workers.load(Ordering::Relaxed)) {\n                break; // pool full: the rest of the table waits for the next poll\n            }\n            let attempts = {\n                let mut c = claims.lock().unwrap();\n                match should_claim(c.get(&s.stream_id), Instant::now()) {\n                    Some(attempts) => {\n                        c.insert(s.stream_id, ClaimState::InFlight { attempts });\n                        attempts\n                    }\n                    None => continue,\n                }\n            };\n            let name = broker_name.to_string();\n            let origin = s.remote_id_hex.clone();\n            let stream_id = s.stream_id;\n            let conn_id = s.conn_id;\n            let floor_seq = s.floor_seq;\n            let finished = s.finished;\n            let p = paths.clone();\n            let reg = Arc::clone(&registry);\n            let claims_done = Arc::clone(&claims);\n            let breaker_done = Arc::clone(&breaker);\n            let pool = Arc::clone(&active_workers);\n            pool.fetch_add(1, Ordering::Relaxed);\n            thread::spawn(move || {\n                let started = Instant::now();\n                let report = worker(&name, stream_id, &origin, floor_seq, finished, &p, &reg);\n                pool.fetch_sub(1, Ordering::Relaxed);\n                let outcome = report.outcome;\n                let elapsed_ms = started.elapsed().as_millis();\n                if let DispatchOutcome::Served(detail) = &outcome {\n                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n                    // this stream's exchange is over — take its row out of\n                    // every FUTURE dispatcher generation's enumeration (the\n                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n                    // an older broker without the verb answers error (KH-2.3)\n                    // and the row retires when the broker cycles.\n                    if let Ok(mut retire_brain) = connect(&name) {\n                        let _ = retire_brain.net_stream_retire(stream_id);\n                    }\n                    // Breaker reset = REAL progress only (gate round 1): a row\n                    // that vanished before serve proves nothing about the\n                    // wedge — a sibling's row-gone would close the breaker\n                    // AND zero its doubling ladder mid-wedge. A real serve\n                    // (including a finished-attach retirement) resets.\n                    // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n                    if !detail.contains(\"terminal pre-serve\") {\n                        breaker_done.lock().unwrap().reset();\n                    }\n                } else {\n                    // Keyed worker-outcome record (Amendment fix 9) — the\n                    // legacy DISPATCH:<id> token is kept as a prefix so\n                    // existing field greps keep matching.\n                    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n                    eprintln!(\"DISPATCH:{stream_id}:{outcome:?}\");\n                    eprintln!(\n                        \"{}\",\n                        dispatch_event(\n                            generation,\n                            stream_id,\n                            report.family,\n                            attempts,\n                            conn_id,\n                            \"worker-outcome\",\n                            &format!(\n                                \"{outcome:?} elapsed_ms={elapsed_ms} endpoint={}\",\n                                report.endpoint.as_deref().unwrap_or(\"-\")\n                            ),\n                        )\n                    );\n                }\n                // Seat-blocked failures trip the GLOBAL breaker; the claim\n                // requeue paces on the breaker window (never burning the\n                // bounded transient budget — T5's recovery discriminator).\n                let breaker_window = match &outcome {\n                    DispatchOutcome::Failed(msg)\n                        if classify_failure(msg) == FailClass::SeatBlocked =>\n                    {\n                        let mut b = breaker_done.lock().unwrap();\n                        let window = b.trip(Instant::now());\n                        eprintln!(\n                            \"{}\",\n                            dispatch_event(\n                                generation,\n                                stream_id,\n                                None,\n                                attempts,\n                                conn_id,\n                                \"breaker-trip\",\n                                &format!(\"window_ms={}\", window.as_millis()),\n                            )\n                        );\n                        window\n                    }\n                    _ => Duration::ZERO,\n                };\n                let mut c = claims_done.lock().unwrap();\n                c.insert(\n                    stream_id,\n                    outcome_transition(&outcome, attempts, Instant::now(), breaker_window),\n                );\n            });\n        }\n        thread::sleep(poll);\n    }\n    Ok(())\n}\n\n/// Bounded redispatch worker pool (ADR-0038 Amendment fix 7): a cold stream\n/// table claims at most this many rows per poll tick.\nconst MAX_DISPATCH_WORKERS: usize = 8;\n\n/// PURE pool gate (Amendment fix 7): may the loop claim another row right\n/// now? The cold-enumeration break rides this — a refresh with a large\n/// backlog claims at most [`MAX_DISPATCH_WORKERS`] rows per poll, the rest\n/// wait for the next tick (batched, never one thread per row).\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n…\nfn first_line(\n…\n) -> io::Result<Option<serde_json::Value>> {\n    match connect(broker_name).and_then(|mut b| b.net_stream_opener(stream_id)) {\n…\n        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n        // retry then manufactured a throwaway replay subscriber against a\n        // possibly-wedged conn (the v0.34 stall engine's first gear).\n        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n        Err(e) if is_unsupported_verb(&e) => {}\n        // Everything else is a transient: Failed -> classified bounded\n        // requeue, never a second replay subscriber.\n        Err(e) => return Err(e),\n    }\n    peek_first_line(broker_name, stream_id, floor_seq)\n}\n\n/// Peek the stream's first retained NDJSON line on a throwaway brain\n/// connection (the pre-ADR-0038 fallback — subject to ring eviction, kept\n/// only for the older-broker window). Subscribes from `floor_seq` (the oldest\n/// retained chunk) so a rolled ring replays instead of tripping the brain's\n/// gap check.\nfn peek_first_line(\n    broker_name: &str,\n    stream_id: u64,\n    floor_seq: u64,\n) -> io::Result<Option<serde_json::Value>> {\n    let mut brain = connect(broker_name)?;\n    brain.net_stream_subscribe(stream_id, floor_seq)?;\n    let mut buf: Vec<u8> = Vec::new();\n    loop {\n        match brain.read_event()? {\n            BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            } if sid == stream_id => {\n                buf.extend_from_slice(&bytes);\n                if let Some(nl) = buf.iter().position(|&b| b == b'\\n') {\n                    return Ok(serde_json::from_slice(&buf[..nl]).ok());\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                // Finished without a complete line: classify what we have\n                // (a line without a trailing newline before EOF still counts).\n                return Ok(serde_json::from_slice(&buf).ok());\n            }\n            BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n            _ => {}\n        }\n    }\n}\n\n/// One claimed stream's worker: peek → classify → run the family's serve\n/// loop on a fresh brain connection.\n/// The endpoint identity a stream's opener names, where the family carries\n/// one (Amendment fix 9 — hertz's live catch on stream 7058 could not bind\n/// the stalled stream to an endpoint; this key is what makes the next catch\n/// a bound stream→endpoint proof). Families without an endpoint concept\n/// answer `None` honestly.\n// [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\nfn line_endpoint(family: StreamFamily, line: &serde_json::Value) -> Option<String> {\n    let key = match family {\n        StreamFamily::Attach => \"endpoint_id\",\n        StreamFamily::Rest | StreamFamily::Xfer => \"endpoint\",\n        StreamFamily::ShellLink => \"owner\",\n        StreamFamily::WanMsg => \"target\",\n        _ => return None,\n    };\n    line.get(key)\n        .and_then(|v| v.as_str())\n        .filter(|s| !s.is_empty())\n        .map(str::to_string)\n}\n\n/// A worker's report: the outcome plus the telemetry keys it resolved\n/// (family + endpoint, once the opener classified) — Amendment fix 9.\nstruct WorkerReport {\n    outcome: DispatchOutcome,\n    family: Option<StreamFamily>,\n    endpoint: Option<String>,\n}\n\nimpl WorkerReport {\n    fn bare(outcome: DispatchOutcome) -> WorkerReport {\n        WorkerReport {\n            outcome,\n            family: None,\n            endpoint: None,\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn worker(\n…\n) -> WorkerReport {\n…\n}","startLine":429,"lineNumbers":[429,null,435,null,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,null,620,null,624,625,null,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,null,738,null,968]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784271660491}}
{"type":"message","id":"326e8fa3","parentId":"30581347","timestamp":"2026-07-17T07:01:00.493Z","message":{"role":"toolResult","toolCallId":"call_gJteT9LSKRplaXCgwytZcU9x|fc_0e15f89f5b7b4062016a59d32bc7208194a071f6055232d2b8","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## applyhost.rs\n 296|/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n*297|/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n 298|/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299|/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300|/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n## brainproc.rs\n 220|    // semantic the live-agent adapter inherits.\n*221|    write_ready(generation);\n 222|\n 223|    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n 224|    // online/offline exclusivity — a reconcile loop that runs each offline\n...\n 296|        let _ = brain.sessions();\n*297|        write_ready(generation);\n 298|    }\n 299|}\n 300|\n...\n 364|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*365|fn write_ready(generation: u64) {\n 366|    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n 367|    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n 368|    // tolerates its absence). Never fail the ready write over a diagnostic field.\n...\n 381|fn current_exe_hash() -> Option<String> {\n*382|    let exe = std::env::current_exe().ok()?;\n 383|    let bytes = std::fs::read(exe).ok()?;\n 384|    Some(crate::release::sha256_hex(&bytes))\n 385|}\n...\n 403|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n*404|pub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n 405|    serde_json::from_str::<serde_json::Value>(body)\n 406|        .ok()?\n 407|        .get(\"exe_hash\")?\n...\n 467|    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\n*468|    fn ready_exe_hash(&self) -> Option<String>;\n 469|    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\n 470|    /// the candidate SHOULD be running), or `None` if no staged metadata /\n 471|    /// version mismatch / platform absent — degrades the gate to readiness-only.\n...\n 499|    fn record_rolled_back(&self, _q: u64, _r: u64, _bin: &str) {}\n*500|    fn ready_exe_hash(&self) -> Option<String> {\n 501|        None\n 502|    }\n 503|    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n...\n 562|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n*563|    fn ready_exe_hash(&self) -> Option<String> {\n 564|        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n*565|        parse_ready_exe_hash(&body)\n 566|    }\n 567|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n 568|    fn staged_artifact_hash(&self, version: u64) -> Option<String> {\n...\n 980|fn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n*981|    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n 982|        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n 983|        (Some(_), Some(_)) => BytesGate::Mismatch,\n 984|        (expected, _) => {\n...\n 1008|/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n*1009|/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n 1010|/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n 1011|/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n*1012|/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n 1013|/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n 1014|/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n 1015|/// record-driven `.old-N` selection); else the `canonical` path captured at\n 1016|/// broker start; else — only if that capture failed — degrade to a per-spawn\n*1017|/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n*1018|/// when a canonical path is available, because on Linux `current_exe()` follows\n 1019|/// the `apply` rename to the old binary.\n 1020|// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n 1021|fn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n...\n 1025|            Some(c) => Ok(c.to_path_buf()),\n*1026|            None => std::env::current_exe(),\n 1027|        },\n 1028|    }\n 1029|}\n...\n 1102|    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n*1103|    // start, before any `apply` can rename it under us. Linux current_exe()\n 1104|    // (readlink /proc/self/exe) is inode-tracking and would later follow the\n 1105|    // apply-rename (spt → spt.old-N) to the OLD bytes; capturing at t=0 pins the\n 1106|    // canonical path string, matching Windows' path-at-start semantics. The one\n...\n 1109|    // bytes-gate (a wrong-bytes trial rolls back loudly, never promotes).\n*1110|    let canonical_exe = std::env::current_exe().ok();\n 1111|    let handle = thread::spawn(move || {\n 1112|        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n 1113|        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n...\n 1404|        assert_eq!(\n*1405|            parse_ready_exe_hash(\n 1406|                &serde_json::json!({\"pid\": 1, \"generation\": 3, \"exe_hash\": \"deadbeef\"})\n 1407|                    .to_string()\n 1408|            ),\n...\n 1412|        assert_eq!(\n*1413|            parse_ready_exe_hash(&serde_json::json!({\"pid\": 1, \"generation\": 3}).to_string()),\n 1414|            None,\n 1415|            \"a D6-era stamp without exe_hash reads None (additive / N-1-readable)\"\n 1416|        );\n*1417|        assert_eq!(parse_ready_exe_hash(\"{ not json\"), None, \"garbage → None\");\n 1418|    }\n 1419|\n 1420|    /// D7-1: the fixture-B swap mechanism — appending padding bytes to a binary\n## daemon.rs\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...\n 639|fn spawn_detached() -> io::Result<()> {\n*640|    let exe = std::env::current_exe()?;\n 641|    #[cfg(windows)]\n 642|    {\n 643|        let exe = exe.to_string_lossy();\n...\n 1560|\n*1561|        let launcher = Command::new(std::env::current_exe().unwrap())\n 1562|            // a unique substring filter selects ONLY this test in the re-run; the\n 1563|            // env presence routes it to the launcher arm above (no recursion).\n 1564|            .args([\"breakaway_spawn_escapes_a_kill_on_close_job\", \"--test-threads=1\"])\n## rollback_compat.rs\n 158|        assert_eq!(\n*159|            crate::brainproc::parse_ready_exe_hash(&with_extra.to_string()),\n 160|            Some(\"ab12\".to_string()),\n 161|            \"the exe_hash reader must tolerate a future additive field\"\n 162|        );\n...\n 165|        assert_eq!(\n*166|            crate::brainproc::parse_ready_exe_hash(\n 167|                &serde_json::json!({ \"pid\": 4321, \"generation\": 9 }).to_string()\n 168|            ),\n 169|            None,"}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":26,"fileCount":4,"files":["crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/rollback_compat.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":1},{"path":"crates/spt-daemon/src/brainproc.rs","count":20},{"path":"crates/spt-daemon/src/daemon.rs","count":3},{"path":"crates/spt-daemon/src/rollback_compat.rs","count":2}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## applyhost.rs\n 296│/// Apply the staged release against a RUNNING daemon (module docs). `exe_path` is\n*297│/// the executable to swap (production: `std::env::current_exe()`); `broker_name`\n 298│/// the canonical broker socket the brain handoff runs against. For a stopped box,\n 299│/// the caller uses [`apply_staged_daemonless`] instead — this path REQUIRES a live\n 300│/// broker to hand off to (a dead broker fails the apply with the seat untouched).\n## brainproc.rs\n  220│    // semantic the live-agent adapter inherits.\n *221│    write_ready(generation);\n  222│\n  223│    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n  224│    // online/offline exclusivity — a reconcile loop that runs each offline\n     │...\n  296│        let _ = brain.sessions();\n *297│        write_ready(generation);\n  298│    }\n  299│}\n  300│\n     │...\n  364│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n *365│fn write_ready(generation: u64) {\n  366│    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n  367│    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n  368│    // tolerates its absence). Never fail the ready write over a diagnostic field.\n     │...\n  381│fn current_exe_hash() -> Option<String> {\n *382│    let exe = std::env::current_exe().ok()?;\n  383│    let bytes = std::fs::read(exe).ok()?;\n  384│    Some(crate::release::sha256_hex(&bytes))\n  385│}\n     │...\n  403│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n *404│pub fn parse_ready_exe_hash(body: &str) -> Option<String> {\n  405│    serde_json::from_str::<serde_json::Value>(body)\n  406│        .ok()?\n  407│        .get(\"exe_hash\")?\n     │...\n  467│    /// bytes-gate to readiness-only (KH 6.11, N-1-safe).\n *468│    fn ready_exe_hash(&self) -> Option<String>;\n  469│    /// The staged artifact's SHA-256 for THIS platform at `version` (the bytes\n  470│    /// the candidate SHOULD be running), or `None` if no staged metadata /\n  471│    /// version mismatch / platform absent — degrades the gate to readiness-only.\n     │...\n  499│    fn record_rolled_back(&self, _q: u64, _r: u64, _bin: &str) {}\n *500│    fn ready_exe_hash(&self) -> Option<String> {\n  501│        None\n  502│    }\n  503│    fn staged_artifact_hash(&self, _version: u64) -> Option<String> {\n     │...\n  562│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n *563│    fn ready_exe_hash(&self) -> Option<String> {\n  564│        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n *565│        parse_ready_exe_hash(&body)\n  566│    }\n  567│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n  568│    fn staged_artifact_hash(&self, version: u64) -> Option<String> {\n     │...\n  980│fn bytes_gate(env: &dyn TrialEnv, version: u64) -> BytesGate {\n *981│    match (env.staged_artifact_hash(version), env.ready_exe_hash()) {\n  982│        (Some(expected), Some(actual)) if expected == actual => BytesGate::Match,\n  983│        (Some(_), Some(_)) => BytesGate::Mismatch,\n  984│        (expected, _) => {\n     │...\n 1008│/// (`spawn_brain_supervisor`). The `None` default uses it — **never** a\n*1009│/// per-spawn `std::env::current_exe()` — because on Linux `current_exe()`\n 1010│/// (`readlink /proc/self/exe`) is inode-tracking and follows the `apply` rename\n 1011│/// (`spt` → `spt.old-N`), which would respawn the brain onto the OLD bytes\n*1012│/// (KH 6.11). Falls back to `current_exe()` only if the t=0 capture failed\n 1013│/// (degrade-safe — restores pre-fix behavior rather than failing to spawn).\n 1014│/// Resolve which binary the brain spawns (KH 6.11). `Some(rollback)` wins (the\n 1015│/// record-driven `.old-N` selection); else the `canonical` path captured at\n 1016│/// broker start; else — only if that capture failed — degrade to a per-spawn\n*1017│/// `current_exe()`. The point of the fix: NEVER a per-spawn `current_exe()`\n*1018│/// when a canonical path is available, because on Linux `current_exe()` follows\n 1019│/// the `apply` rename to the old binary.\n 1020│// [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n 1021│fn select_brain_exe(binary: Option<&Path>, canonical: Option<&Path>) -> io::Result<PathBuf> {\n     │...\n 1025│            Some(c) => Ok(c.to_path_buf()),\n*1026│            None => std::env::current_exe(),\n 1027│        },\n 1028│    }\n 1029│}\n     │...\n 1102│    // KH 6.11: capture the canonical brain binary path ONCE here, at broker\n*1103│    // start, before any `apply` can rename it under us. Linux current_exe()\n 1104│    // (readlink /proc/self/exe) is inode-tracking and would later follow the\n 1105│    // apply-rename (spt → spt.old-N) to the OLD bytes; capturing at t=0 pins the\n 1106│    // canonical path string, matching Windows' path-at-start semantics. The one\n     │...\n 1109│    // bytes-gate (a wrong-bytes trial rolls back loudly, never promotes).\n*1110│    let canonical_exe = std::env::current_exe().ok();\n 1111│    let handle = thread::spawn(move || {\n 1112│        // The DRAINED gate (REQ-UPDATE-PROMOTE-DRAINED) reads broker truth in-process;\n 1113│        // a net-less / bind-failed daemon has no broker (None) → the gate degrades to\n     │...\n 1404│        assert_eq!(\n*1405│            parse_ready_exe_hash(\n 1406│                &serde_json::json!({\"pid\": 1, \"generation\": 3, \"exe_hash\": \"deadbeef\"})\n 1407│                    .to_string()\n 1408│            ),\n     │...\n 1412│        assert_eq!(\n*1413│            parse_ready_exe_hash(&serde_json::json!({\"pid\": 1, \"generation\": 3}).to_string()),\n 1414│            None,\n 1415│            \"a D6-era stamp without exe_hash reads None (additive / N-1-readable)\"\n 1416│        );\n*1417│        assert_eq!(parse_ready_exe_hash(\"{ not json\"), None, \"garbage → None\");\n 1418│    }\n 1419│\n 1420│    /// D7-1: the fixture-B swap mechanism — appending padding bytes to a binary\n## daemon.rs\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     │...\n  639│fn spawn_detached() -> io::Result<()> {\n *640│    let exe = std::env::current_exe()?;\n  641│    #[cfg(windows)]\n  642│    {\n  643│        let exe = exe.to_string_lossy();\n     │...\n 1560│\n*1561│        let launcher = Command::new(std::env::current_exe().unwrap())\n 1562│            // a unique substring filter selects ONLY this test in the re-run; the\n 1563│            // env presence routes it to the launcher arm above (no recursion).\n 1564│            .args([\"breakaway_spawn_escapes_a_kill_on_close_job\", \"--test-threads=1\"])\n## rollback_compat.rs\n 158│        assert_eq!(\n*159│            crate::brainproc::parse_ready_exe_hash(&with_extra.to_string()),\n 160│            Some(\"ab12\".to_string()),\n 161│            \"the exe_hash reader must tolerate a future additive field\"\n 162│        );\n    │...\n 165│        assert_eq!(\n*166│            crate::brainproc::parse_ready_exe_hash(\n 167│                &serde_json::json!({ \"pid\": 4321, \"generation\": 9 }).to_string()\n 168│            ),\n 169│            None,"},"isError":false,"timestamp":1784271660493}}
{"type":"message","id":"3a08430d","parentId":"326e8fa3","timestamp":"2026-07-17T07:01:00.511Z","message":{"role":"toolResult","toolCallId":"call_i6v52LO6EgpUb3sQTT5CSSuB|fc_0e15f89f5b7b4062016a59d32bc70c8194beb34cd8ae68560c","toolName":"read","content":[{"type":"text","text":"    pub fn inject_endpoint(\n…\n    ) -> io::Result<(bool, bool)> {\n…\n        loop {\n            match self.read_event()? {\n…\n            }\n        }\n    }\n\n    /// List the broker's hosted sessions (id + owning endpoint) — the inbound\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n    /// discipline on a QUIC stream, D4b).\n    // [impl->REQ-DAEMON-2]\n    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n        self.net_cursors.insert(stream_id, from_seq);\n        self.send(\n            KIND_NET_STREAM_SUBSCRIBE,\n            serde_json::to_value(NetStreamSubscribeReq {\n                stream_id,\n                from_seq,\n            })\n            .expect(\"NetStreamSubscribeReq serializes\"),\n        )\n    }\n\n    /// The next stream-read sequence still needed for `stream_id` (the durable\n    /// state a handed-off brain carries to resubscribe gaplessly).\n    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n        self.net_cursors.get(&stream_id).copied()\n    }\n\n    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n    /// events produced while no brain was attached replay from the cursor.\n    // [impl->REQ-EP-4]\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n        self.presence_cursor = Some(from_seq);\n…\n    }\n\n[Showing lines 1569-1613 of 2247. Use :1614 to continue]"}],"details":{"truncation":{"content":"            }\n        }\n    }\n\n    /// List the broker's hosted sessions (id + owning endpoint) — the inbound\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n    /// discipline on a QUIC stream, D4b).\n    // [impl->REQ-DAEMON-2]\n    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n        self.net_cursors.insert(stream_id, from_seq);\n        self.send(\n            KIND_NET_STREAM_SUBSCRIBE,\n            serde_json::to_value(NetStreamSubscribeReq {\n                stream_id,\n                from_seq,\n            })\n            .expect(\"NetStreamSubscribeReq serializes\"),\n        )\n    }\n\n    /// The next stream-read sequence still needed for `stream_id` (the durable\n    /// state a handed-off brain carries to resubscribe gaplessly).\n    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n        self.net_cursors.get(&stream_id).copied()\n    }\n\n    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n    /// events produced while no brain was attached replay from the cursor.\n    // [impl->REQ-EP-4]\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n        self.presence_cursor = Some(from_seq);","truncated":true,"truncatedBy":"lines","totalLines":679,"totalBytes":1843,"outputLines":45,"outputBytes":1843,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn inject_endpoint(\n…\n    ) -> io::Result<(bool, bool)> {\n…\n        loop {\n            match self.read_event()? {\n…\n            }\n        }\n    }\n\n    /// List the broker's hosted sessions (id + owning endpoint) — the inbound\n    /// dispatcher's session→endpoint resolution (D9-1).\n    pub fn sessions(&mut self) -> io::Result<SessionsReply> {\n        self.send(KIND_SESSIONS, serde_json::Value::Null)?;\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Sessions(ev) => return Ok(ev),\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n    /// discipline on a QUIC stream, D4b).\n    // [impl->REQ-DAEMON-2]\n    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n        self.net_cursors.insert(stream_id, from_seq);\n        self.send(\n            KIND_NET_STREAM_SUBSCRIBE,\n            serde_json::to_value(NetStreamSubscribeReq {\n                stream_id,\n                from_seq,\n            })\n            .expect(\"NetStreamSubscribeReq serializes\"),\n        )\n    }\n\n    /// The next stream-read sequence still needed for `stream_id` (the durable\n    /// state a handed-off brain carries to resubscribe gaplessly).\n    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {\n        self.net_cursors.get(&stream_id).copied()\n    }\n\n    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,\n    /// setting the local cursor (D4c) — gapless, exactly-once presence resume:\n    /// events produced while no brain was attached replay from the cursor.\n    // [impl->REQ-EP-4]\n    pub fn net_presence_subscribe(&mut self, from_seq: u64) -> io::Result<()> {\n        self.presence_cursor = Some(from_seq);\n…\n    }","startLine":1545,"lineNumbers":[1545,null,1550,null,1560,1561,null,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,null,1619]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2247,"totalBytes":1843,"outputLines":45,"outputBytes":1843,"shownRange":{"start":1569,"end":1613},"nextOffset":1614}}},"isError":false,"timestamp":1784271660511}}
{"type":"message","id":"a8e595c3","parentId":"3a08430d","timestamp":"2026-07-17T07:01:00.530Z","message":{"role":"toolResult","toolCallId":"call_e980i9JPS6i6sSK6cSlhYSiR|fc_0e15f89f5b7b4062016a59d32bc700819486c9dfd282d4a124","toolName":"read","content":[{"type":"text","text":"    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n…\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.honored);\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n    /// broker resolves endpoint→session atomically and writes the bytes (a\n    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n    /// = caller should spool deferred (active window), `false` = spool non-deferred\n    /// (idle window). When `delivered=true` the spool_deferred hint is false (no spool).\n    /// `native` (ADR-0028 W3): a `--prefer/force-native` send → the broker injects\n    /// via the translation binary regardless of the `.idle` activity gate.\n    // [impl->REQ-SEND-SPT-HOSTED]\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    pub fn inject_endpoint(\n        &mut self,\n        endpoint: &str,\n        bytes: &[u8],\n        native: bool,\n    ) -> io::Result<(bool, bool)> {\n        self.send(\n            KIND_ENDPOINT_INPUT,\n            serde_json::to_value(EndpointInputReq {\n…\n            })\n            .expect(\"EndpointInputReq serializes\"),\n        )?;\n…\n    }\n\n[Showing lines 1524-1553 of 2247. Use :1554 to continue]"}],"details":{"truncation":{"content":"                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.honored);\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n    /// broker resolves endpoint→session atomically and writes the bytes (a\n    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n    /// = caller should spool deferred (active window), `false` = spool non-deferred\n    /// (idle window). When `delivered=true` the spool_deferred hint is false (no spool).\n    /// `native` (ADR-0028 W3): a `--prefer/force-native` send → the broker injects\n    /// via the translation binary regardless of the `.idle` activity gate.\n    // [impl->REQ-SEND-SPT-HOSTED]\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    pub fn inject_endpoint(\n        &mut self,\n        endpoint: &str,\n        bytes: &[u8],\n        native: bool,\n    ) -> io::Result<(bool, bool)> {\n        self.send(\n            KIND_ENDPOINT_INPUT,\n            serde_json::to_value(EndpointInputReq {","truncated":true,"truncatedBy":"lines","totalLines":724,"totalBytes":1457,"outputLines":30,"outputBytes":1457,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn request_brain_restart(&mut self) -> io::Result<bool> {\n…\n        loop {\n            match self.read_event()? {\n                BrokerEvent::Other(env) if env.kind == KIND_BRAIN_RESTARTED => {\n                    let reply: BrainRestarted = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(reply.honored);\n                }\n                BrokerEvent::Error { message } => return Err(io::Error::other(message)),\n                _ => continue,\n            }\n        }\n    }\n\n    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n    /// broker resolves endpoint→session atomically and writes the bytes (a\n    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n    /// = caller should spool deferred (active window), `false` = spool non-deferred\n    /// (idle window). When `delivered=true` the spool_deferred hint is false (no spool).\n    /// `native` (ADR-0028 W3): a `--prefer/force-native` send → the broker injects\n    /// via the translation binary regardless of the `.idle` activity gate.\n    // [impl->REQ-SEND-SPT-HOSTED]\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    pub fn inject_endpoint(\n        &mut self,\n        endpoint: &str,\n        bytes: &[u8],\n        native: bool,\n    ) -> io::Result<(bool, bool)> {\n        self.send(\n            KIND_ENDPOINT_INPUT,\n            serde_json::to_value(EndpointInputReq {\n…\n            })\n            .expect(\"EndpointInputReq serializes\"),\n        )?;\n…\n    }","startLine":1518,"lineNumbers":[1518,null,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,null,1557,1558,1559,null,1571]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2247,"totalBytes":1457,"outputLines":30,"outputBytes":1457,"shownRange":{"start":1524,"end":1553},"nextOffset":1554}}},"isError":false,"timestamp":1784271660530}}
{"type":"message","id":"e823786f","parentId":"a8e595c3","timestamp":"2026-07-17T07:01:02.834Z","message":{"role":"toolResult","toolCallId":"call_nYyxokunHrwB5yhXoxjFMsOG|fc_0e15f89f5b7b4062016a59d32bc6b881949b4b1d087d07cdf4","toolName":"glob","content":[{"type":"text","text":"# target/debug/\nspt.exe\n## deps/\nspt.exe\n# target-seam/debug/\nspt.exe\n## deps/\nspt.exe"}],"details":{"scopePath":".","fileCount":4,"files":["target/debug/deps/spt.exe","target/debug/spt.exe","target-seam/debug/deps/spt.exe","target-seam/debug/spt.exe"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784271662834}}
{"type":"message","id":"b91ea533","parentId":"e823786f","timestamp":"2026-07-17T07:01:17.187Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning detailed loop cadence analysis**"},{"type":"toolCall","id":"call_KuwhqY7eqmZ9ovWBIzxSwag0|fc_0e15f89f5b7b4062016a59d33c5ee48194ad6588e3f91ba401","name":"grep","arguments":{"i":"Locating index cadence","pattern":"const .*INTERVAL|from_millis|fn spawn_index_writer|fn run|while !stop|thread::sleep|drain|refresh","path":"crates/spt-daemon/src/projwriter.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_A3yXDIsZ9Fsikaa5BJqZNQtd|fc_0e15f89f5b7b4062016a59d33c5f008194856eabd33081b037","name":"read","arguments":{"i":"Reading index writer loop","path":"crates/spt-daemon/src/projwriter.rs","selector":"1-180,670-790"}},{"type":"toolCall","id":"call_cnXtC9QA2pjm4CyyAmJkkvJJ|fc_0e15f89f5b7b4062016a59d33c5f1481948c31dd57a6b21113","name":"grep","arguments":{"i":"Locating dispatch cadence","pattern":"FULL_DISPATCH_POLL|spawn_dispatcher|run_dispatch_loop\\(","path":"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/brainproc.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_FFqRHBchizlQ2U1gppBYcweu|fc_0e15f89f5b7b4062016a59d33c5f24819480aef2273d69588d","name":"grep","arguments":{"i":"Locating live cadence","pattern":"LIVE_RECONCILE_INTERVAL_MS|run_pulse_loop|pulse_period|thread::sleep|while !stop","path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-daemon/src/brainproc.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_DqQhMveK1ryiwswozk1B1KeK|fc_0e15f89f5b7b4062016a59d33c5f308194a7fd100a3a904d47","name":"grep","arguments":{"i":"Locating pump cadences","pattern":"PumpCadence|cadence:|from_secs|from_millis|thread::sleep|PUMP_|poll","path":"crates/spt-daemon/src/pump","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Ui0aB2pfB0s7ejmnY62MjCKH|fc_0e15f89f5b7b4062016a59d33c5f3c8194bf25baecb7c8c82d","name":"grep","arguments":{"i":"Finding large recurring reads","pattern":"std::fs::read\\(|std::fs::read_to_string\\(|read_dir\\(|read_all\\(|fetch_history\\(|branches_by_recency\\(|ls_files_at\\(","path":"crates/spt-daemon/src;crates/spt-live/src;crates/spt-store/src","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":12196,"output":598,"cacheRead":51712,"cacheWrite":0,"totalTokens":64506,"reasoningTokens":112,"cost":{"input":0.060980000000000006,"output":0.01794,"cacheRead":0.025856,"cacheWrite":0,"total":0.10477600000000001}},"stopReason":"toolUse","timestamp":1784271662843,"responseId":"resp_0e15f89f5b7b4062016a59d32f109081949e899c001248e7a3","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d33180dc8194aa5bf19fb75f82c0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdM8ld9UR2x6Ir-XrOppITlLPiA6Qn_hcNC3cK3cZfgqc9BH94u9XG4gfdWZ77G1XP5FJEA9pGQ9gyaVsoSdF-dWTLXgWFvquj63AKy-yQMP790BMcBAmp7mIfHtb5fL_K_iXmw-fNnPboPa1T4k245dQ2oRe9w9HbC7IQng66gXNcX9-y_fY2WqOM3Sds1sM7XN3nguDk_lY5kMhv-DU0DWH6NIa4nZ8cioyanRHgN7O8Lc2Nm2m506EaNUhndRPerwv8SxTuZSuH_n9AgX0xhad6b78YpXH1xDipTCzy73Qguz1W4fKtTEoeGtC3xJADmcVzAhW-kqDMSp8JPEGzTpSVtsfwfJammsRIwE9pFaG_LW8ngkwsXl4E0HgRfQ0cz2vCPYCwhiDDMRiJEjBua19fADykX8WVOX8oEyqr6_N0-1vw3vx008jcGFkS_OL3GRU0c2dKZCRVfIVgJCn_MoAaOmjUX5nSMUK61l-uQuVUT5VzWdWbTELvcLGh9ORI9GPBDBvlnFDCqm7sLF7LeJEZgrWrVKjFlHMqlgxm-ih8H045YZslubSOzJgN8cp9aj1u-7GoRd-dlttiBgY5_91aQe8FL3gGYyFx47npf-alSfXbeMupJEyxDdsX5pGrmyinc_yQB96tSH8MHi3m4h17QRRm6A2zY2_KJ1yf4v_JMkM9Shp70VoFoU9lYXpG9exgtbOUTJINGIF8TaOG7eu6C7TU3E6Q4HCxibUFoBzx8GIrvXxpCm_jkyvua_YZYrrrDXjxCQwFPtHIqkd2lGQF5YB9LbdCei-85r8vzka1uEYfVn8fBCr6THetTS2bBJDLC3m1n6I9TU6zWwVz8DAGiqQafVGypekWVJfDWRKJXRBZJeIEht7ia48tHoJrPZZt0ouhcpiop7UyjrvOGhbXlnnPHpGdbVOGfebkSZ_ho_iMtP2tpGvWQRb0gJu-mfFjsY3_AgnJgZPRKAz2pqlzAiAVfEkM1TD4xsXW_RC_-Ild6C1cdvALPbEdPBBHoloChJcX_r8vIqhIptUebDt_MRdRSy2hGgaonUbT5_EbYmCubwb_MgNbhtNsOfaBAMvXpoDXoN6Xt6TuweLQNs6lGOmV9p-qFTAeoKLNmLS5A4GdGCSiyf6YoO3UPZRG6SFedNyQKJz8RaKW9XwYoIKWFN2fLmDdNNVrHl52iUZn-szqp3q6gWTEA1UovOceQe4digw4snzm3PdLuiTNWAT7NlXg1kSn5xn7MhBg4hYIHQZWE8Fa9CpaufK4hPTVaHxQDV2AGZGQaZ6_idOtdwRsiNQ3uWjFWVroUMXoxAgUbgTI-eseEnshBCjGnECH9RmSwBJW_QGZklTcOVM7dUAjdt_0ydqoqf3tNZQxD-9c25DXp-GjNTwNlE4yc3-osPWXLsRjbg_Ow1Rs69O8PBZVlyRz-Vaq1PJJfgQaz1eHPR5K2LkkrUJo0P2-Thcn17ROMJrdf15CP-TfUY0rRmXXdKa3q429ocXHN83GJIJbXqo4bplLh4lIdtwR_xoVDFiElVXRGy6IQOQot8Oy36YVz6hP1YtwOmLlN6qOzg55gxT7kKBeaA5c1DLFI7j5_FOK6oGwOGqdzLCs_3hC7meGb3D4SuYjqmHdqv2rQT-gTckgNBzaidnx8fTwmKDdLV","summary":[{"type":"summary_text","text":"**Planning detailed loop cadence analysis**"}]},{"id":"fc_0e15f89f5b7b4062016a59d33c5ee48194ad6588e3f91ba401","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating index cadence\",\"pattern\":\"const .*INTERVAL|from_millis|fn spawn_index_writer|fn run|while !stop|thread::sleep|drain|refresh\",\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_KuwhqY7eqmZ9ovWBIzxSwag0","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d33c5f008194856eabd33081b037","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading index writer loop\",\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"selector\":\"1-180,670-790\"}","call_id":"call_A3yXDIsZ9Fsikaa5BJqZNQtd","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d33c5f1481948c31dd57a6b21113","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating dispatch cadence\",\"pattern\":\"FULL_DISPATCH_POLL|spawn_dispatcher|run_dispatch_loop\\\\(\",\"path\":\"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/brainproc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_cnXtC9QA2pjm4CyyAmJkkvJJ","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d33c5f24819480aef2273d69588d","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating live cadence\",\"pattern\":\"LIVE_RECONCILE_INTERVAL_MS|run_pulse_loop|pulse_period|thread::sleep|while !stop\",\"path\":\"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-daemon/src/brainproc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_FFqRHBchizlQ2U1gppBYcweu","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d33c5f308194a7fd100a3a904d47","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating pump cadences\",\"pattern\":\"PumpCadence|cadence:|from_secs|from_millis|thread::sleep|PUMP_|poll\",\"path\":\"crates/spt-daemon/src/pump\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_DqQhMveK1ryiwswozk1B1KeK","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d33c5f3c8194bf25baecb7c8c82d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding large recurring reads\",\"pattern\":\"std::fs::read\\\\(|std::fs::read_to_string\\\\(|read_dir\\\\(|read_all\\\\(|fetch_history\\\\(|branches_by_recency\\\\(|ls_files_at\\\\(\",\"path\":\"crates/spt-daemon/src;crates/spt-live/src;crates/spt-store/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Ui0aB2pfB0s7ejmnY62MjCKH","name":"grep"}]},"duration":14267.161600008607,"ttft":2675.609200000763,"contextSnapshot":{"promptTokens":63908,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KuwhqY7eqmZ9ovWBIzxSwag0|fc_0e15f89f5b7b4062016a59d33c5ee48194ad6588e3f91ba401","toolName":"grep","startedAt":"2026-07-17T07:01:17.188Z","args":{"path":"crates/spt-daemon/src/projwriter.rs"},"intent":"Locating index cadence"},"id":"502c7620","parentId":"b91ea533","timestamp":"2026-07-17T07:01:17.188Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_A3yXDIsZ9Fsikaa5BJqZNQtd|fc_0e15f89f5b7b4062016a59d33c5f008194856eabd33081b037","toolName":"read","startedAt":"2026-07-17T07:01:17.188Z","args":{"path":"crates/spt-daemon/src/projwriter.rs"},"intent":"Reading index writer loop"},"id":"2f04bd56","parentId":"502c7620","timestamp":"2026-07-17T07:01:17.188Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cnXtC9QA2pjm4CyyAmJkkvJJ|fc_0e15f89f5b7b4062016a59d33c5f1481948c31dd57a6b21113","toolName":"grep","startedAt":"2026-07-17T07:01:17.188Z","args":{"path":"crates/spt-daemon/src/dispatch.rs;crates/spt-daemon/src/brainproc.rs"},"intent":"Locating dispatch cadence"},"id":"4c0c350d","parentId":"2f04bd56","timestamp":"2026-07-17T07:01:17.188Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FFqRHBchizlQ2U1gppBYcweu|fc_0e15f89f5b7b4062016a59d33c5f24819480aef2273d69588d","toolName":"grep","startedAt":"2026-07-17T07:01:17.188Z","args":{"path":"crates/spt-daemon/src/livehost.rs;crates/spt-daemon/src/lifecycle.rs;crates/spt-daemon/src/brainproc.rs"},"intent":"Locating live cadence"},"id":"c6a73e73","parentId":"4c0c350d","timestamp":"2026-07-17T07:01:17.188Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DqQhMveK1ryiwswozk1B1KeK|fc_0e15f89f5b7b4062016a59d33c5f308194a7fd100a3a904d47","toolName":"grep","startedAt":"2026-07-17T07:01:17.188Z","args":{"path":"crates/spt-daemon/src/pump"},"intent":"Locating pump cadences"},"id":"347a8544","parentId":"c6a73e73","timestamp":"2026-07-17T07:01:17.188Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Ui0aB2pfB0s7ejmnY62MjCKH|fc_0e15f89f5b7b4062016a59d33c5f3c8194bf25baecb7c8c82d","toolName":"grep","startedAt":"2026-07-17T07:01:17.188Z","args":{"path":"crates/spt-daemon/src;crates/spt-live/src;crates/spt-store/src"},"intent":"Finding large recurring reads"},"id":"110fd0d4","parentId":"347a8544","timestamp":"2026-07-17T07:01:17.188Z"}
{"type":"message","id":"1cfd051a","parentId":"110fd0d4","timestamp":"2026-07-17T07:01:17.282Z","message":{"role":"toolResult","toolCallId":"call_DqQhMveK1ryiwswozk1B1KeK|fc_0e15f89f5b7b4062016a59d33c5f308194a7fd100a3a904d47","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/pump/\n## mod.rs\n 96|/// The pump's tick granularity — each cadence fires when its period elapsed.\n*97|const TICK: Duration = Duration::from_millis(200);\n 98|\n 99|/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall\n 100|/// warning reads 3-cadence staleness, so a few seconds of heartbeat\n 101|/// granularity is invisible to it — and 5 writes/sec of disk churn is not.\n*102|const HEARTBEAT_PERIOD: Duration = Duration::from_secs(5);\n 103|\n 104|/// Per-call deadline budget for the pump's brain-IPC reads (REQ-HAZARD-PUMP-\n 105|/// IPC-DEADLINE). Far above any legitimate stream-open / sync round-trip, below\n...\n 108|/// escalated to a supervised restart, NEVER a per-peer retry.\n*109|const PUMP_PEER_IO_TIMEOUT: Duration = Duration::from_secs(30);\n 110|\n 111|/// Supervisor backoff floor: the first restart after a pump death.\n*112|const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(5);\n 113|\n 114|/// Supervisor backoff cap (M8 decision 23): a persistently-crashing pump\n 115|/// retries at most every 5 minutes — visible, bounded, never silent.\n*116|const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(300);\n 117|\n 118|/// A run that survived this long before dying counts as healthy — the next\n 119|/// backoff resets to the floor instead of doubling (a once-a-day panic must\n 120|/// not ratchet the pump toward permanent 5-minute outages).\n*121|const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(60);\n 122|\n 123|/// Per-peer dial-backoff floor (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): the\n 124|/// first failed dial to a peer waits this long before the pump re-dials THAT\n 125|/// peer. Scoped to the peer — a dead peer backs off while live peers stay hot.\n*126|const PEER_BACKOFF_BASE: Duration = Duration::from_secs(5);\n 127|\n 128|/// Per-peer dial-backoff cap (G5): a persistently-dead peer is re-dialed at most\n 129|/// every 5 minutes (same bound as the supervisor) — bounded, never a hot redial\n 130|/// loop, never silent. Reset to the floor on any successful connect.\n*131|const PEER_BACKOFF_CAP: Duration = Duration::from_secs(300);\n 132|\n 133|/// Round-drain margin over W1's per-dial reply-read budget (G3, Q1): because the\n 134|/// dials are CONCURRENT, every outcome lands within ~one W1 bound, so the round\n...\n 136|/// [`PEER_REPLY_READ_BUDGET`] (not a hardcoded 12s) so it tracks any W1 retune.\n*137|const ROUND_DRAIN_MARGIN: Duration = Duration::from_secs(2);\n 138|\n 139|/// Per-peer dial schedule (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): a peer is\n 140|/// dial-eligible when `now >= next_due`. A `DIAL_FAILED` (or a mid-round\n...\n 294|#[derive(Debug, Clone, Copy)]\n*295|pub struct PumpCadence {\n 296|    pub registry: Duration,\n 297|    pub notif: Duration,\n 298|    pub sync: Duration,\n...\n 304|\n*305|impl PumpCadence {\n 306|    /// From the daemon config knobs.\n*307|    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {\n*308|        PumpCadence {\n 309|            registry: cfg.registry_pump_period,\n 310|            notif: cfg.notif_pump_period,\n 311|            sync: cfg.sync_pull_period,\n...\n 320|pub struct PumpConfig {\n*321|    pub cadence: PumpCadence,\n 322|    /// `true` = the explicit opt-in to prompt-free updates; default `false`\n 323|    /// surfaces every staged release as a consent notif.\n 324|    pub full_auto_update: bool,\n...\n 330|        PumpConfig {\n*331|            cadence: PumpCadence::from_config(cfg),\n 332|            full_auto_update: cfg.full_auto_update,\n 333|        }\n 334|    }\n...\n 378|    /// wake claims exactly one forced round. Default: no wake edge.\n*379|    fn poll_wake(&mut self) -> bool {\n 380|        false\n 381|    }\n 382|\n...\n 513|/// The pure scheduler + fan-out (pump-seam P4, all four legs behind the seam):\n*514|/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\n 515|/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\n 516|/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\n 517|/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\n...\n 532|    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\n*533|    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n 534|    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n 535|    // hfenduleam stall). A timed-out read escalates to a supervised restart\n 536|    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n...\n 611|        if !any_due {\n*612|            thread::sleep(TICK);\n 613|            continue;\n 614|        }\n 615|\n...\n 663|                            eprintln!(\n*664|                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\n 665|                                now_ms(),\n 666|                                mono_ms()\n 667|                            );\n## notif.rs\n 15|\n*16|use super::{PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n 17|\n 18|/// The notif leg as a [`PumpWorker`]: the full semilattice spool per subnet\n 19|/// ([`emit_notif_feed`]); the receiver's join dedups.\n...\n 23|    /// This worker's scheduling period (`cadence.notif`).\n*24|    cadence: Duration,\n 25|}\n 26|\n 27|impl NotifWorker {\n 28|    /// Construction capture (§Frozen-4): the notif spool DB path + the cadence.\n*29|    pub(super) fn new(paths: &PumpPaths, cadence: &PumpCadence) -> NotifWorker {\n 30|        NotifWorker {\n 31|            notif_db: paths.notif_db.clone(),\n*32|            cadence: cadence.notif,\n 33|        }\n 34|    }\n 35|}\n## registry.rs\n 3|//!\n*4|//! `poll_wake` consumes the event-driven advertisement marker (force a round\n 5|//! NOW on an endpoint online/offline transition); `pre_round` runs the\n 6|//! node-local sweep (repair-evictions → silent-peer eviction → due seed\n 7|//! rotations → re-advertise) ONCE per due round and holds the produced adverts\n...\n 22|\n*23|use super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n 24|\n 25|/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,\n 26|/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the\n...\n 76|    rotations: PathBuf,\n*77|    /// The event-driven advertisement marker (`poll_wake`).\n 78|    advertise_marker: PathBuf,\n 79|    /// This worker's scheduling period (`cadence.registry`).\n*80|    cadence: Duration,\n 81|    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10).\n 82|    evict_after: Duration,\n 83|    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\n...\n 92|        paths: &PumpPaths,\n*93|        cadence: &PumpCadence,\n 94|    ) -> RegistryWorker {\n 95|        RegistryWorker {\n 96|            registry,\n...\n 100|            advertise_marker: paths.advertise_marker.clone(),\n*101|            cadence: cadence.registry,\n 102|            evict_after: cadence.registry_evict_after,\n 103|            adverts: Vec::new(),\n 104|        }\n...\n 119|    // [impl->REQ-CONV-2]\n*120|    fn poll_wake(&mut self) -> bool {\n 121|        crate::registryhost::take_advertise_now(&self.advertise_marker)\n 122|    }\n 123|\n...\n 223|\n*224|    // [unit->REQ-CONV-2] the worker's `poll_wake` consumes the advertise-now\n 225|    // marker exactly once — one online/offline transition forces exactly one\n 226|    // registry round, and an absent marker forces nothing.\n 227|    #[test]\n*228|    fn poll_wake_takes_the_advertise_marker_exactly_once() {\n 229|        let dir = tempfile::tempdir().unwrap();\n 230|        let marker = dir.path().join(\"advertise-now\");\n 231|        let mut w = RegistryWorker {\n...\n 240|            advertise_marker: marker.clone(),\n*241|            cadence: Duration::from_secs(30),\n*242|            evict_after: Duration::from_secs(600),\n 243|            adverts: Vec::new(),\n 244|        };\n*245|        assert!(!w.poll_wake(), \"nothing to take yet\");\n 246|        std::fs::write(&marker, b\"\").unwrap();\n*247|        assert!(w.poll_wake(), \"first take claims the transition\");\n*248|        assert!(!w.poll_wake(), \"second take gets nothing\");\n 249|    }\n 250|\n 251|    // [unit->REQ-MESH-4] `pre_round` runs the full node-local sweep once per due\n...\n 299|                advertise_marker: home.join(\"advertise-now\"),\n*300|                cadence: Duration::from_secs(30),\n*301|                evict_after: Duration::from_secs(600),\n 302|                adverts: Vec::new(),\n 303|            };\n 304|\n## sync.rs\n 2|//!\n*3|//! `poll_wake` consumes the wake-edge freshness-pull marker (a dormant→active\n 4|//! transition forces a pull NOW); `peer_step` builds the want-set — the union\n 5|//! of locally-tracked allowed branches ([`select_refs`]) and registry-derived\n 6|//! `a-<id>` bootstrap refs (endpoints known in the shared subnet this node\n...\n 23|\n*24|use super::{PeerIo, PumpCadence, PumpHooks, PumpPaths, PumpWorker, RoundCtx};\n 25|\n 26|/// The sync leg as a [`PumpWorker`]: anti-entropy pull for the minds.\n 27|pub(super) struct SyncWorker {\n...\n 38|    /// This worker's scheduling period (`cadence.sync`).\n*39|    cadence: Duration,\n 40|    /// The conflict-surface hooks (ADR-0013): `on_pull` fires after every\n 41|    /// completed pull. A sync-worker construction dep (the conflict surface).\n 42|    hooks: PumpHooks,\n...\n 50|        paths: &PumpPaths,\n*51|        cadence: &PumpCadence,\n 52|        hooks: PumpHooks,\n 53|    ) -> SyncWorker {\n 54|        SyncWorker {\n...\n 58|            pull_marker: paths.pull_marker.clone(),\n*59|            cadence: cadence.sync,\n 60|            hooks,\n 61|        }\n 62|    }\n...\n 74|    // [impl->REQ-INST-3]\n*75|    fn poll_wake(&mut self) -> bool {\n 76|        crate::resting::take_freshness_pull(&self.pull_marker)\n 77|    }\n 78|\n## update.rs\n 28|\n*29|use super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n 30|\n 31|/// The update leg as a [`PumpWorker`]: the peer-propagation pull (REQ-UPD-1),\n 32|/// consent-gated by default (REQ-UPD-4), with typed convergence outcomes\n...\n 53|    /// This worker's scheduling period (`cadence.update`).\n*54|    cadence: Duration,\n 55|}\n 56|\n 57|impl UpdateWorker {\n...\n 62|        paths: &PumpPaths,\n*63|        cadence: &PumpCadence,\n 64|        full_auto_update: bool,\n 65|    ) -> UpdateWorker {\n 66|        UpdateWorker {\n...\n 74|            full_auto_update,\n*75|            cadence: cadence.update,\n 76|        }\n 77|    }\n 78|}"}],"details":{"scopePath":"crates/spt-daemon/src/pump","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":50,"fileCount":5,"files":["crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/pump/notif.rs","crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/pump/sync.rs","crates/spt-daemon/src/pump/update.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/pump/mod.rs","count":20},{"path":"crates/spt-daemon/src/pump/notif.rs","count":4},{"path":"crates/spt-daemon/src/pump/registry.rs","count":16},{"path":"crates/spt-daemon/src/pump/sync.rs","count":6},{"path":"crates/spt-daemon/src/pump/update.rs","count":4}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/pump/\n## mod.rs\n  96│/// The pump's tick granularity — each cadence fires when its period elapsed.\n *97│const TICK: Duration = Duration::from_millis(200);\n  98│\n  99│/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall\n 100│/// warning reads 3-cadence staleness, so a few seconds of heartbeat\n 101│/// granularity is invisible to it — and 5 writes/sec of disk churn is not.\n*102│const HEARTBEAT_PERIOD: Duration = Duration::from_secs(5);\n 103│\n 104│/// Per-call deadline budget for the pump's brain-IPC reads (REQ-HAZARD-PUMP-\n 105│/// IPC-DEADLINE). Far above any legitimate stream-open / sync round-trip, below\n    │...\n 108│/// escalated to a supervised restart, NEVER a per-peer retry.\n*109│const PUMP_PEER_IO_TIMEOUT: Duration = Duration::from_secs(30);\n 110│\n 111│/// Supervisor backoff floor: the first restart after a pump death.\n*112│const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(5);\n 113│\n 114│/// Supervisor backoff cap (M8 decision 23): a persistently-crashing pump\n 115│/// retries at most every 5 minutes — visible, bounded, never silent.\n*116│const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(300);\n 117│\n 118│/// A run that survived this long before dying counts as healthy — the next\n 119│/// backoff resets to the floor instead of doubling (a once-a-day panic must\n 120│/// not ratchet the pump toward permanent 5-minute outages).\n*121│const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(60);\n 122│\n 123│/// Per-peer dial-backoff floor (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): the\n 124│/// first failed dial to a peer waits this long before the pump re-dials THAT\n 125│/// peer. Scoped to the peer — a dead peer backs off while live peers stay hot.\n*126│const PEER_BACKOFF_BASE: Duration = Duration::from_secs(5);\n 127│\n 128│/// Per-peer dial-backoff cap (G5): a persistently-dead peer is re-dialed at most\n 129│/// every 5 minutes (same bound as the supervisor) — bounded, never a hot redial\n 130│/// loop, never silent. Reset to the floor on any successful connect.\n*131│const PEER_BACKOFF_CAP: Duration = Duration::from_secs(300);\n 132│\n 133│/// Round-drain margin over W1's per-dial reply-read budget (G3, Q1): because the\n 134│/// dials are CONCURRENT, every outcome lands within ~one W1 bound, so the round\n    │...\n 136│/// [`PEER_REPLY_READ_BUDGET`] (not a hardcoded 12s) so it tracks any W1 retune.\n*137│const ROUND_DRAIN_MARGIN: Duration = Duration::from_secs(2);\n 138│\n 139│/// Per-peer dial schedule (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION, G5): a peer is\n 140│/// dial-eligible when `now >= next_due`. A `DIAL_FAILED` (or a mid-round\n    │...\n 294│#[derive(Debug, Clone, Copy)]\n*295│pub struct PumpCadence {\n 296│    pub registry: Duration,\n 297│    pub notif: Duration,\n 298│    pub sync: Duration,\n    │...\n 304│\n*305│impl PumpCadence {\n 306│    /// From the daemon config knobs.\n*307│    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {\n*308│        PumpCadence {\n 309│            registry: cfg.registry_pump_period,\n 310│            notif: cfg.notif_pump_period,\n 311│            sync: cfg.sync_pull_period,\n    │...\n 320│pub struct PumpConfig {\n*321│    pub cadence: PumpCadence,\n 322│    /// `true` = the explicit opt-in to prompt-free updates; default `false`\n 323│    /// surfaces every staged release as a consent notif.\n 324│    pub full_auto_update: bool,\n    │...\n 330│        PumpConfig {\n*331│            cadence: PumpCadence::from_config(cfg),\n 332│            full_auto_update: cfg.full_auto_update,\n 333│        }\n 334│    }\n    │...\n 378│    /// wake claims exactly one forced round. Default: no wake edge.\n*379│    fn poll_wake(&mut self) -> bool {\n 380│        false\n 381│    }\n 382│\n    │...\n 513│/// The pure scheduler + fan-out (pump-seam P4, all four legs behind the seam):\n*514│/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\n 515│/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\n 516│/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\n 517│/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\n    │...\n 532│    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\n*533│    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n 534│    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n 535│    // hfenduleam stall). A timed-out read escalates to a supervised restart\n 536│    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n    │...\n 611│        if !any_due {\n*612│            thread::sleep(TICK);\n 613│            continue;\n 614│        }\n 615│\n    │...\n 663│                            eprintln!(\n*664│                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\n 665│                                now_ms(),\n 666│                                mono_ms()\n 667│                            );\n## notif.rs\n 15│\n*16│use super::{PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n 17│\n 18│/// The notif leg as a [`PumpWorker`]: the full semilattice spool per subnet\n 19│/// ([`emit_notif_feed`]); the receiver's join dedups.\n   │...\n 23│    /// This worker's scheduling period (`cadence.notif`).\n*24│    cadence: Duration,\n 25│}\n 26│\n 27│impl NotifWorker {\n 28│    /// Construction capture (§Frozen-4): the notif spool DB path + the cadence.\n*29│    pub(super) fn new(paths: &PumpPaths, cadence: &PumpCadence) -> NotifWorker {\n 30│        NotifWorker {\n 31│            notif_db: paths.notif_db.clone(),\n*32│            cadence: cadence.notif,\n 33│        }\n 34│    }\n 35│}\n## registry.rs\n   3│//!\n  *4│//! `poll_wake` consumes the event-driven advertisement marker (force a round\n   5│//! NOW on an endpoint online/offline transition); `pre_round` runs the\n   6│//! node-local sweep (repair-evictions → silent-peer eviction → due seed\n   7│//! rotations → re-advertise) ONCE per due round and holds the produced adverts\n    │...\n  22│\n *23│use super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n  24│\n  25│/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,\n  26│/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the\n    │...\n  76│    rotations: PathBuf,\n *77│    /// The event-driven advertisement marker (`poll_wake`).\n  78│    advertise_marker: PathBuf,\n  79│    /// This worker's scheduling period (`cadence.registry`).\n *80│    cadence: Duration,\n  81│    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10).\n  82│    evict_after: Duration,\n  83│    /// Adverts computed in `pre_round`, consumed per-subnet in `peer_step`.\n    │...\n  92│        paths: &PumpPaths,\n *93│        cadence: &PumpCadence,\n  94│    ) -> RegistryWorker {\n  95│        RegistryWorker {\n  96│            registry,\n    │...\n 100│            advertise_marker: paths.advertise_marker.clone(),\n*101│            cadence: cadence.registry,\n 102│            evict_after: cadence.registry_evict_after,\n 103│            adverts: Vec::new(),\n 104│        }\n    │...\n 119│    // [impl->REQ-CONV-2]\n*120│    fn poll_wake(&mut self) -> bool {\n 121│        crate::registryhost::take_advertise_now(&self.advertise_marker)\n 122│    }\n 123│\n    │...\n 223│\n*224│    // [unit->REQ-CONV-2] the worker's `poll_wake` consumes the advertise-now\n 225│    // marker exactly once — one online/offline transition forces exactly one\n 226│    // registry round, and an absent marker forces nothing.\n 227│    #[test]\n*228│    fn poll_wake_takes_the_advertise_marker_exactly_once() {\n 229│        let dir = tempfile::tempdir().unwrap();\n 230│        let marker = dir.path().join(\"advertise-now\");\n 231│        let mut w = RegistryWorker {\n    │...\n 240│            advertise_marker: marker.clone(),\n*241│            cadence: Duration::from_secs(30),\n*242│            evict_after: Duration::from_secs(600),\n 243│            adverts: Vec::new(),\n 244│        };\n*245│        assert!(!w.poll_wake(), \"nothing to take yet\");\n 246│        std::fs::write(&marker, b\"\").unwrap();\n*247│        assert!(w.poll_wake(), \"first take claims the transition\");\n*248│        assert!(!w.poll_wake(), \"second take gets nothing\");\n 249│    }\n 250│\n 251│    // [unit->REQ-MESH-4] `pre_round` runs the full node-local sweep once per due\n    │...\n 299│                advertise_marker: home.join(\"advertise-now\"),\n*300│                cadence: Duration::from_secs(30),\n*301│                evict_after: Duration::from_secs(600),\n 302│                adverts: Vec::new(),\n 303│            };\n 304│\n## sync.rs\n  2│//!\n *3│//! `poll_wake` consumes the wake-edge freshness-pull marker (a dormant→active\n  4│//! transition forces a pull NOW); `peer_step` builds the want-set — the union\n  5│//! of locally-tracked allowed branches ([`select_refs`]) and registry-derived\n  6│//! `a-<id>` bootstrap refs (endpoints known in the shared subnet this node\n   │...\n 23│\n*24│use super::{PeerIo, PumpCadence, PumpHooks, PumpPaths, PumpWorker, RoundCtx};\n 25│\n 26│/// The sync leg as a [`PumpWorker`]: anti-entropy pull for the minds.\n 27│pub(super) struct SyncWorker {\n   │...\n 38│    /// This worker's scheduling period (`cadence.sync`).\n*39│    cadence: Duration,\n 40│    /// The conflict-surface hooks (ADR-0013): `on_pull` fires after every\n 41│    /// completed pull. A sync-worker construction dep (the conflict surface).\n 42│    hooks: PumpHooks,\n   │...\n 50│        paths: &PumpPaths,\n*51│        cadence: &PumpCadence,\n 52│        hooks: PumpHooks,\n 53│    ) -> SyncWorker {\n 54│        SyncWorker {\n   │...\n 58│            pull_marker: paths.pull_marker.clone(),\n*59│            cadence: cadence.sync,\n 60│            hooks,\n 61│        }\n 62│    }\n   │...\n 74│    // [impl->REQ-INST-3]\n*75│    fn poll_wake(&mut self) -> bool {\n 76│        crate::resting::take_freshness_pull(&self.pull_marker)\n 77│    }\n 78│\n## update.rs\n 28│\n*29│use super::{now_ms, PeerIo, PumpCadence, PumpPaths, PumpWorker, RoundCtx};\n 30│\n 31│/// The update leg as a [`PumpWorker`]: the peer-propagation pull (REQ-UPD-1),\n 32│/// consent-gated by default (REQ-UPD-4), with typed convergence outcomes\n   │...\n 53│    /// This worker's scheduling period (`cadence.update`).\n*54│    cadence: Duration,\n 55│}\n 56│\n 57│impl UpdateWorker {\n   │...\n 62│        paths: &PumpPaths,\n*63│        cadence: &PumpCadence,\n 64│        full_auto_update: bool,\n 65│    ) -> UpdateWorker {\n 66│        UpdateWorker {\n   │...\n 74│            full_auto_update,\n*75│            cadence: cadence.update,\n 76│        }\n 77│    }\n 78│}"},"isError":false,"timestamp":1784271677282}}
{"type":"message","id":"738911ed","parentId":"1cfd051a","timestamp":"2026-07-17T07:01:17.286Z","message":{"role":"toolResult","toolCallId":"call_KuwhqY7eqmZ9ovWBIzxSwag0|fc_0e15f89f5b7b4062016a59d33c5ee48194ad6588e3f91ba401","toolName":"grep","content":[{"type":"text","text":" 17|//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\n*18|//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n*19|//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n 20|//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n 21|//!   scan;\n 22|//! - maintains an observability sidecar (`project-index-stats.json`) — index\n 23|//!   presence alone is not health: generated time, source generation, pending\n*24|//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n 25|//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n 26|//!   (wall-clock budgets are manual acceptance ONLY).\n 27|// [impl->REQ-PROJECT-INDEX-WRITER]\n...\n 90|    pub source_generation: String,\n*91|    /// Whether a refresh is queued/underway right now.\n 92|    #[serde(default)]\n*93|    pub pending_refresh: bool,\n 94|    /// When the last reconcile cycle ran (epoch ms).\n 95|    #[serde(default)]\n 96|    pub last_run_ms: u64,\n...\n 179|/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n*180|/// for a linked worktree). Identity refreshes ONLY when the marker path or\n 181|/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\n 182|/// §3: commits do not alter project identity).\n 183|#[derive(Debug, Clone)]\n...\n 245|            source_generation: last_fingerprint.clone(),\n*246|            pending_refresh: true, // the boot reconcile is queued by definition\n 247|            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n 248|            ..IndexWriterStats::default()\n 249|        };\n...\n 518|        self.stats.last_run_ms = now_ms();\n*519|        self.stats.pending_refresh = true; // the work is still owed\n 520|        self.write_stats();\n 521|    }\n 522|\n...\n 528|        self.stats.last_error = None;\n*529|        self.stats.pending_refresh = false;\n 530|        self.stats.cwds = self.cwd_cache.len() as u64;\n 531|        self.write_stats();\n 532|    }\n...\n 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 619|/// brain rehydrates from the persisted index (ready without warm).\n...\n 621|// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*622|pub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n 623|    std::thread::spawn(move || {\n 624|        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\n 625|        // untouched): rigs that need a writer-silent daemon set it — the\n...\n 629|        }\n*630|        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\n*631|        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\n*632|        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\n 633|\n 634|        let mut engine = WriterEngine::new(WriterPaths::production());\n 635|        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\n...\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 645|        let mut since_periodic = Duration::ZERO;\n*646|        while !stop.load(Ordering::SeqCst) {\n 647|            // Sleep one poll slice (sliced so a stop lands promptly).\n 648|            let mut left = poll.as_millis() as u64;\n 649|            while left > 0 && !stop.load(Ordering::SeqCst) {\n 650|                let step = left.min(100);\n*651|                std::thread::sleep(Duration::from_millis(step));\n 652|                left -= step;\n 653|            }\n 654|            if stop.load(Ordering::SeqCst) {\n...\n 665|            if has_events {\n*666|                // Debounce: let the burst land, then drain it ALL — the\n*667|                // coalescing window (multiple invalidations → one refresh).\n 668|                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*669|                std::thread::sleep(debounce);\n*670|                engine.stats.pending_refresh = true;\n 671|                engine.write_stats();\n 672|            }\n*673|            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 674|            if due_periodic {\n 675|                since_periodic = Duration::ZERO;\n 676|            }\n...\n 894|        assert!(stats.last_error.is_some(), \"the failure is an observable fact\");\n*895|        assert!(stats.pending_refresh, \"the owed work stays visible\");\n 896|    }\n 897|\n 898|    // [unit->REQ-PROJECT-INDEX-INVALIDATION] scoped invalidation: an endpoint\n...\n 1004|        // and the attribution follows the new identity.\n*1005|        std::thread::sleep(Duration::from_millis(30)); // distinct mtime/len either way\n 1006|        spt_store::gitrun::run_git_ok(\n 1007|            &[\n 1008|                \"-C\",\n...\n 1080|            // Wait for the boot cycle to settle.\n*1081|            while stats().map(|s| s.pending_refresh || s.generated_ms == 0).unwrap_or(true) {\n 1082|                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n*1083|                std::thread::sleep(Duration::from_millis(25));\n 1084|            }\n 1085|            let enums_after_boot = stats().unwrap().cumulative.branch_enumerations;\n 1086|\n...\n 1095|            while spt_store::projinval::pending_at(&spt_store::projinval::invalidations_dir())\n*1096|                || stats().map(|s| s.pending_refresh).unwrap_or(true)\n 1097|            {\n 1098|                assert!(Instant::now() < consumed, \"burst never consumed\");\n*1099|                std::thread::sleep(Duration::from_millis(25));\n 1100|            }\n 1101|            let s = stats().unwrap();\n 1102|            stop.store(true, Ordering::SeqCst);\n...\n 1266|        assert_eq!(stats.endpoints, 1);\n*1267|        assert!(!stats.pending_refresh);\n 1268|    }\n 1269|}"}],"details":{"scopePath":"crates/spt-daemon/src/projwriter.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\projwriter.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":29,"fileCount":1,"files":["crates/spt-daemon/src/projwriter.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/projwriter.rs","count":29}],"truncated":false,"displayContent":"   17│//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\n  *18│//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n  *19│//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n   20│//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n   21│//!   scan;\n   22│//! - maintains an observability sidecar (`project-index-stats.json`) — index\n   23│//!   presence alone is not health: generated time, source generation, pending\n  *24│//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n   25│//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n   26│//!   (wall-clock budgets are manual acceptance ONLY).\n   27│// [impl->REQ-PROJECT-INDEX-WRITER]\n     │...\n   90│    pub source_generation: String,\n  *91│    /// Whether a refresh is queued/underway right now.\n   92│    #[serde(default)]\n  *93│    pub pending_refresh: bool,\n   94│    /// When the last reconcile cycle ran (epoch ms).\n   95│    #[serde(default)]\n   96│    pub last_run_ms: u64,\n     │...\n  179│/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n *180│/// for a linked worktree). Identity refreshes ONLY when the marker path or\n  181│/// its (mtime, len) stamp moves — ordinary commits touch neither (ADR-0037\n  182│/// §3: commits do not alter project identity).\n  183│#[derive(Debug, Clone)]\n     │...\n  245│            source_generation: last_fingerprint.clone(),\n *246│            pending_refresh: true, // the boot reconcile is queued by definition\n  247│            endpoints: current.as_ref().map(|i| i.endpoints.len() as u64).unwrap_or(0),\n  248│            ..IndexWriterStats::default()\n  249│        };\n     │...\n  518│        self.stats.last_run_ms = now_ms();\n *519│        self.stats.pending_refresh = true; // the work is still owed\n  520│        self.write_stats();\n  521│    }\n  522│\n     │...\n  528│        self.stats.last_error = None;\n *529│        self.stats.pending_refresh = false;\n  530│        self.stats.cwds = self.cwd_cache.len() as u64;\n  531│        self.write_stats();\n  532│    }\n     │...\n  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  619│/// brain rehydrates from the persisted index (ready without warm).\n     │...\n  621│// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n *622│pub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n  623│    std::thread::spawn(move || {\n  624│        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\n  625│        // untouched): rigs that need a writer-silent daemon set it — the\n     │...\n  629│        }\n *630│        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\n *631│        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\n *632│        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\n  633│\n  634│        let mut engine = WriterEngine::new(WriterPaths::production());\n  635│        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\n     │...\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  645│        let mut since_periodic = Duration::ZERO;\n *646│        while !stop.load(Ordering::SeqCst) {\n  647│            // Sleep one poll slice (sliced so a stop lands promptly).\n  648│            let mut left = poll.as_millis() as u64;\n  649│            while left > 0 && !stop.load(Ordering::SeqCst) {\n  650│                let step = left.min(100);\n *651│                std::thread::sleep(Duration::from_millis(step));\n  652│                left -= step;\n  653│            }\n  654│            if stop.load(Ordering::SeqCst) {\n     │...\n  665│            if has_events {\n *666│                // Debounce: let the burst land, then drain it ALL — the\n *667│                // coalescing window (multiple invalidations → one refresh).\n  668│                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n *669│                std::thread::sleep(debounce);\n *670│                engine.stats.pending_refresh = true;\n  671│                engine.write_stats();\n  672│            }\n *673│            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n  674│            if due_periodic {\n  675│                since_periodic = Duration::ZERO;\n  676│            }\n     │...\n  894│        assert!(stats.last_error.is_some(), \"the failure is an observable fact\");\n *895│        assert!(stats.pending_refresh, \"the owed work stays visible\");\n  896│    }\n  897│\n  898│    // [unit->REQ-PROJECT-INDEX-INVALIDATION] scoped invalidation: an endpoint\n     │...\n 1004│        // and the attribution follows the new identity.\n*1005│        std::thread::sleep(Duration::from_millis(30)); // distinct mtime/len either way\n 1006│        spt_store::gitrun::run_git_ok(\n 1007│            &[\n 1008│                \"-C\",\n     │...\n 1080│            // Wait for the boot cycle to settle.\n*1081│            while stats().map(|s| s.pending_refresh || s.generated_ms == 0).unwrap_or(true) {\n 1082│                assert!(Instant::now() < deadline, \"boot cycle never settled\");\n*1083│                std::thread::sleep(Duration::from_millis(25));\n 1084│            }\n 1085│            let enums_after_boot = stats().unwrap().cumulative.branch_enumerations;\n 1086│\n     │...\n 1095│            while spt_store::projinval::pending_at(&spt_store::projinval::invalidations_dir())\n*1096│                || stats().map(|s| s.pending_refresh).unwrap_or(true)\n 1097│            {\n 1098│                assert!(Instant::now() < consumed, \"burst never consumed\");\n*1099│                std::thread::sleep(Duration::from_millis(25));\n 1100│            }\n 1101│            let s = stats().unwrap();\n 1102│            stop.store(true, Ordering::SeqCst);\n     │...\n 1266│        assert_eq!(stats.endpoints, 1);\n*1267│        assert!(!stats.pending_refresh);\n 1268│    }\n 1269│}"},"isError":false,"timestamp":1784271677286}}
{"type":"message","id":"e7825f08","parentId":"738911ed","timestamp":"2026-07-17T07:01:17.349Z","message":{"role":"toolResult","toolCallId":"call_cnXtC9QA2pjm4CyyAmJkkvJJ|fc_0e15f89f5b7b4062016a59d33c5f1481948c31dd57a6b21113","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## dispatch.rs\n 428|/// stream).\n*429|pub fn run_dispatch_loop(\n 430|    broker_name: &str,\n 431|    paths: &DispatchPaths,\n 432|    registry: Arc<RegistryHost>,\n...\n 1196|/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\n*1197|pub fn spawn_dispatcher(\n 1198|    broker_name: String,\n 1199|    registry: Arc<RegistryHost>,\n 1200|    stop: Arc<AtomicBool>,\n...\n 1204|        if let Err(e) =\n*1205|            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 1206|        {\n 1207|            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 1208|        }\n## brainproc.rs\n 323|    let stop = Arc::new(AtomicBool::new(false));\n*324|    let _ = crate::dispatch::spawn_dispatcher(\n 325|        broker_socket_name(),\n 326|        Arc::clone(&registry),\n 327|        Arc::clone(&stop),"}],"details":{"scopePath":"crates/spt-daemon/src/dispatch.rs, crates/spt-daemon/src/brainproc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":4,"fileCount":2,"files":["crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/brainproc.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/dispatch.rs","count":3},{"path":"crates/spt-daemon/src/brainproc.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## dispatch.rs\n  428│/// stream).\n *429│pub fn run_dispatch_loop(\n  430│    broker_name: &str,\n  431│    paths: &DispatchPaths,\n  432│    registry: Arc<RegistryHost>,\n     │...\n 1196│/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\n*1197│pub fn spawn_dispatcher(\n 1198│    broker_name: String,\n 1199│    registry: Arc<RegistryHost>,\n 1200│    stop: Arc<AtomicBool>,\n     │...\n 1204│        if let Err(e) =\n*1205│            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n 1206│        {\n 1207│            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n 1208│        }\n## brainproc.rs\n 323│    let stop = Arc::new(AtomicBool::new(false));\n*324│    let _ = crate::dispatch::spawn_dispatcher(\n 325│        broker_socket_name(),\n 326│        Arc::clone(&registry),\n 327│        Arc::clone(&stop),"},"isError":false,"timestamp":1784271677349}}
{"type":"message","id":"c08c99d2","parentId":"e7825f08","timestamp":"2026-07-17T07:01:17.417Z","message":{"role":"toolResult","toolCallId":"call_FFqRHBchizlQ2U1gppBYcweu|fc_0e15f89f5b7b4062016a59d33c5f24819480aef2273d69588d","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## livehost.rs\n 3|//!\n*4|//! This is the production caller `run_pulse_loop` lacked: the brain instantiates\n 5|//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n 6|//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n 7|//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n...\n 49|/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).\n*50|pub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;\n 51|\n 52|/// One brain-hosted live lifecycle: its config-paced pulse-driver thread and the\n 53|/// held stop flag (tripped to un-host on an offline-transition). F-030 W3: the\n...\n 133|/// One reconcile sweep — the brain's live host-set invariant holder, run at boot\n*134|/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the\n 135|/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):\n 136|///\n 137|/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a\n...\n 637|        std::thread::spawn(move || {\n*638|            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});\n 639|        })\n 640|    };\n 641|    set.insert(id, HostedLife { stop, thread: handle });\n...\n 971|/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n*972|/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n 973|/// config are re-read each sweep (adapter / pulse-period changes land between\n 974|/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n 975|/// for symmetry — the threads die with the brain process and the next brain\n...\n 1004|        }\n*1005|        while !stop.load(Ordering::SeqCst) {\n 1006|            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n 1007|            // unset = normal production, untouched): skip the reconcile poll+body so an\n 1008|            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n 1009|            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n*1010|            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n 1011|            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n 1012|            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n 1013|            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n...\n 1019|            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n*1020|                std::thread::sleep(Duration::from_millis(100));\n 1021|                continue;\n 1022|            }\n 1023|            let owlery = perch::owlery_dir();\n...\n 1047|            // Sleep in slices so a stop lands promptly.\n*1048|            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n 1049|            while left > 0 && !stop.load(Ordering::SeqCst) {\n 1050|                let step = left.min(100);\n*1051|                std::thread::sleep(Duration::from_millis(step));\n 1052|                left -= step;\n 1053|            }\n 1054|        }\n...\n 1100|        DaemonConfig {\n*1101|            pulse_period: Duration::from_millis(15),\n 1102|            ..DaemonConfig::default()\n 1103|        }\n 1104|    }\n...\n 1116|            }\n*1117|            std::thread::sleep(Duration::from_millis(20));\n 1118|        }\n 1119|        pred()\n 1120|    }\n...\n 1491|                    }\n*1492|                    std::thread::sleep(Duration::from_millis(10));\n 1493|                }\n 1494|                b.expect(\"connect to test broker\")\n 1495|            };\n...\n 1781|                }\n*1782|                std::thread::sleep(std::time::Duration::from_millis(20));\n 1783|            }\n 1784|            assert!(removed, \"could not remove the perch dir for the torn-down case\");\n 1785|            run();\n## lifecycle.rs\n 301|    /// sync side-effect both succeeded; `Some(Err(reason))` ⇒ the turn OR the\n*302|    /// commune-sync failed (a strike — [`run_pulse_loop`] feeds it the failure budget).\n 303|    pub turn_outcome: Option<Result<(), String>>,\n 304|}\n 305|\n...\n 506|    /// The configured pulse period (config-driven, not a constant).\n*507|    pub fn pulse_period(&self) -> Duration {\n*508|        self.cfg.pulse_period\n 509|    }\n 510|\n 511|    /// Base substitution keys spt-core fills for the lifecycle templates.\n...\n 677|    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n*678|    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n 679|    /// restart instant on every restart. Now the loop rehydrates a\n 680|    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n 681|    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n...\n 691|    // [impl->REQ-DAEMON-1]\n*692|    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n 693|        &self,\n 694|        session_id: Option<&str>,\n 695|        stop: &AtomicBool,\n...\n 701|        let key = session_id.unwrap_or(\"pulse\");\n*702|        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\n 703|        // A disk error must not stop the pulse: degrade to an in-memory anchor at\n 704|        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\n 705|        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\n...\n 722|        let mut turn_strikes: u32 = 0;\n*723|        while !stop.load(Ordering::Relaxed) {\n 724|            let now = now_ms();\n 725|            if now < deadline {\n 726|                // Sleep toward the deadline in small slices so a stop is honored\n...\n 900|    fn event_turn_timeout(&self) -> Duration {\n*901|        event_turn_timeout_for(self.pulse_period())\n 902|    }\n 903|\n 904|    /// The agent's OWN subnet — the parent endpoint's `home_subnet` (info.json),\n...\n 927|    /// non-zero exit, or the 7.3 null-stdout shape) — a failure is fed the W1 failure\n*928|    /// budget by [`run_pulse_loop`], NEVER a respawn (nothing resident to respawn).\n 929|    ///\n 930|    /// ── PSYCHE-TURN CONTRACT (adapter FREEZE point — perri's `psyche_resume` role) ──\n 931|    ///\n...\n 1100|        // reseed and a plain failure return `Err`, so BOTH count a strike vs the W1 budget\n*1101|        // (`run_pulse_loop` feeds the outcome to `note_turn_outcome`).\n 1102|        // [impl->REQ-PSYCHE-SID-CUSTODY]\n 1103|        match psyche_turn_and_relay(\n 1104|            &runtime, &keys, stdin, reply_to, &psyche_id, &mut ctx, &owlery, timeout,\n...\n 1317|    /// set before any death was observed. `owner_pid` is the supervised harness\n*1318|    /// pid; the poll cadence reuses the configured `pulse_period` (the same\n 1319|    /// scheduled-supervision clock as the pulse loop).\n 1320|    // [impl->REQ-DAEMON-4]\n 1321|    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]\n...\n 1330|        self.sweep_on_start();\n*1331|        while !stop.load(Ordering::Relaxed) {\n 1332|            if !is_process_alive(owner_pid) {\n 1333|                // Observed gone — run the crash teardown. The recheck re-reads the\n 1334|                // perch's CURRENT owner, so a Self that rebound across the grace\n...\n 1339|            }\n*1340|            sleep_interruptible(self.cfg.pulse_period, stop);\n 1341|        }\n 1342|        None\n 1343|    }\n...\n 1355|        let nap = left.min(SLICE);\n*1356|        std::thread::sleep(nap);\n 1357|        left = left.saturating_sub(nap);\n 1358|    }\n 1359|}\n...\n 2272|            let cfg = DaemonConfig {\n*2273|                pulse_period: Duration::from_millis(15),\n 2274|                ..DaemonConfig::default()\n 2275|            };\n 2276|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n*2277|            assert_eq!(host.pulse_period(), Duration::from_millis(15));\n 2278|\n 2279|            let stop = AtomicBool::new(false);\n 2280|            let ticks = AtomicUsize::new(0);\n...\n 2282|            // loop actually schedules repeated ticks and honors the stop flag.\n*2283|            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n 2284|                if ticks.fetch_add(1, Ordering::Relaxed) + 1 >= 3 {\n 2285|                    stop.store(true, Ordering::Relaxed);\n 2286|                }\n...\n 2304|            let cfg = DaemonConfig {\n*2305|                pulse_period: Duration::from_millis(10),\n 2306|                ..DaemonConfig::default()\n 2307|            };\n 2308|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n...\n 2311|            let first_bytes: Mutex<Option<Vec<u8>>> = Mutex::new(None);\n*2312|            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n 2313|                let cur = std::fs::read(crate::deadline::anchor_path(\"pulse\"))\n 2314|                    .expect(\"anchor file exists after loop entry\");\n 2315|                let mut slot = first_bytes.lock().unwrap();\n...\n 2339|            let cfg = DaemonConfig {\n*2340|                pulse_period: Duration::from_millis(10),\n 2341|                ..DaemonConfig::default()\n 2342|            };\n 2343|            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n## brainproc.rs\n 189|    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n*190|    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n 191|    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n 192|    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n 193|    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n...\n 237|    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n*238|    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n 239|    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\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...\n 259|    loop {\n*260|        thread::sleep(BRAIN_HEARTBEAT);\n 261|        // `net-status` is both the net-capability probe and the broker-liveness\n 262|        // probe: an `Err` is the broker gone (or a future planned\n 263|        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n...\n 342|                last = Some(e);\n*343|                thread::sleep(Duration::from_millis(10));\n 344|            }\n 345|        }\n 346|    }\n...\n 629|    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630|        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n 631|        slept += TICK;\n 632|    }\n 633|}\n...\n 718|        }\n*719|        thread::sleep(TICK);\n 720|    }\n 721|}\n 722|\n...\n 769|    let mut pre_ready_exits: u32 = 0;\n*770|    while !stop.load(Ordering::Relaxed) {\n 771|        let started = Instant::now();\n 772|        // Record-driven binary selection + trial latch (A8). The latch is the\n 773|        // unpromoted `AppliedPending` record, NOT the start reason; a `RolledBack`\n...\n 898|                            }\n*899|                            Ok(None) => thread::sleep(TICK),\n 900|                            Err(e) => {\n 901|                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n 902|                                break;"}],"details":{"scopePath":"crates/spt-daemon/src/livehost.rs, crates/spt-daemon/src/lifecycle.rs, crates/spt-daemon/src/brainproc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":42,"fileCount":3,"files":["crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/brainproc.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/livehost.rs","count":14},{"path":"crates/spt-daemon/src/lifecycle.rs","count":20},{"path":"crates/spt-daemon/src/brainproc.rs","count":8}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## livehost.rs\n    3│//!\n   *4│//! This is the production caller `run_pulse_loop` lacked: the brain instantiates\n    5│//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced\n    6│//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of\n    7│//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from\n     │...\n   49│/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).\n  *50│pub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;\n   51│\n   52│/// One brain-hosted live lifecycle: its config-paced pulse-driver thread and the\n   53│/// held stop flag (tripped to un-host on an offline-transition). F-030 W3: the\n     │...\n  133│/// One reconcile sweep — the brain's live host-set invariant holder, run at boot\n *134│/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the\n  135│/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):\n  136│///\n  137│/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a\n     │...\n  637│        std::thread::spawn(move || {\n *638│            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});\n  639│        })\n  640│    };\n  641│    set.insert(id, HostedLife { stop, thread: handle });\n     │...\n  971│/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n *972│/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n  973│/// config are re-read each sweep (adapter / pulse-period changes land between\n  974│/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n  975│/// for symmetry — the threads die with the brain process and the next brain\n     │...\n 1004│        }\n*1005│        while !stop.load(Ordering::SeqCst) {\n 1006│            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n 1007│            // unset = normal production, untouched): skip the reconcile poll+body so an\n 1008│            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n 1009│            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n*1010│            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n 1011│            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n 1012│            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n 1013│            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n     │...\n 1019│            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n*1020│                std::thread::sleep(Duration::from_millis(100));\n 1021│                continue;\n 1022│            }\n 1023│            let owlery = perch::owlery_dir();\n     │...\n 1047│            // Sleep in slices so a stop lands promptly.\n*1048│            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n 1049│            while left > 0 && !stop.load(Ordering::SeqCst) {\n 1050│                let step = left.min(100);\n*1051│                std::thread::sleep(Duration::from_millis(step));\n 1052│                left -= step;\n 1053│            }\n 1054│        }\n     │...\n 1100│        DaemonConfig {\n*1101│            pulse_period: Duration::from_millis(15),\n 1102│            ..DaemonConfig::default()\n 1103│        }\n 1104│    }\n     │...\n 1116│            }\n*1117│            std::thread::sleep(Duration::from_millis(20));\n 1118│        }\n 1119│        pred()\n 1120│    }\n     │...\n 1491│                    }\n*1492│                    std::thread::sleep(Duration::from_millis(10));\n 1493│                }\n 1494│                b.expect(\"connect to test broker\")\n 1495│            };\n     │...\n 1781│                }\n*1782│                std::thread::sleep(std::time::Duration::from_millis(20));\n 1783│            }\n 1784│            assert!(removed, \"could not remove the perch dir for the torn-down case\");\n 1785│            run();\n## lifecycle.rs\n  301│    /// sync side-effect both succeeded; `Some(Err(reason))` ⇒ the turn OR the\n *302│    /// commune-sync failed (a strike — [`run_pulse_loop`] feeds it the failure budget).\n  303│    pub turn_outcome: Option<Result<(), String>>,\n  304│}\n  305│\n     │...\n  506│    /// The configured pulse period (config-driven, not a constant).\n *507│    pub fn pulse_period(&self) -> Duration {\n *508│        self.cfg.pulse_period\n  509│    }\n  510│\n  511│    /// Base substitution keys spt-core fills for the lifecycle templates.\n     │...\n  677│    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n *678│    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n  679│    /// restart instant on every restart. Now the loop rehydrates a\n  680│    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n  681│    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n     │...\n  691│    // [impl->REQ-DAEMON-1]\n *692│    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n  693│        &self,\n  694│        session_id: Option<&str>,\n  695│        stop: &AtomicBool,\n     │...\n  701│        let key = session_id.unwrap_or(\"pulse\");\n *702│        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\n  703│        // A disk error must not stop the pulse: degrade to an in-memory anchor at\n  704│        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\n  705│        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\n     │...\n  722│        let mut turn_strikes: u32 = 0;\n *723│        while !stop.load(Ordering::Relaxed) {\n  724│            let now = now_ms();\n  725│            if now < deadline {\n  726│                // Sleep toward the deadline in small slices so a stop is honored\n     │...\n  900│    fn event_turn_timeout(&self) -> Duration {\n *901│        event_turn_timeout_for(self.pulse_period())\n  902│    }\n  903│\n  904│    /// The agent's OWN subnet — the parent endpoint's `home_subnet` (info.json),\n     │...\n  927│    /// non-zero exit, or the 7.3 null-stdout shape) — a failure is fed the W1 failure\n *928│    /// budget by [`run_pulse_loop`], NEVER a respawn (nothing resident to respawn).\n  929│    ///\n  930│    /// ── PSYCHE-TURN CONTRACT (adapter FREEZE point — perri's `psyche_resume` role) ──\n  931│    ///\n     │...\n 1100│        // reseed and a plain failure return `Err`, so BOTH count a strike vs the W1 budget\n*1101│        // (`run_pulse_loop` feeds the outcome to `note_turn_outcome`).\n 1102│        // [impl->REQ-PSYCHE-SID-CUSTODY]\n 1103│        match psyche_turn_and_relay(\n 1104│            &runtime, &keys, stdin, reply_to, &psyche_id, &mut ctx, &owlery, timeout,\n     │...\n 1317│    /// set before any death was observed. `owner_pid` is the supervised harness\n*1318│    /// pid; the poll cadence reuses the configured `pulse_period` (the same\n 1319│    /// scheduled-supervision clock as the pulse loop).\n 1320│    // [impl->REQ-DAEMON-4]\n 1321│    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]\n     │...\n 1330│        self.sweep_on_start();\n*1331│        while !stop.load(Ordering::Relaxed) {\n 1332│            if !is_process_alive(owner_pid) {\n 1333│                // Observed gone — run the crash teardown. The recheck re-reads the\n 1334│                // perch's CURRENT owner, so a Self that rebound across the grace\n     │...\n 1339│            }\n*1340│            sleep_interruptible(self.cfg.pulse_period, stop);\n 1341│        }\n 1342│        None\n 1343│    }\n     │...\n 1355│        let nap = left.min(SLICE);\n*1356│        std::thread::sleep(nap);\n 1357│        left = left.saturating_sub(nap);\n 1358│    }\n 1359│}\n     │...\n 2272│            let cfg = DaemonConfig {\n*2273│                pulse_period: Duration::from_millis(15),\n 2274│                ..DaemonConfig::default()\n 2275│            };\n 2276│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n*2277│            assert_eq!(host.pulse_period(), Duration::from_millis(15));\n 2278│\n 2279│            let stop = AtomicBool::new(false);\n 2280│            let ticks = AtomicUsize::new(0);\n     │...\n 2282│            // loop actually schedules repeated ticks and honors the stop flag.\n*2283│            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n 2284│                if ticks.fetch_add(1, Ordering::Relaxed) + 1 >= 3 {\n 2285│                    stop.store(true, Ordering::Relaxed);\n 2286│                }\n     │...\n 2304│            let cfg = DaemonConfig {\n*2305│                pulse_period: Duration::from_millis(10),\n 2306│                ..DaemonConfig::default()\n 2307│            };\n 2308│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n     │...\n 2311│            let first_bytes: Mutex<Option<Vec<u8>>> = Mutex::new(None);\n*2312│            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n 2313│                let cur = std::fs::read(crate::deadline::anchor_path(\"pulse\"))\n 2314│                    .expect(\"anchor file exists after loop entry\");\n 2315│                let mut slot = first_bytes.lock().unwrap();\n     │...\n 2339│            let cfg = DaemonConfig {\n*2340│                pulse_period: Duration::from_millis(10),\n 2341│                ..DaemonConfig::default()\n 2342│            };\n 2343│            let host = BrainLifecycle::with_config(&manifest, \"doyle\", cfg).expect(\"live host\");\n## brainproc.rs\n 189│    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n*190│    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n 191│    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n 192│    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n 193│    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    │...\n 237│    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n*238│    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n 239│    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\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    │...\n 259│    loop {\n*260│        thread::sleep(BRAIN_HEARTBEAT);\n 261│        // `net-status` is both the net-capability probe and the broker-liveness\n 262│        // probe: an `Err` is the broker gone (or a future planned\n 263│        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n    │...\n 342│                last = Some(e);\n*343│                thread::sleep(Duration::from_millis(10));\n 344│            }\n 345│        }\n 346│    }\n    │...\n 629│    while slept < backoff && !stop.load(Ordering::Relaxed) {\n*630│        thread::sleep(TICK.min(backoff.saturating_sub(slept)));\n 631│        slept += TICK;\n 632│    }\n 633│}\n    │...\n 718│        }\n*719│        thread::sleep(TICK);\n 720│    }\n 721│}\n 722│\n    │...\n 769│    let mut pre_ready_exits: u32 = 0;\n*770│    while !stop.load(Ordering::Relaxed) {\n 771│        let started = Instant::now();\n 772│        // Record-driven binary selection + trial latch (A8). The latch is the\n 773│        // unpromoted `AppliedPending` record, NOT the start reason; a `RolledBack`\n    │...\n 898│                            }\n*899│                            Ok(None) => thread::sleep(TICK),\n 900│                            Err(e) => {\n 901│                                eprintln!(\"BRAIN_WAIT_FAIL: {e}\");\n 902│                                break;"},"isError":false,"timestamp":1784271677417}}
{"type":"message","id":"d9198593","parentId":"c08c99d2","timestamp":"2026-07-17T07:01:17.441Z","message":{"role":"toolResult","toolCallId":"call_Ui0aB2pfB0s7ejmnY62MjCKH|fc_0e15f89f5b7b4062016a59d33c5f3c8194bf25baecb7c8c82d","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## applyhost.rs\n 756|        );\n*757|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 758|\n 759|        // 2. Single stamped for the OTHER OS refuses too. \"Other\" = any\n 760|        //    registered platform that is not this host — derived from the\n...\n 775|        );\n*776|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 777|\n 778|        // 3. Stamped for THIS platform: the single applies (the legitimate\n 779|        //    origin-fetched-local path).\n...\n 787|        );\n*788|        assert_eq!(std::fs::read(&exe).unwrap(), b\"CROSS-OS BYTES\");\n 789|    }\n 790|\n 791|    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] [unit->REQ-UPD-3] the planned\n...\n 898|        assert_eq!(\n*899|            std::fs::read(&exe).unwrap(),\n 900|            b\"NEW BINARY BYTES\",\n 901|            \"artifact landed\"\n 902|        );\n 903|        assert_eq!(\n*904|            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n 905|            b\"OLD BINARY\",\n 906|            \"the outgoing binary stepped aside\"\n 907|        );\n...\n 937|        assert_eq!(\n*938|            std::fs::read(&exe).unwrap(),\n 939|            b\"NEW BINARY BYTES\",\n 940|            \"artifact landed without any broker handoff\"\n 941|        );\n 942|        assert_eq!(\n*943|            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n 944|            b\"OLD BINARY\",\n 945|            \"the outgoing binary stepped aside\"\n 946|        );\n...\n 984|        );\n*985|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 986|\n 987|        // Already-applied: idempotent no-op, never re-renames an already-swapped exe.\n 988|        // A still-open trial that already landed this version's bytes marks it applied.\n...\n 1002|        );\n*1003|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1004|    }\n 1005|\n 1006|    // [unit->REQ-UPD-6] apply-time re-verification handles platform-targeted\n...\n 1028|        );\n*1029|        assert_eq!(std::fs::read(&exe).unwrap(), b\"SET BINARY BYTES\");\n 1030|        assert_eq!(broker.session_pid(sid), child_pid, \"child pid unchanged\");\n 1031|    }\n 1032|\n...\n 1050|        );\n*1051|        assert_eq!(std::fs::read(&exe).unwrap(), b\"NEW WITHOUT SESSIONS\");\n 1052|        assert_eq!(broker.session_count(), 0);\n 1053|    }\n 1054|\n...\n 1081|        );\n*1082|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1083|        assert!(cache.staged_update().is_some(), \"candidate stays staged\");\n 1084|    }\n 1085|\n...\n 1112|        );\n*1113|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1114|        assert!(cache.staged().is_some(), \"artifact stays staged\");\n 1115|\n 1116|        // Tampered at rest: the apply-time re-verification rejects.\n...\n 1123|        );\n*1124|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1125|\n 1126|        // Unprovisioned keys: fail-closed.\n 1127|        let (cache, _) = stage(dir.path(), 10, b\"Y\", BROKER_RESOURCE_ABI);\n...\n 1133|        );\n*1134|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\");\n 1135|    }\n 1136|\n 1137|    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] the quarantine guard (D6-2): a\n...\n 1164|        );\n*1165|        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1166|\n 1167|        // A NEWER version (v8) is not quarantined — the marker is version-keyed.\n 1168|        let (cache8, keys8) = stage(dir.path(), 8, b\"V8 BYTES\", BROKER_RESOURCE_ABI);\n## brainproc.rs\n 382|    let exe = std::env::current_exe().ok()?;\n*383|    let bytes = std::fs::read(exe).ok()?;\n 384|    Some(crate::release::sha256_hex(&bytes))\n 385|}\n 386|\n...\n 418|pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n*419|    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n 420|}\n 421|\n 422|/// The supervisor's next backoff after a brain death: a healthy run (≥\n...\n 563|    fn ready_exe_hash(&self) -> Option<String> {\n*564|        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n 565|        parse_ready_exe_hash(&body)\n 566|    }\n 567|    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n## broker.rs\n 2028|fn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n*2029|    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n 2030|    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n 2031|    let decl = manifest.message_idle_translation_binary.as_ref()?;\n 2032|    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n## config.rs\n 241|    pub fn load_from(path: &Path) -> Self {\n*242|        let Ok(raw) = std::fs::read_to_string(path) else {\n 243|            return Self::default();\n 244|        };\n 245|        let file: DaemonConfigFile = match serde_json::from_str(&raw) {\n## crc_swap.rs\n 52|) -> std::io::Result<()> {\n*53|    for entry in std::fs::read_dir(dir)? {\n 54|        let entry = entry?;\n 55|        let path = entry.path();\n 56|        if path.is_dir() {\n...\n 64|        let target = install_dir.join(&rel);\n*65|        let differs = match std::fs::read(&target) {\n*66|            Ok(existing) => sha256_hex(&std::fs::read(&path)?) != sha256_hex(&existing),\n 67|            // Absent (or unreadable) target ⇒ a new/changed file to place.\n 68|            Err(_) => true,\n 69|        };\n...\n 226|    let dashed = format!(\"{base}-\");\n*227|    let Ok(entries) = std::fs::read_dir(dir) else {\n 228|        return;\n 229|    };\n 230|    for entry in entries.flatten() {\n...\n 303|        fn walk(dir: &Path, found: &mut bool) {\n*304|            for entry in fs::read_dir(dir).unwrap() {\n 305|                let path = entry.unwrap().path();\n 306|                if path.is_dir() {\n 307|                    walk(&path, found);\n## daemon.rs\n 1606|            while Instant::now() < deadline {\n*1607|                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608|                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609|                        if p != 0 {\n 1610|                            found = Some(p);\n...\n 1641|        let diagpath = std::env::temp_dir().join(format!(\"spt_breakaway_diag_{}.log\", std::process::id()));\n*1642|        let diag = std::fs::read_to_string(&diagpath).unwrap_or_default();\n 1643|        let _ = std::fs::remove_file(&diagpath);\n 1644|\n 1645|        // Second skip path (sibling of the ACCESS_DENIED skip above): on some\n## deadline.rs\n 117|    fn load(path: &Path) -> Option<Self> {\n*118|        let raw = std::fs::read_to_string(path).ok()?;\n 119|        serde_json::from_str(&raw).ok()\n 120|    }\n 121|\n## deelevate.rs\n 197|    pub(super) fn elected_user_from(path: &std::path::Path) -> Option<SudoInvoker> {\n*198|        let name = std::fs::read_to_string(path).ok()?;\n 199|        user_by_name(name.trim())\n 200|    }\n 201|\n## digest.rs\n 417|fn log_less_activity(id: &str, perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*418|    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 419|        return Vec::new();\n 420|    };\n 421|    let mut items = Vec::new();\n...\n 462|fn read_context_items(perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*463|    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 464|        return Vec::new();\n 465|    };\n 466|    s.lines()\n## docshost.rs\n 188|    }\n*189|    match std::fs::read(&canon_target) {\n 190|        Ok(bytes) => respond(StatusCode::OK, content_type_for(&rel), bytes),\n 191|        Err(e) => respond(\n 192|            StatusCode::INTERNAL_SERVER_ERROR,\n## effect.rs\n 930|        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n*931|        let text = std::fs::read_to_string(j.path()).expect(\"read journal file\");\n 932|        // New line shape carries the minter tag: \"PENDING 1 cli 2 net-send\".\n 933|        let net_token = format!(\"{} {} {}\", net_key.class, net_key.minter.as_tag(), net_key.op);\n 934|        let pty_token = format!(\"{} {} {}\", pty_key.class, pty_key.minter.as_tag(), pty_key.op);\n## endpoint.rs\n 98|pub fn read_pump_heartbeat() -> Option<u64> {\n*99|    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n 100|    serde_json::from_str::<u64>(s.trim()).ok()\n 101|}\n 102|\n## lifecycle.rs\n 465|        let dir = self.install_dir.as_ref()?;\n*466|        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n 467|        Some(crate::release::sha256_hex(&bytes))\n 468|    }\n 469|\n...\n 486|        };\n*487|        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n 488|            return;\n 489|        };\n 490|        let hash = crate::release::sha256_hex(&bytes);\n...\n 760|        };\n*761|        fetch_history(\n 762|            h,\n 763|            &self.base_keys(session_id),\n 764|            &self.id,\n...\n 1503|\n*1504|            let got = std::fs::read_to_string(&marker)\n 1505|                .expect(\"the psyche child must have spawned + written the marker\");\n 1506|            assert_eq!(\n 1507|                got.trim(),\n...\n 1823|        let fresh = write_psyche_context_file(&perch, first_turn_psyche_context(Some(\"MIND\"))).unwrap();\n*1824|        assert_eq!(std::fs::read_to_string(&fresh).unwrap(), \"MIND\");\n 1825|        assert!(std::fs::metadata(&fresh).unwrap().len() > 0);\n 1826|\n 1827|        // FRESH with a zero-context agent: the NON-EMPTY <fresh-psyche/> marker (never\n...\n 1830|        assert_eq!(marker, cont, \"overwrite in place — same nested-perch path each turn\");\n*1831|        assert_eq!(std::fs::read_to_string(&marker).unwrap(), PSYCHE_FRESH_MARKER);\n 1832|        assert!(std::fs::metadata(&marker).unwrap().len() > 0);\n 1833|    }\n 1834|\n...\n 2105|            let drop_path = project.path().join(\".claude\").join(\"doyle-commune.md\");\n*2106|            let body = std::fs::read_to_string(&drop_path)\n 2107|                .expect(\"commune drop written under <endpoint cwd>/.claude\");\n 2108|            // The rider: the summarizer child RAN in the endpoint cwd (its printed\n 2109|            // cwd is the project dir). Normalize separators/case for the compare.\n...\n 2312|            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n*2313|                let cur = std::fs::read(crate::deadline::anchor_path(\"pulse\"))\n 2314|                    .expect(\"anchor file exists after loop entry\");\n 2315|                let mut slot = first_bytes.lock().unwrap();\n 2316|                match &*slot {\n...\n 2355|            let anchor_of = || {\n*2356|                let raw = std::fs::read_to_string(crate::deadline::anchor_path(\"pulse\")).unwrap();\n 2357|                serde_json::from_str::<DeadlineAnchor>(&raw).unwrap().anchor_ms\n 2358|            };\n 2359|\n## livehost.rs\n 297|fn read_resume_pid(perch: &Path) -> Option<u32> {\n*298|    std::fs::read_to_string(perch.join(RESUME_PID_FILE))\n 299|        .ok()\n 300|        .and_then(|s| s.trim().parse::<u32>().ok())\n 301|        .filter(|&p| p != 0)\n...\n 918|        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n*919|        let Ok(entries) = std::fs::read_dir(&perch) else {\n 920|            continue;\n 921|        };\n 922|        for entry in entries.flatten() {\n...\n 1889|        );\n*1890|        let displaced: Vec<_> = std::fs::read_dir(&perch)\n 1891|            .unwrap()\n 1892|            .flatten()\n 1893|            .filter(|e| e.file_name().to_string_lossy().starts_with(\".live-bin.old\"))\n## machineid.rs\n 44|    for path in [\"/etc/machine-id\", \"/var/lib/dbus/machine-id\"] {\n*45|        if let Ok(s) = std::fs::read_to_string(path) {\n 46|            let s = s.trim().to_string();\n 47|            if !s.is_empty() {\n 48|                return Some(s);\n...\n 109|    let path = spt_store::perch::identity_dir().join(\"machine-id\");\n*110|    if let Ok(s) = std::fs::read_to_string(&path) {\n 111|        let s = s.trim().to_string();\n 112|        if !s.is_empty() {\n 113|            return Some(s);\n## notif.rs\n 1170|            let content = loop {\n*1171|                if let Ok(c) = std::fs::read_to_string(&marker) {\n 1172|                    if !c.trim().is_empty() {\n 1173|                        break c;\n 1174|                    }\n## presence.rs\n 59|    let mut out = BTreeMap::new();\n*60|    let Ok(entries) = std::fs::read_dir(dir) else {\n 61|        return out;\n 62|    };\n 63|    for entry in entries.flatten() {\n...\n 70|        };\n*71|        let Ok(raw) = std::fs::read_to_string(&path) else {\n 72|            continue;\n 73|        };\n 74|        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else {\n## projwriter.rs\n 140|pub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n*141|    let raw = std::fs::read_to_string(path).ok()?;\n 142|    serde_json::from_str(&raw).ok()\n 143|}\n 144|\n...\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...\n 416|        for (id, perch_path, origin_cwd) in &targets {\n*417|            let entries = sessions::read_all(perch_path);\n 418|            let store_ids: Vec<String> = self\n 419|                .branch_order\n 420|                .iter()\n...\n 559|fn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\n*560|    let Ok(entries) = std::fs::read_dir(owlery) else {\n 561|        return Vec::new();\n 562|    };\n 563|    let mut out = Vec::new();\n...\n 845|        first.reconcile(&global(), false).unwrap();\n*846|        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n 847|\n 848|        // The restarted brain: fresh engine, warm disk.\n 849|        let mut warm = WriterEngine::new(fx.paths.clone());\n...\n 857|        assert_eq!(\n*858|            std::fs::read(&fx.paths.index_path).unwrap(),\n 859|            bytes_before,\n 860|            \"the snapshot is byte-untouched — no rewrite, no generated_ms churn\"\n 861|        );\n...\n 874|        engine.reconcile(&global(), false).unwrap();\n*875|        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n 876|\n 877|        // Wreck the store: the seed git DIR becomes a plain FILE — it still\n 878|        // `exists()`, but opening it as a repo fails loudly (and cannot be\n...\n 886|        assert_eq!(\n*887|            std::fs::read(&fx.paths.index_path).unwrap(),\n 888|            bytes_before,\n 889|            \"a failed cycle never touches the published snapshot\"\n 890|        );\n...\n 1174|                store\n*1175|                    .branches_by_recency()\n 1176|                    .unwrap()\n 1177|                    .into_iter()\n 1178|                    .filter_map(|b| {\n...\n 1186|            let perch = owlery.join(id);\n*1187|            let entries = sessions::read_all(&perch);\n 1188|            let origin = info::read_info(&perch).and_then(|r| r.cwd);\n 1189|            projderive::project_refs_from(\n 1190|                &entries,\n## propagate.rs\n 473|                            drop(file.take()); // flush before reading back\n*474|                            let artifact = std::fs::read(&artifact_path).unwrap_or_default();\n 475|                            let _ = std::fs::remove_file(&artifact_path);\n 476|                            let offer = signed.take().ok_or_else(|| {\n 477|                                io::Error::other(\"peer sent Done before any Offer\")\n\n## pump/\n### health.rs\n 79|    pub fn load_from(path: &Path) -> PumpHealth {\n*80|        match std::fs::read_to_string(path) {\n 81|            Ok(s) => serde_json::from_str(&s).unwrap_or_default(),\n 82|            Err(_) => PumpHealth::default(),\n 83|        }\n\nShowing files 1-20 of 68. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/src, crates/spt-live/src, crates/spt-store/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":64,"fileCount":20,"files":["crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/machineid.rs","crates/spt-daemon/src/notif.rs","crates/spt-daemon/src/presence.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/pump/health.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/applyhost.rs","count":16},{"path":"crates/spt-daemon/src/brainproc.rs","count":3},{"path":"crates/spt-daemon/src/broker.rs","count":1},{"path":"crates/spt-daemon/src/config.rs","count":1},{"path":"crates/spt-daemon/src/crc_swap.rs","count":5},{"path":"crates/spt-daemon/src/daemon.rs","count":2},{"path":"crates/spt-daemon/src/deadline.rs","count":1},{"path":"crates/spt-daemon/src/deelevate.rs","count":1},{"path":"crates/spt-daemon/src/digest.rs","count":2},{"path":"crates/spt-daemon/src/docshost.rs","count":1},{"path":"crates/spt-daemon/src/effect.rs","count":1},{"path":"crates/spt-daemon/src/endpoint.rs","count":1},{"path":"crates/spt-daemon/src/lifecycle.rs","count":9},{"path":"crates/spt-daemon/src/livehost.rs","count":3},{"path":"crates/spt-daemon/src/machineid.rs","count":2},{"path":"crates/spt-daemon/src/notif.rs","count":1},{"path":"crates/spt-daemon/src/presence.rs","count":2},{"path":"crates/spt-daemon/src/projwriter.rs","count":10},{"path":"crates/spt-daemon/src/propagate.rs","count":1},{"path":"crates/spt-daemon/src/pump/health.rs","count":1}],"truncated":true,"fileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## applyhost.rs\n  756│        );\n *757│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n  758│\n  759│        // 2. Single stamped for the OTHER OS refuses too. \"Other\" = any\n  760│        //    registered platform that is not this host — derived from the\n     │...\n  775│        );\n *776│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n  777│\n  778│        // 3. Stamped for THIS platform: the single applies (the legitimate\n  779│        //    origin-fetched-local path).\n     │...\n  787│        );\n *788│        assert_eq!(std::fs::read(&exe).unwrap(), b\"CROSS-OS BYTES\");\n  789│    }\n  790│\n  791│    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] [unit->REQ-UPD-3] the planned\n     │...\n  898│        assert_eq!(\n *899│            std::fs::read(&exe).unwrap(),\n  900│            b\"NEW BINARY BYTES\",\n  901│            \"artifact landed\"\n  902│        );\n  903│        assert_eq!(\n *904│            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n  905│            b\"OLD BINARY\",\n  906│            \"the outgoing binary stepped aside\"\n  907│        );\n     │...\n  937│        assert_eq!(\n *938│            std::fs::read(&exe).unwrap(),\n  939│            b\"NEW BINARY BYTES\",\n  940│            \"artifact landed without any broker handoff\"\n  941│        );\n  942│        assert_eq!(\n *943│            std::fs::read(swap_aside_path(&exe, 7)).unwrap(),\n  944│            b\"OLD BINARY\",\n  945│            \"the outgoing binary stepped aside\"\n  946│        );\n     │...\n  984│        );\n *985│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n  986│\n  987│        // Already-applied: idempotent no-op, never re-renames an already-swapped exe.\n  988│        // A still-open trial that already landed this version's bytes marks it applied.\n     │...\n 1002│        );\n*1003│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1004│    }\n 1005│\n 1006│    // [unit->REQ-UPD-6] apply-time re-verification handles platform-targeted\n     │...\n 1028│        );\n*1029│        assert_eq!(std::fs::read(&exe).unwrap(), b\"SET BINARY BYTES\");\n 1030│        assert_eq!(broker.session_pid(sid), child_pid, \"child pid unchanged\");\n 1031│    }\n 1032│\n     │...\n 1050│        );\n*1051│        assert_eq!(std::fs::read(&exe).unwrap(), b\"NEW WITHOUT SESSIONS\");\n 1052│        assert_eq!(broker.session_count(), 0);\n 1053│    }\n 1054│\n     │...\n 1081│        );\n*1082│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1083│        assert!(cache.staged_update().is_some(), \"candidate stays staged\");\n 1084│    }\n 1085│\n     │...\n 1112│        );\n*1113│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1114│        assert!(cache.staged().is_some(), \"artifact stays staged\");\n 1115│\n 1116│        // Tampered at rest: the apply-time re-verification rejects.\n     │...\n 1123│        );\n*1124│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1125│\n 1126│        // Unprovisioned keys: fail-closed.\n 1127│        let (cache, _) = stage(dir.path(), 10, b\"Y\", BROKER_RESOURCE_ABI);\n     │...\n 1133│        );\n*1134│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\");\n 1135│    }\n 1136│\n 1137│    // [unit->REQ-HAZARD-BROKER-PROCESS-ISOLATION] the quarantine guard (D6-2): a\n     │...\n 1164│        );\n*1165│        assert_eq!(std::fs::read(&exe).unwrap(), b\"OLD\", \"seat untouched\");\n 1166│\n 1167│        // A NEWER version (v8) is not quarantined — the marker is version-keyed.\n 1168│        let (cache8, keys8) = stage(dir.path(), 8, b\"V8 BYTES\", BROKER_RESOURCE_ABI);\n## brainproc.rs\n 382│    let exe = std::env::current_exe().ok()?;\n*383│    let bytes = std::fs::read(exe).ok()?;\n 384│    Some(crate::release::sha256_hex(&bytes))\n 385│}\n 386│\n    │...\n 418│pub fn ready_generation_at(path: &std::path::Path) -> Option<u64> {\n*419│    parse_ready_generation(&std::fs::read_to_string(path).ok()?)\n 420│}\n 421│\n 422│/// The supervisor's next backoff after a brain death: a healthy run (≥\n    │...\n 563│    fn ready_exe_hash(&self) -> Option<String> {\n*564│        let body = std::fs::read_to_string(brain_ready_path()).ok()?;\n 565│        parse_ready_exe_hash(&body)\n 566│    }\n 567│    // [impl->REQ-HAZARD-BRAIN-RESPAWN-PATH]\n## broker.rs\n 2028│fn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n*2029│    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n 2030│    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n 2031│    let decl = manifest.message_idle_translation_binary.as_ref()?;\n 2032│    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n## config.rs\n 241│    pub fn load_from(path: &Path) -> Self {\n*242│        let Ok(raw) = std::fs::read_to_string(path) else {\n 243│            return Self::default();\n 244│        };\n 245│        let file: DaemonConfigFile = match serde_json::from_str(&raw) {\n## crc_swap.rs\n  52│) -> std::io::Result<()> {\n *53│    for entry in std::fs::read_dir(dir)? {\n  54│        let entry = entry?;\n  55│        let path = entry.path();\n  56│        if path.is_dir() {\n    │...\n  64│        let target = install_dir.join(&rel);\n *65│        let differs = match std::fs::read(&target) {\n *66│            Ok(existing) => sha256_hex(&std::fs::read(&path)?) != sha256_hex(&existing),\n  67│            // Absent (or unreadable) target ⇒ a new/changed file to place.\n  68│            Err(_) => true,\n  69│        };\n    │...\n 226│    let dashed = format!(\"{base}-\");\n*227│    let Ok(entries) = std::fs::read_dir(dir) else {\n 228│        return;\n 229│    };\n 230│    for entry in entries.flatten() {\n    │...\n 303│        fn walk(dir: &Path, found: &mut bool) {\n*304│            for entry in fs::read_dir(dir).unwrap() {\n 305│                let path = entry.unwrap().path();\n 306│                if path.is_dir() {\n 307│                    walk(&path, found);\n## daemon.rs\n 1606│            while Instant::now() < deadline {\n*1607│                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n 1608│                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609│                        if p != 0 {\n 1610│                            found = Some(p);\n     │...\n 1641│        let diagpath = std::env::temp_dir().join(format!(\"spt_breakaway_diag_{}.log\", std::process::id()));\n*1642│        let diag = std::fs::read_to_string(&diagpath).unwrap_or_default();\n 1643│        let _ = std::fs::remove_file(&diagpath);\n 1644│\n 1645│        // Second skip path (sibling of the ACCESS_DENIED skip above): on some\n## deadline.rs\n 117│    fn load(path: &Path) -> Option<Self> {\n*118│        let raw = std::fs::read_to_string(path).ok()?;\n 119│        serde_json::from_str(&raw).ok()\n 120│    }\n 121│\n## deelevate.rs\n 197│    pub(super) fn elected_user_from(path: &std::path::Path) -> Option<SudoInvoker> {\n*198│        let name = std::fs::read_to_string(path).ok()?;\n 199│        user_by_name(name.trim())\n 200│    }\n 201│\n## digest.rs\n 417│fn log_less_activity(id: &str, perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*418│    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 419│        return Vec::new();\n 420│    };\n 421│    let mut items = Vec::new();\n    │...\n 462│fn read_context_items(perch_path: &std::path::Path) -> Vec<TimelineItem> {\n*463│    let Ok(s) = std::fs::read_to_string(perch_path.join(\"digest.log\")) else {\n 464│        return Vec::new();\n 465│    };\n 466│    s.lines()\n## docshost.rs\n 188│    }\n*189│    match std::fs::read(&canon_target) {\n 190│        Ok(bytes) => respond(StatusCode::OK, content_type_for(&rel), bytes),\n 191│        Err(e) => respond(\n 192│            StatusCode::INTERNAL_SERVER_ERROR,\n## effect.rs\n 930│        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n*931│        let text = std::fs::read_to_string(j.path()).expect(\"read journal file\");\n 932│        // New line shape carries the minter tag: \"PENDING 1 cli 2 net-send\".\n 933│        let net_token = format!(\"{} {} {}\", net_key.class, net_key.minter.as_tag(), net_key.op);\n 934│        let pty_token = format!(\"{} {} {}\", pty_key.class, pty_key.minter.as_tag(), pty_key.op);\n## endpoint.rs\n  98│pub fn read_pump_heartbeat() -> Option<u64> {\n *99│    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n 100│    serde_json::from_str::<u64>(s.trim()).ok()\n 101│}\n 102│\n## lifecycle.rs\n  465│        let dir = self.install_dir.as_ref()?;\n *466│        let bytes = std::fs::read(dir.join(\"manifest.toml\")).ok()?;\n  467│        Some(crate::release::sha256_hex(&bytes))\n  468│    }\n  469│\n     │...\n  486│        };\n *487│        let Ok(bytes) = std::fs::read(dir.join(\"manifest.toml\")) else {\n  488│            return;\n  489│        };\n  490│        let hash = crate::release::sha256_hex(&bytes);\n     │...\n  760│        };\n *761│        fetch_history(\n  762│            h,\n  763│            &self.base_keys(session_id),\n  764│            &self.id,\n     │...\n 1503│\n*1504│            let got = std::fs::read_to_string(&marker)\n 1505│                .expect(\"the psyche child must have spawned + written the marker\");\n 1506│            assert_eq!(\n 1507│                got.trim(),\n     │...\n 1823│        let fresh = write_psyche_context_file(&perch, first_turn_psyche_context(Some(\"MIND\"))).unwrap();\n*1824│        assert_eq!(std::fs::read_to_string(&fresh).unwrap(), \"MIND\");\n 1825│        assert!(std::fs::metadata(&fresh).unwrap().len() > 0);\n 1826│\n 1827│        // FRESH with a zero-context agent: the NON-EMPTY <fresh-psyche/> marker (never\n     │...\n 1830│        assert_eq!(marker, cont, \"overwrite in place — same nested-perch path each turn\");\n*1831│        assert_eq!(std::fs::read_to_string(&marker).unwrap(), PSYCHE_FRESH_MARKER);\n 1832│        assert!(std::fs::metadata(&marker).unwrap().len() > 0);\n 1833│    }\n 1834│\n     │...\n 2105│            let drop_path = project.path().join(\".claude\").join(\"doyle-commune.md\");\n*2106│            let body = std::fs::read_to_string(&drop_path)\n 2107│                .expect(\"commune drop written under <endpoint cwd>/.claude\");\n 2108│            // The rider: the summarizer child RAN in the endpoint cwd (its printed\n 2109│            // cwd is the project dir). Normalize separators/case for the compare.\n     │...\n 2312│            host.run_pulse_loop(None, &stop, StartReason::Cold, |_| {\n*2313│                let cur = std::fs::read(crate::deadline::anchor_path(\"pulse\"))\n 2314│                    .expect(\"anchor file exists after loop entry\");\n 2315│                let mut slot = first_bytes.lock().unwrap();\n 2316│                match &*slot {\n     │...\n 2355│            let anchor_of = || {\n*2356│                let raw = std::fs::read_to_string(crate::deadline::anchor_path(\"pulse\")).unwrap();\n 2357│                serde_json::from_str::<DeadlineAnchor>(&raw).unwrap().anchor_ms\n 2358│            };\n 2359│\n## livehost.rs\n  297│fn read_resume_pid(perch: &Path) -> Option<u32> {\n *298│    std::fs::read_to_string(perch.join(RESUME_PID_FILE))\n  299│        .ok()\n  300│        .and_then(|s| s.trim().parse::<u32>().ok())\n  301│        .filter(|&p| p != 0)\n     │...\n  918│        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n *919│        let Ok(entries) = std::fs::read_dir(&perch) else {\n  920│            continue;\n  921│        };\n  922│        for entry in entries.flatten() {\n     │...\n 1889│        );\n*1890│        let displaced: Vec<_> = std::fs::read_dir(&perch)\n 1891│            .unwrap()\n 1892│            .flatten()\n 1893│            .filter(|e| e.file_name().to_string_lossy().starts_with(\".live-bin.old\"))\n## machineid.rs\n  44│    for path in [\"/etc/machine-id\", \"/var/lib/dbus/machine-id\"] {\n *45│        if let Ok(s) = std::fs::read_to_string(path) {\n  46│            let s = s.trim().to_string();\n  47│            if !s.is_empty() {\n  48│                return Some(s);\n    │...\n 109│    let path = spt_store::perch::identity_dir().join(\"machine-id\");\n*110│    if let Ok(s) = std::fs::read_to_string(&path) {\n 111│        let s = s.trim().to_string();\n 112│        if !s.is_empty() {\n 113│            return Some(s);\n## notif.rs\n 1170│            let content = loop {\n*1171│                if let Ok(c) = std::fs::read_to_string(&marker) {\n 1172│                    if !c.trim().is_empty() {\n 1173│                        break c;\n 1174│                    }\n## presence.rs\n 59│    let mut out = BTreeMap::new();\n*60│    let Ok(entries) = std::fs::read_dir(dir) else {\n 61│        return out;\n 62│    };\n 63│    for entry in entries.flatten() {\n   │...\n 70│        };\n*71│        let Ok(raw) = std::fs::read_to_string(&path) else {\n 72│            continue;\n 73│        };\n 74│        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else {\n## projwriter.rs\n  140│pub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n *141│    let raw = std::fs::read_to_string(path).ok()?;\n  142│    serde_json::from_str(&raw).ok()\n  143│}\n  144│\n     │...\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     │...\n  416│        for (id, perch_path, origin_cwd) in &targets {\n *417│            let entries = sessions::read_all(perch_path);\n  418│            let store_ids: Vec<String> = self\n  419│                .branch_order\n  420│                .iter()\n     │...\n  559│fn enumerate_perches(owlery: &Path) -> Vec<(String, PathBuf, Option<String>)> {\n *560│    let Ok(entries) = std::fs::read_dir(owlery) else {\n  561│        return Vec::new();\n  562│    };\n  563│    let mut out = Vec::new();\n     │...\n  845│        first.reconcile(&global(), false).unwrap();\n *846│        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n  847│\n  848│        // The restarted brain: fresh engine, warm disk.\n  849│        let mut warm = WriterEngine::new(fx.paths.clone());\n     │...\n  857│        assert_eq!(\n *858│            std::fs::read(&fx.paths.index_path).unwrap(),\n  859│            bytes_before,\n  860│            \"the snapshot is byte-untouched — no rewrite, no generated_ms churn\"\n  861│        );\n     │...\n  874│        engine.reconcile(&global(), false).unwrap();\n *875│        let bytes_before = std::fs::read(&fx.paths.index_path).unwrap();\n  876│\n  877│        // Wreck the store: the seed git DIR becomes a plain FILE — it still\n  878│        // `exists()`, but opening it as a repo fails loudly (and cannot be\n     │...\n  886│        assert_eq!(\n *887│            std::fs::read(&fx.paths.index_path).unwrap(),\n  888│            bytes_before,\n  889│            \"a failed cycle never touches the published snapshot\"\n  890│        );\n     │...\n 1174│                store\n*1175│                    .branches_by_recency()\n 1176│                    .unwrap()\n 1177│                    .into_iter()\n 1178│                    .filter_map(|b| {\n     │...\n 1186│            let perch = owlery.join(id);\n*1187│            let entries = sessions::read_all(&perch);\n 1188│            let origin = info::read_info(&perch).and_then(|r| r.cwd);\n 1189│            projderive::project_refs_from(\n 1190│                &entries,\n## propagate.rs\n 473│                            drop(file.take()); // flush before reading back\n*474│                            let artifact = std::fs::read(&artifact_path).unwrap_or_default();\n 475│                            let _ = std::fs::remove_file(&artifact_path);\n 476│                            let offer = signed.take().ok_or_else(|| {\n 477│                                io::Error::other(\"peer sent Done before any Offer\")\n\n## pump/\n### health.rs\n 79│    pub fn load_from(path: &Path) -> PumpHealth {\n*80│        match std::fs::read_to_string(path) {\n 81│            Ok(s) => serde_json::from_str(&s).unwrap_or_default(),\n 82│            Err(_) => PumpHealth::default(),\n 83│        }"},"isError":false,"timestamp":1784271677441}}
{"type":"message","id":"e8a250f1","parentId":"d9198593","timestamp":"2026-07-17T07:01:17.559Z","message":{"role":"toolResult","toolCallId":"call_A3yXDIsZ9Fsikaa5BJqZNQtd|fc_0e15f89f5b7b4062016a59d33c5f008194856eabd33081b037","toolName":"read","content":[{"type":"text","text":"//! The materialized project-index WRITER — the daemon's derived-state\n//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\n//!\n//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\n//! `$SPT_HOME/index/project-index.json` (format + read path owned by\n//! `spt_store::projindex`, W1). One brain-hosted thread:\n//!\n//! - loads the persisted index at start and is READY WITHOUT WARM — the boot\n//!   reconcile runs in the background; a cold daemon never blocks on it;\n//! - reconciles with BATCHED work, `O(P + B + F + C)`: ONE branch enumeration\n//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\n//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\n//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\n//!   in a project repo touch neither the path nor `.git/config`, so they are\n//!   a NO-OP by construction — ADR-0037 §3);\n//! - publishes atomically via `projindex::write_index_at` and preserves\n//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\n//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n//!   scan;\n//! - maintains an observability sidecar (`project-index-stats.json`) — index\n//!   presence alone is not health: generated time, source generation, pending\n//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n//!   (wall-clock budgets are manual acceptance ONLY).\n// [impl->REQ-PROJECT-INDEX-WRITER]\n\nuse std::collections::{BTreeMap, BTreeSet, HashMap};\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse spt_store::branchstore::BranchStore;\nuse spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};\nuse spt_store::info;\nuse spt_store::perch;\nuse spt_store::projderive;\nuse spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\nuse spt_store::projinval::{self, Coalesced};\nuse spt_store::sessions;\n\nuse crate::brain::now_ms;\n\n/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\nconst POLL_MS: u64 = 500;\n/// Debounce window after the first observed event — a burst (an adapter\n/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\nconst DEBOUNCE_MS: u64 = 250;\n/// The low-frequency periodic FULL reconcile — the backstop that catches a\n/// lost nudge (`SPT_PROJINDEX_PERIODIC_MS` override for int rigs).\nconst PERIODIC_MS: u64 = 300_000;\n\n/// The complexity counters — the deterministic CI gate surface (gate ruling:\n/// counters gate, wall-clock is manual acceptance only).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct CycleCounters {\n    /// Branch enumerations this cycle (contract: exactly 1 when the store exists).\n    pub branch_enumerations: u64,\n    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\n    pub tree_scans: u64,\n    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\n    pub derivations: u64,\n}\n\nimpl CycleCounters {\n    fn add_to(&self, total: &mut CycleCounters) {\n        total.branch_enumerations += self.branch_enumerations;\n        total.tree_scans += self.tree_scans;\n        total.derivations += self.derivations;\n    }\n}\n\n/// The observability sidecar (`$SPT_HOME/index/project-index-stats.json`) —\n/// written atomically by the writer after every cycle; read by\n/// `spt daemon status` and by the int gates. Additive serde-default shape\n/// (derived + regenerable, like the index itself).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct IndexWriterStats {\n    /// When the current snapshot was generated (epoch ms; 0 = never).\n    #[serde(default)]\n    pub generated_ms: u64,\n    /// The published snapshot's branch-tip fingerprint.\n    #[serde(default)]\n    pub source_generation: String,\n    /// Whether a refresh is queued/underway right now.\n    #[serde(default)]\n    pub pending_refresh: bool,\n    /// When the last reconcile cycle ran (epoch ms).\n    #[serde(default)]\n    pub last_run_ms: u64,\n    /// How long it took (ms).\n    #[serde(default)]\n    pub last_duration_ms: u64,\n    /// The last cycle failure, if the most recent cycle failed (`None` = last\n    /// cycle succeeded). While set, readers are on last-known-good.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub last_error: Option<String>,\n    /// Row counts of the published snapshot.\n    #[serde(default)]\n    pub endpoints: u64,\n    #[serde(default)]\n    pub projects: u64,\n    /// Distinct cwds in the derivation cache.\n    #[serde(default)]\n    pub cwds: u64,\n    /// Cumulative cwd-cache hit/miss census.\n    #[serde(default)]\n    pub cwd_cache_hits: u64,\n    #[serde(default)]\n    pub cwd_cache_misses: u64,\n    /// Cycles that FAILED to publish — every read until the next success is a\n    /// stale (last-known-good) read by contract.\n    #[serde(default)]\n    pub stale_reads: u64,\n    /// Publishes that REPLACED an unreadable / schema-mismatched index file\n    /// (the writer repairing a torn or foreign snapshot).\n    #[serde(default)]\n    pub repairs: u64,\n    /// The most recent cycle's complexity counters (the CI-gate surface).\n    #[serde(default)]\n    pub last_cycle: CycleCounters,\n    /// Lifetime totals (this brain generation).\n    #[serde(default)]\n    pub cumulative: CycleCounters,\n}\n\n/// The stats sidecar path, beside the index inside `projindex::index_dir()`.\npub fn stats_path() -> PathBuf {\n    projindex::index_dir().join(\"project-index-stats.json\")\n}\n\n/// Read the stats sidecar (tolerant: absent/corrupt → `None`) — the\n/// `spt daemon status` consumer half.\npub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n    let raw = std::fs::read_to_string(path).ok()?;\n    serde_json::from_str(&raw).ok()\n}\n\n/// Every filesystem root the engine touches, injected so units run against\n/// plain temp dirs with zero env games.\n#[derive(Debug, Clone)]\npub struct WriterPaths {\n    pub owlery: PathBuf,\n    /// The context store's bare seed repo (`tracked/.seed.git`).\n    pub seed_git: PathBuf,\n    pub index_path: PathBuf,\n    pub stats_path: PathBuf,\n    pub invalidations_dir: PathBuf,\n}\n\nimpl WriterPaths {\n    /// The production layout under the live `$SPT_HOME`.\n    pub fn production() -> WriterPaths {\n        WriterPaths {\n            owlery: perch::owlery_dir(),\n            seed_git: contextstore::seed_git_dir(&perch::tracked_dir()),\n            index_path: projindex::index_path(),\n            stats_path: stats_path(),\n            invalidations_dir: projinval::invalidations_dir(),\n        }\n    }\n}\n\n/// One scanned branch: its tip at scan time + the endpoint ids holding\n/// `<id>/project-context.md` in that tip tree.\n#[derive(Debug, Clone)]\nstruct BranchScan {\n    tip: String,\n    members: BTreeSet<String>,\n}\n\n/// One cached cwd derivation, stamped with the repo-identity marker that\n/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n/// for a linked worktree). Identity refreshes ONLY when the marker path or\n…\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n…\n        while !stop.load(Ordering::SeqCst) {\n…\n            if has_events {\n…\n                engine.stats.pending_refresh = true;\n                engine.write_stats();\n            }\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n            if due_periodic {\n                since_periodic = Duration::ZERO;\n            }\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\n            if req.is_empty() && !due_periodic {\n                continue; // a racer consumed it (never happens in production)\n            }\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\n                engine.record_failure(e);\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_store::contextstore::ContextStore;\n    use spt_store::projinval::InvalScope;\n    use std::time::Instant;\n    use tempfile::TempDir;\n\n    /// A self-contained home-shaped fixture tree (NO env, NO real $SPT_HOME):\n    /// owlery + tracked context store + index paths, all under one TempDir.\n    struct Fixture {\n        _tmp: TempDir,\n        home: PathBuf,\n        paths: WriterPaths,\n    }\n\n    impl Fixture {\n        fn new() -> Fixture {\n            let tmp = TempDir::new().unwrap();\n            let home = tmp.path().to_path_buf();\n            let paths = WriterPaths {\n                owlery: home.join(\"owlery\"),\n                seed_git: contextstore::seed_git_dir(&home.join(\"tracked\")),\n                index_path: home.join(\"index\").join(\"project-index.json\"),\n                stats_path: home.join(\"index\").join(\"project-index-stats.json\"),\n                invalidations_dir: home.join(\"index\").join(\"invalidations\"),\n            };\n            std::fs::create_dir_all(&paths.owlery).unwrap();\n            Fixture { _tmp: tmp, home, paths }\n        }\n\n        fn store(&self) -> ContextStore {\n            ContextStore::open_or_init_in(&self.home.join(\"tracked\")).unwrap()\n        }\n\n        /// A perch with an optional origin cwd and optional session-ledger cwd.\n        fn perch(&self, id: &str, origin_cwd: Option<&Path>, session_cwd: Option<&Path>) {\n            let perch = self.paths.owlery.join(id);\n            std::fs::create_dir_all(&perch).unwrap();\n            let mut rec = info::InfoJson::new(id, \"t\", 0, &format!(\"sid-{id}\"), \"live_agent\");\n            rec.cwd = origin_cwd.map(|p| p.to_string_lossy().to_string());\n            info::write_info(&perch, &rec).unwrap();\n            if let Some(cwd) = session_cwd {\n                sessions::append(\n                    &perch,\n                    &sessions::SessionEntry {\n                        ts: \"2026-07-15T00:00:00Z\".to_string(),\n                        session_id: format!(\"s-{id}\"),\n                        trigger: sessions::SessionTrigger::Boot,\n                        cwd: Some(cwd.to_string_lossy().to_string()),\n                        ordinal: None,\n                        adapter: None,\n                    },\n                )\n                .unwrap();\n            }\n        }\n\n        /// Give `id` committed context in `project` (a p-* membership row).\n        fn membership(&self, project: &str, id: &str) {\n            let cs = self.store();\n            let file = cs.project_context_path(project, id).unwrap();\n            std::fs::write(&file, format!(\"{id} in {project}\")).unwrap();\n            cs.commit_project(project, &format!(\"{id} slice\")).unwrap().unwrap();\n        }\n\n        /// A plain (non-git) project dir whose folder name becomes the id.\n        fn project_dir(&self, name: &str) -> PathBuf {\n            let dir = self.home.join(\"workdirs\").join(name);\n            std::fs::create_dir_all(&dir).unwrap();\n            dir\n        }\n    }\n\n    fn global() -> Coalesced {\n        Coalesced { global: true, ..Coalesced::default() }\n    }\n\n    // [unit->REQ-PROJECT-INDEX-WRITER] THE complexity-counter gate (the CI gate\n    // per the pre-made ruling; wall-clock is manual acceptance only): a cold\n    // cycle over P=3 perches / B=2 project branches / C=2 distinct cwds does\n    // EXACTLY 1 branch enumeration, 2 tree scans, 2 derivations; a second full\n    // cycle over the unchanged store does 1 enumeration, 0 scans, 0 derivations\n    // (memberships cached by tip, cwds cached by identity stamp); and after ONE\n    // branch moves, exactly ONE branch rescans.\n    #[test]\n    fn batched_complexity_counters_hold() {\n        let fx = Fixture::new();\n        let dir_a = fx.project_dir(\"proj-a\");\n        let dir_b = fx.project_dir(\"proj-b\");\n        fx.perch(\"ep1\", None, Some(&dir_a)); // session-cwd + membership\n        fx.perch(\"ep2\", None, None); // membership only\n        fx.perch(\"ep3\", Some(&dir_b), None); // origin-cwd only\n        fx.membership(\"alpha\", \"ep1\");\n        fx.membership(\"alpha\", \"ep2\");\n        fx.membership(\"beta\", \"ep2\");\n\n        let mut engine = WriterEngine::new(fx.paths.clone());\n        let r1 = engine.reconcile(&global(), false).unwrap();\n        assert!(r1.published);\n        assert_eq!(\n            r1.counters,\n            CycleCounters { branch_enumerations: 1, tree_scans: 2, derivations: 2 },\n…\n        );\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! The materialized project-index WRITER — the daemon's derived-state\n//! maintenance duty (ADR-0037 §2, REQ-PROJECT-INDEX-WRITER).\n//!\n//! The daemon (brain) is the SOLE, SINGLE-FLIGHT writer of\n//! `$SPT_HOME/index/project-index.json` (format + read path owned by\n//! `spt_store::projindex`, W1). One brain-hosted thread:\n//!\n//! - loads the persisted index at start and is READY WITHOUT WARM — the boot\n//!   reconcile runs in the background; a cold daemon never blocks on it;\n//! - reconciles with BATCHED work, `O(P + B + F + C)`: ONE branch enumeration\n//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\n//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\n//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\n//!   in a project repo touch neither the path nor `.git/config`, so they are\n//!   a NO-OP by construction — ADR-0037 §3);\n//! - publishes atomically via `projindex::write_index_at` and preserves\n//!   last-known-good on ANY failure (a failed cycle changes nothing on disk);\n//! - drains + coalesces the `projinval` event queue (multiple invalidations →\n//!   one refresh) with a debounce window, keyed on the branch-tip FINGERPRINT\n//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n//!   scan;\n//! - maintains an observability sidecar (`project-index-stats.json`) — index\n//!   presence alone is not health: generated time, source generation, pending\n//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n//!   repairs, and the per-cycle complexity COUNTERS that are the CI gate\n//!   (wall-clock budgets are manual acceptance ONLY).\n// [impl->REQ-PROJECT-INDEX-WRITER]\n\nuse std::collections::{BTreeMap, BTreeSet, HashMap};\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse spt_store::branchstore::BranchStore;\nuse spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};\nuse spt_store::info;\nuse spt_store::perch;\nuse spt_store::projderive;\nuse spt_store::projindex::{self, AbsentReason, EndpointProject, IndexRead, ProjectIndex};\nuse spt_store::projinval::{self, Coalesced};\nuse spt_store::sessions;\n\nuse crate::brain::now_ms;\n\n/// Queue poll cadence (a single `read_dir` — pure file I/O). Runtime-env\n/// overridable for int rigs (`SPT_PROJINDEX_POLL_MS`), never for production.\nconst POLL_MS: u64 = 500;\n/// Debounce window after the first observed event — a burst (an adapter\n/// stampede, a multi-endpoint rename) coalesces into ONE reconcile.\nconst DEBOUNCE_MS: u64 = 250;\n/// The low-frequency periodic FULL reconcile — the backstop that catches a\n/// lost nudge (`SPT_PROJINDEX_PERIODIC_MS` override for int rigs).\nconst PERIODIC_MS: u64 = 300_000;\n\n/// The complexity counters — the deterministic CI gate surface (gate ruling:\n/// counters gate, wall-clock is manual acceptance only).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct CycleCounters {\n    /// Branch enumerations this cycle (contract: exactly 1 when the store exists).\n    pub branch_enumerations: u64,\n    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\n    pub tree_scans: u64,\n    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\n    pub derivations: u64,\n}\n\nimpl CycleCounters {\n    fn add_to(&self, total: &mut CycleCounters) {\n        total.branch_enumerations += self.branch_enumerations;\n        total.tree_scans += self.tree_scans;\n        total.derivations += self.derivations;\n    }\n}\n\n/// The observability sidecar (`$SPT_HOME/index/project-index-stats.json`) —\n/// written atomically by the writer after every cycle; read by\n/// `spt daemon status` and by the int gates. Additive serde-default shape\n/// (derived + regenerable, like the index itself).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct IndexWriterStats {\n    /// When the current snapshot was generated (epoch ms; 0 = never).\n    #[serde(default)]\n    pub generated_ms: u64,\n    /// The published snapshot's branch-tip fingerprint.\n    #[serde(default)]\n    pub source_generation: String,\n    /// Whether a refresh is queued/underway right now.\n    #[serde(default)]\n    pub pending_refresh: bool,\n    /// When the last reconcile cycle ran (epoch ms).\n    #[serde(default)]\n    pub last_run_ms: u64,\n    /// How long it took (ms).\n    #[serde(default)]\n    pub last_duration_ms: u64,\n    /// The last cycle failure, if the most recent cycle failed (`None` = last\n    /// cycle succeeded). While set, readers are on last-known-good.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub last_error: Option<String>,\n    /// Row counts of the published snapshot.\n    #[serde(default)]\n    pub endpoints: u64,\n    #[serde(default)]\n    pub projects: u64,\n    /// Distinct cwds in the derivation cache.\n    #[serde(default)]\n    pub cwds: u64,\n    /// Cumulative cwd-cache hit/miss census.\n    #[serde(default)]\n    pub cwd_cache_hits: u64,\n    #[serde(default)]\n    pub cwd_cache_misses: u64,\n    /// Cycles that FAILED to publish — every read until the next success is a\n    /// stale (last-known-good) read by contract.\n    #[serde(default)]\n    pub stale_reads: u64,\n    /// Publishes that REPLACED an unreadable / schema-mismatched index file\n    /// (the writer repairing a torn or foreign snapshot).\n    #[serde(default)]\n    pub repairs: u64,\n    /// The most recent cycle's complexity counters (the CI-gate surface).\n    #[serde(default)]\n    pub last_cycle: CycleCounters,\n    /// Lifetime totals (this brain generation).\n    #[serde(default)]\n    pub cumulative: CycleCounters,\n}\n\n/// The stats sidecar path, beside the index inside `projindex::index_dir()`.\npub fn stats_path() -> PathBuf {\n    projindex::index_dir().join(\"project-index-stats.json\")\n}\n\n/// Read the stats sidecar (tolerant: absent/corrupt → `None`) — the\n/// `spt daemon status` consumer half.\npub fn read_stats_at(path: &Path) -> Option<IndexWriterStats> {\n    let raw = std::fs::read_to_string(path).ok()?;\n    serde_json::from_str(&raw).ok()\n}\n\n/// Every filesystem root the engine touches, injected so units run against\n/// plain temp dirs with zero env games.\n#[derive(Debug, Clone)]\npub struct WriterPaths {\n    pub owlery: PathBuf,\n    /// The context store's bare seed repo (`tracked/.seed.git`).\n    pub seed_git: PathBuf,\n    pub index_path: PathBuf,\n    pub stats_path: PathBuf,\n    pub invalidations_dir: PathBuf,\n}\n\nimpl WriterPaths {\n    /// The production layout under the live `$SPT_HOME`.\n    pub fn production() -> WriterPaths {\n        WriterPaths {\n            owlery: perch::owlery_dir(),\n            seed_git: contextstore::seed_git_dir(&perch::tracked_dir()),\n            index_path: projindex::index_path(),\n            stats_path: stats_path(),\n            invalidations_dir: projinval::invalidations_dir(),\n        }\n    }\n}\n\n/// One scanned branch: its tip at scan time + the endpoint ids holding\n/// `<id>/project-context.md` in that tip tree.\n#[derive(Debug, Clone)]\nstruct BranchScan {\n    tip: String,\n    members: BTreeSet<String>,\n}\n\n/// One cached cwd derivation, stamped with the repo-identity marker that\n/// decides its validity: the discovered `.git/config` (or the `.git` gitfile\n/// for a linked worktree). Identity refreshes ONLY when the marker path or\n…\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n…\n        while !stop.load(Ordering::SeqCst) {\n…\n            if has_events {\n…\n                engine.stats.pending_refresh = true;\n                engine.write_stats();\n            }\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n            if due_periodic {\n                since_periodic = Duration::ZERO;\n            }\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\n            if req.is_empty() && !due_periodic {\n                continue; // a racer consumed it (never happens in production)\n            }\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\n                engine.record_failure(e);\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_store::contextstore::ContextStore;\n    use spt_store::projinval::InvalScope;\n    use std::time::Instant;\n    use tempfile::TempDir;\n\n    /// A self-contained home-shaped fixture tree (NO env, NO real $SPT_HOME):\n    /// owlery + tracked context store + index paths, all under one TempDir.\n    struct Fixture {\n        _tmp: TempDir,\n        home: PathBuf,\n        paths: WriterPaths,\n    }\n\n    impl Fixture {\n        fn new() -> Fixture {\n            let tmp = TempDir::new().unwrap();\n            let home = tmp.path().to_path_buf();\n            let paths = WriterPaths {\n                owlery: home.join(\"owlery\"),\n                seed_git: contextstore::seed_git_dir(&home.join(\"tracked\")),\n                index_path: home.join(\"index\").join(\"project-index.json\"),\n                stats_path: home.join(\"index\").join(\"project-index-stats.json\"),\n                invalidations_dir: home.join(\"index\").join(\"invalidations\"),\n            };\n            std::fs::create_dir_all(&paths.owlery).unwrap();\n            Fixture { _tmp: tmp, home, paths }\n        }\n\n        fn store(&self) -> ContextStore {\n            ContextStore::open_or_init_in(&self.home.join(\"tracked\")).unwrap()\n        }\n\n        /// A perch with an optional origin cwd and optional session-ledger cwd.\n        fn perch(&self, id: &str, origin_cwd: Option<&Path>, session_cwd: Option<&Path>) {\n            let perch = self.paths.owlery.join(id);\n            std::fs::create_dir_all(&perch).unwrap();\n            let mut rec = info::InfoJson::new(id, \"t\", 0, &format!(\"sid-{id}\"), \"live_agent\");\n            rec.cwd = origin_cwd.map(|p| p.to_string_lossy().to_string());\n            info::write_info(&perch, &rec).unwrap();\n            if let Some(cwd) = session_cwd {\n                sessions::append(\n                    &perch,\n                    &sessions::SessionEntry {\n                        ts: \"2026-07-15T00:00:00Z\".to_string(),\n                        session_id: format!(\"s-{id}\"),\n                        trigger: sessions::SessionTrigger::Boot,\n                        cwd: Some(cwd.to_string_lossy().to_string()),\n                        ordinal: None,\n                        adapter: None,\n                    },\n                )\n                .unwrap();\n            }\n        }\n\n        /// Give `id` committed context in `project` (a p-* membership row).\n        fn membership(&self, project: &str, id: &str) {\n            let cs = self.store();\n            let file = cs.project_context_path(project, id).unwrap();\n            std::fs::write(&file, format!(\"{id} in {project}\")).unwrap();\n            cs.commit_project(project, &format!(\"{id} slice\")).unwrap().unwrap();\n        }\n\n        /// A plain (non-git) project dir whose folder name becomes the id.\n        fn project_dir(&self, name: &str) -> PathBuf {\n            let dir = self.home.join(\"workdirs\").join(name);\n            std::fs::create_dir_all(&dir).unwrap();\n            dir\n        }\n    }\n\n    fn global() -> Coalesced {\n        Coalesced { global: true, ..Coalesced::default() }\n    }\n\n    // [unit->REQ-PROJECT-INDEX-WRITER] THE complexity-counter gate (the CI gate\n    // per the pre-made ruling; wall-clock is manual acceptance only): a cold\n    // cycle over P=3 perches / B=2 project branches / C=2 distinct cwds does\n    // EXACTLY 1 branch enumeration, 2 tree scans, 2 derivations; a second full\n    // cycle over the unchanged store does 1 enumeration, 0 scans, 0 derivations\n    // (memberships cached by tip, cwds cached by identity stamp); and after ONE\n    // branch moves, exactly ONE branch rescans.\n    #[test]\n    fn batched_complexity_counters_hold() {\n        let fx = Fixture::new();\n        let dir_a = fx.project_dir(\"proj-a\");\n        let dir_b = fx.project_dir(\"proj-b\");\n        fx.perch(\"ep1\", None, Some(&dir_a)); // session-cwd + membership\n        fx.perch(\"ep2\", None, None); // membership only\n        fx.perch(\"ep3\", Some(&dir_b), None); // origin-cwd only\n        fx.membership(\"alpha\", \"ep1\");\n        fx.membership(\"alpha\", \"ep2\");\n        fx.membership(\"beta\", \"ep2\");\n\n        let mut engine = WriterEngine::new(fx.paths.clone());\n        let r1 = engine.reconcile(&global(), false).unwrap();\n        assert!(r1.published);\n        assert_eq!(\n            r1.counters,\n            CycleCounters { branch_enumerations: 1, tree_scans: 2, derivations: 2 },\n…\n        );\n…\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,null,622,623,null,646,null,665,null,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,null,792,null,833,null,1269]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\projwriter.rs"}}},"isError":false,"timestamp":1784271677559}}
{"type":"message","id":"ef9535da","parentId":"e8a250f1","timestamp":"2026-07-17T07:01:24.478Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Requesting missing code lines and event check**"},{"type":"toolCall","id":"call_EiN7XZJgiV4mcc3EKzeGxnST|fc_0e15f89f5b7b4062016a59d343bed88194b28b435ea69132b3","name":"read","arguments":{"i":"Reading full index poll body","path":"crates/spt-daemon/src/projwriter.rs","selector":"610-690"}},{"type":"toolCall","id":"call_eODamssMjFrg74Wb4MQBf7cQ|fc_0e15f89f5b7b4062016a59d343bef481948bc86363e1253bac","name":"read","arguments":{"i":"Reading lifecycle tick body","path":"crates/spt-daemon/src/lifecycle.rs","selector":"690-790"}},{"type":"toolCall","id":"call_St4sOaQTQpCl0RFmsdqlbOdp|fc_0e15f89f5b7b4062016a59d343befc81948960a26677441d40","name":"read","arguments":{"i":"Reading live reconcile scans","path":"crates/spt-daemon/src/livehost.rs","selector":"900-1060"}},{"type":"toolCall","id":"call_s2b0l0zR5KAdFSG1eZSffN3r|fc_0e15f89f5b7b4062016a59d343bf04819483599ec460e7c263","name":"read","arguments":{"i":"Reading daemon config defaults","path":"crates/spt-daemon/src/config.rs","selector":"1-125"}},{"type":"toolCall","id":"call_g141o2nxc61pn4lGpcwkWAEx|fc_0e15f89f5b7b4062016a59d343bf0c8194a9f8f978e530fbc8","name":"read","arguments":{"i":"Reading pump round body","path":"crates/spt-daemon/src/pump/mod.rs","selector":"500-690"}},{"type":"toolCall","id":"call_6JiMKZb0K1vtXhtvpBF9ogqO|fc_0e15f89f5b7b4062016a59d343bf14819483b9b20955191787","name":"read","arguments":{"i":"Reading registry pre-round","path":"crates/spt-daemon/src/pump/registry.rs","selector":"108-180"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":20511,"output":258,"cacheRead":59904,"cacheWrite":0,"totalTokens":80673,"reasoningTokens":21,"cost":{"input":0.10255500000000001,"output":0.00774,"cacheRead":0.029952,"cacheWrite":0,"total":0.140247}},"stopReason":"toolUse","timestamp":1784271677672,"responseId":"resp_0e15f89f5b7b4062016a59d33df18481949e9c528d63d0dcaf","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d33ef8e08194b1cfc4ade8d929ec","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdNDPlOxcUQpGGG6mav173PygCXsckTc3B8RS1uqeEieZKf2RQfacnVIhRM9Dn92icM9PzhvyHjFwEF7drVCzzEhqtxDup0kbqTKrUPr1t3bkr3HeKw8GoNDeZvVb0u0YyCoL9-K87xElej8OnRfvllEc_mdB84dldPWh0AvC2GyC8FM5ig0W2cAchgsNz3S3_SKy0Jj6i6ku4lDnUK4cL8U4IjzhIuG5hvxUe_w-waLUx0JbfmFKVbLSkpYiWA1sEsKBjBOE0PqhBd7K34hGsPNfwrND6lrflOW7ilV7oP1tRMMdeth1bxt7K3dGSn5398tzIzta1gIPwZmisfsO360ftVJU0DoDVPJTn7GMe9pxrSF-MVJfgmIj1r-PYXxhrcAEBf0PMwpd7f9ecLglJBeoRXrXG--f0ZYOsbVidkGXV3T_J86SiwbRgW89rAMTJmwBhuH2QBIWcVQCrq_eDs8bJ30ydjiFomawsXTq5frO38_1Ga8qPgljZphLXBjQ3CbebMz27wJcxjPezFQ9qHysLutXjOGiyZ4wXI5DMAJ1gSyOVLA31PZvKjXiLYsjuNfzfHNrSgDS0xrZmqLW379YS2f53l1bbROqaUJ_DqvjgBr1mnKQ6jLH8R8lrAjvOwgyKZNC96Sd68D9G5WQGlte1STW5YICCQLHAWFwEBl_HMP8rdXVIzB_u6A72TmU7P6fN9a_nlfT3iCOljHh--olX0BAm3OMM3ZTL5feQgbDxWDd8vfRZ3NZtc8ryk6JhYE84Xbyu0_Zs_FY9SVOCgATSVxnosqV2L8TXR8GqqI5NIsEQ4mUkWkA-1C_6YjUdaAb142f2FEJ0Tw7VWWiFDAQR8mISCM5umdtfe9Ci0llyu31QfXuwttTChpQyOzujd3rR-k8QAACJ11Y-mkG66EwF4ttuvhZ-_ksT6mwP2bOSLkPQlQzbxyS_r79UUnggKwNI8jtponsSnZPlUUYV9AQtcU2kaij7mbl2LeVIwnTAlleNU-FW6OunADr37n9KjZ","summary":[{"type":"summary_text","text":"**Requesting missing code lines and event check**"}]},{"id":"fc_0e15f89f5b7b4062016a59d343bed88194b28b435ea69132b3","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading full index poll body\",\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"selector\":\"610-690\"}","call_id":"call_EiN7XZJgiV4mcc3EKzeGxnST","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d343bef481948bc86363e1253bac","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading lifecycle tick body\",\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"selector\":\"690-790\"}","call_id":"call_eODamssMjFrg74Wb4MQBf7cQ","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d343befc81948960a26677441d40","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading live reconcile scans\",\"path\":\"crates/spt-daemon/src/livehost.rs\",\"selector\":\"900-1060\"}","call_id":"call_St4sOaQTQpCl0RFmsdqlbOdp","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d343bf04819483599ec460e7c263","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon config defaults\",\"path\":\"crates/spt-daemon/src/config.rs\",\"selector\":\"1-125\"}","call_id":"call_s2b0l0zR5KAdFSG1eZSffN3r","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d343bf0c8194a9f8f978e530fbc8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading pump round body\",\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"selector\":\"500-690\"}","call_id":"call_g141o2nxc61pn4lGpcwkWAEx","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d343bf14819483b9b20955191787","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry pre-round\",\"path\":\"crates/spt-daemon/src/pump/registry.rs\",\"selector\":\"108-180\"}","call_id":"call_6JiMKZb0K1vtXhtvpBF9ogqO","name":"read"}]},"duration":6804.80490000546,"ttft":1316.943599998951,"contextSnapshot":{"promptTokens":80415,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_EiN7XZJgiV4mcc3EKzeGxnST|fc_0e15f89f5b7b4062016a59d343bed88194b28b435ea69132b3","toolName":"read","startedAt":"2026-07-17T07:01:24.479Z","args":{"path":"crates/spt-daemon/src/projwriter.rs"},"intent":"Reading full index poll body"},"id":"add9976c","parentId":"ef9535da","timestamp":"2026-07-17T07:01:24.479Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eODamssMjFrg74Wb4MQBf7cQ|fc_0e15f89f5b7b4062016a59d343bef481948bc86363e1253bac","toolName":"read","startedAt":"2026-07-17T07:01:24.479Z","args":{"path":"crates/spt-daemon/src/lifecycle.rs"},"intent":"Reading lifecycle tick body"},"id":"0e738a1f","parentId":"add9976c","timestamp":"2026-07-17T07:01:24.479Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_St4sOaQTQpCl0RFmsdqlbOdp|fc_0e15f89f5b7b4062016a59d343befc81948960a26677441d40","toolName":"read","startedAt":"2026-07-17T07:01:24.479Z","args":{"path":"crates/spt-daemon/src/livehost.rs"},"intent":"Reading live reconcile scans"},"id":"f16b9e5b","parentId":"0e738a1f","timestamp":"2026-07-17T07:01:24.479Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_s2b0l0zR5KAdFSG1eZSffN3r|fc_0e15f89f5b7b4062016a59d343bf04819483599ec460e7c263","toolName":"read","startedAt":"2026-07-17T07:01:24.479Z","args":{"path":"crates/spt-daemon/src/config.rs"},"intent":"Reading daemon config defaults"},"id":"8259c8e1","parentId":"f16b9e5b","timestamp":"2026-07-17T07:01:24.479Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_g141o2nxc61pn4lGpcwkWAEx|fc_0e15f89f5b7b4062016a59d343bf0c8194a9f8f978e530fbc8","toolName":"read","startedAt":"2026-07-17T07:01:24.479Z","args":{"path":"crates/spt-daemon/src/pump/mod.rs"},"intent":"Reading pump round body"},"id":"1eb3a119","parentId":"8259c8e1","timestamp":"2026-07-17T07:01:24.479Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6JiMKZb0K1vtXhtvpBF9ogqO|fc_0e15f89f5b7b4062016a59d343bf14819483b9b20955191787","toolName":"read","startedAt":"2026-07-17T07:01:24.479Z","args":{"path":"crates/spt-daemon/src/pump/registry.rs"},"intent":"Reading registry pre-round"},"id":"0184f4e9","parentId":"1eb3a119","timestamp":"2026-07-17T07:01:24.479Z"}
{"type":"message","id":"e62479e1","parentId":"0184f4e9","timestamp":"2026-07-17T07:01:24.491Z","message":{"role":"toolResult","toolCallId":"call_6JiMKZb0K1vtXhtvpBF9ogqO|fc_0e15f89f5b7b4062016a59d343bf14819483b9b20955191787","toolName":"read","content":[{"type":"text","text":"\nimpl PumpWorker for RegistryWorker {\n    fn cadence(&self) -> Duration {\n        self.cadence\n    }\n\n    /// Event-driven advertisement (M8 decision 15, REQ-CONV-2): an endpoint\n    /// online/offline transition dropped the marker — consume it and run the\n    /// registry leg NOW. A wake of the pump loop, not a second advertisement\n    /// path: the forced round is the same advertise_local + push code, so the\n    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\n    /// so one wake claims exactly one forced round.\n    // [impl->REQ-CONV-2]\n    fn poll_wake(&mut self) -> bool {\n        crate::registryhost::take_advertise_now(&self.advertise_marker)\n    }\n\n    /// The node-local sweep, run ONCE per due round before any `peer_step`\n    /// (§Frozen-7, order preserved verbatim):\n    fn pre_round(&mut self, _ctx: &RoundCtx) {\n        // Consume the re-pair eviction marker FIRST (M8 decision 13,\n        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\n        // tick's snapshot mirror can resurrect the pairing host's disk-side\n        // eviction.\n        // [impl->REQ-SUBNET-7]\n        self.registry.consume_repair_evictions();\n        // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-HAZARDS\n        // 4.10): a dead node identity's rows decay instead of poisoning bare-id\n        // resolution forever; a live peer re-stamps its heard entry with every\n        // admitted feed and is never touched.\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n        self.registry.evict_silent_peers(self.evict_after);\n        // Trigger (b) — per-row Offline-TTL (#2-secondary): the SAME sweep also\n        // evicts rows that have been non-routable (Offline) beyond the grace even\n        // while their author node is alive (a purge/erase Offline ghost that\n        // ghost-heal re-advertised once and whole-node eviction never reaps). Same\n        // registry_evict_after grace; own rows never decay; the receiver-observed\n        // offline_since is self-pruning so a whole-node eviction above drops its\n        // stamps for free.\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\n        self.registry.evict_aged_offline_rows(self.evict_after);\n        // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window has\n        // closed — after the eviction sweep, before re-advertising. The\n        // re-minted seed rides the member control stream on the next connect;\n        // the revokee's tombstone (already propagated) keeps it locked out.\n        fire_due_rotations(&self.subnets, &self.rotations, now_ms());\n        // Re-advertise this node's own rows; held as worker state for the\n        // per-subnet `peer_step` filter below.\n        self.adverts = self.registry.advertise_local(&self.owlery);\n    }\n\n    /// Push this node's own rows for `subnet` over the peer's feed stream\n    /// (per-subnet filter of the `pre_round` adverts). Fire-and-forget, one\n    /// stream per feed.\n    fn peer_step(\n        &mut self,\n        io: &mut PeerIo,\n        subnet: &str,\n        _peer_hex: &str,\n        _ctx: &RoundCtx,\n    ) -> io::Result<()> {\n        let lines: Vec<u8> = self\n            .adverts\n            .iter()\n            .filter(|u| u.subnet() == subnet)\n            .flat_map(|u| u.encode_line())\n            .collect();\n        if !lines.is_empty() {\n            io.push_feed(&lines)?;\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n…\n}\n\n[Showing lines 107-183 of 334. Use :184 to continue]"}],"details":{"truncation":{"content":"\r\nimpl PumpWorker for RegistryWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    /// Event-driven advertisement (M8 decision 15, REQ-CONV-2): an endpoint\r\n    /// online/offline transition dropped the marker — consume it and run the\r\n    /// registry leg NOW. A wake of the pump loop, not a second advertisement\r\n    /// path: the forced round is the same advertise_local + push code, so the\r\n    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\r\n    /// so one wake claims exactly one forced round.\r\n    // [impl->REQ-CONV-2]\r\n    fn poll_wake(&mut self) -> bool {\r\n        crate::registryhost::take_advertise_now(&self.advertise_marker)\r\n    }\r\n\r\n    /// The node-local sweep, run ONCE per due round before any `peer_step`\r\n    /// (§Frozen-7, order preserved verbatim):\r\n    fn pre_round(&mut self, _ctx: &RoundCtx) {\r\n        // Consume the re-pair eviction marker FIRST (M8 decision 13,\r\n        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\r\n        // tick's snapshot mirror can resurrect the pairing host's disk-side\r\n        // eviction.\r\n        // [impl->REQ-SUBNET-7]\r\n        self.registry.consume_repair_evictions();\r\n        // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-HAZARDS\r\n        // 4.10): a dead node identity's rows decay instead of poisoning bare-id\r\n        // resolution forever; a live peer re-stamps its heard entry with every\r\n        // admitted feed and is never touched.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_silent_peers(self.evict_after);\r\n        // Trigger (b) — per-row Offline-TTL (#2-secondary): the SAME sweep also\r\n        // evicts rows that have been non-routable (Offline) beyond the grace even\r\n        // while their author node is alive (a purge/erase Offline ghost that\r\n        // ghost-heal re-advertised once and whole-node eviction never reaps). Same\r\n        // registry_evict_after grace; own rows never decay; the receiver-observed\r\n        // offline_since is self-pruning so a whole-node eviction above drops its\r\n        // stamps for free.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_aged_offline_rows(self.evict_after);\r\n        // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window has\r\n        // closed — after the eviction sweep, before re-advertising. The\r\n        // re-minted seed rides the member control stream on the next connect;\r\n        // the revokee's tombstone (already propagated) keeps it locked out.\r\n        fire_due_rotations(&self.subnets, &self.rotations, now_ms());\r\n        // Re-advertise this node's own rows; held as worker state for the\r\n        // per-subnet `peer_step` filter below.\r\n        self.adverts = self.registry.advertise_local(&self.owlery);\r\n    }\r\n\r\n    /// Push this node's own rows for `subnet` over the peer's feed stream\r\n    /// (per-subnet filter of the `pre_round` adverts). Fire-and-forget, one\r\n    /// stream per feed.\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        _peer_hex: &str,\r\n        _ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        let lines: Vec<u8> = self\r\n            .adverts\r\n            .iter()\r\n            .filter(|u| u.subnet() == subnet)\r\n            .flat_map(|u| u.encode_line())\r\n            .collect();\r\n        if !lines.is_empty() {\r\n            io.push_feed(&lines)?;\r\n        }\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r","truncated":true,"truncatedBy":"lines","totalLines":228,"totalBytes":3548,"outputLines":77,"outputBytes":3548,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\nimpl PumpWorker for RegistryWorker {\r\n    fn cadence(&self) -> Duration {\r\n        self.cadence\r\n    }\r\n\r\n    /// Event-driven advertisement (M8 decision 15, REQ-CONV-2): an endpoint\r\n    /// online/offline transition dropped the marker — consume it and run the\r\n    /// registry leg NOW. A wake of the pump loop, not a second advertisement\r\n    /// path: the forced round is the same advertise_local + push code, so the\r\n    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\r\n    /// so one wake claims exactly one forced round.\r\n    // [impl->REQ-CONV-2]\r\n    fn poll_wake(&mut self) -> bool {\r\n        crate::registryhost::take_advertise_now(&self.advertise_marker)\r\n    }\r\n\r\n    /// The node-local sweep, run ONCE per due round before any `peer_step`\r\n    /// (§Frozen-7, order preserved verbatim):\r\n    fn pre_round(&mut self, _ctx: &RoundCtx) {\r\n        // Consume the re-pair eviction marker FIRST (M8 decision 13,\r\n        // REQ-SUBNET-7): a superseded identity's in-memory rows go before this\r\n        // tick's snapshot mirror can resurrect the pairing host's disk-side\r\n        // eviction.\r\n        // [impl->REQ-SUBNET-7]\r\n        self.registry.consume_repair_evictions();\r\n        // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-HAZARDS\r\n        // 4.10): a dead node identity's rows decay instead of poisoning bare-id\r\n        // resolution forever; a live peer re-stamps its heard entry with every\r\n        // admitted feed and is never touched.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_silent_peers(self.evict_after);\r\n        // Trigger (b) — per-row Offline-TTL (#2-secondary): the SAME sweep also\r\n        // evicts rows that have been non-routable (Offline) beyond the grace even\r\n        // while their author node is alive (a purge/erase Offline ghost that\r\n        // ghost-heal re-advertised once and whole-node eviction never reaps). Same\r\n        // registry_evict_after grace; own rows never decay; the receiver-observed\r\n        // offline_since is self-pruning so a whole-node eviction above drops its\r\n        // stamps for free.\r\n        // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]\r\n        self.registry.evict_aged_offline_rows(self.evict_after);\r\n        // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window has\r\n        // closed — after the eviction sweep, before re-advertising. The\r\n        // re-minted seed rides the member control stream on the next connect;\r\n        // the revokee's tombstone (already propagated) keeps it locked out.\r\n        fire_due_rotations(&self.subnets, &self.rotations, now_ms());\r\n        // Re-advertise this node's own rows; held as worker state for the\r\n        // per-subnet `peer_step` filter below.\r\n        self.adverts = self.registry.advertise_local(&self.owlery);\r\n    }\r\n\r\n    /// Push this node's own rows for `subnet` over the peer's feed stream\r\n    /// (per-subnet filter of the `pre_round` adverts). Fire-and-forget, one\r\n    /// stream per feed.\r\n    fn peer_step(\r\n        &mut self,\r\n        io: &mut PeerIo,\r\n        subnet: &str,\r\n        _peer_hex: &str,\r\n        _ctx: &RoundCtx,\r\n    ) -> io::Result<()> {\r\n        let lines: Vec<u8> = self\r\n            .adverts\r\n            .iter()\r\n            .filter(|u| u.subnet() == subnet)\r\n            .flat_map(|u| u.encode_line())\r\n            .collect();\r\n        if !lines.is_empty() {\r\n            io.push_feed(&lines)?;\r\n        }\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n…\n}","startLine":107,"lineNumbers":[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,null,333]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\registry.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":334,"totalBytes":3548,"outputLines":77,"outputBytes":3548,"shownRange":{"start":107,"end":183},"nextOffset":184}}},"isError":false,"timestamp":1784271684491}}
{"type":"message","id":"e6a617a3","parentId":"e62479e1","timestamp":"2026-07-17T07:01:24.499Z","message":{"role":"toolResult","toolCallId":"call_s2b0l0zR5KAdFSG1eZSffN3r|fc_0e15f89f5b7b4062016a59d343bf04819483599ec460e7c263","toolName":"read","content":[{"type":"text","text":"//! Durable daemon configuration (B3) — the configurable pulse period that\n//! replaces the interim 5s constant.\n//!\n//! In the no-daemon interim model the pulse cadence was a hard-coded\n//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n//! period is **durable and configurable**, sourced from a small JSON file under\n//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n//! from config, never a constant.\n// [impl->REQ-DAEMON-1]\n\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\n/// The interim default pulse period carried forward as the config default. The\n/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may\n/// override it per machine.\npub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n\n/// Default drop-file precedence protection window (epoch-ms), mirroring the\n/// interim `spt_live` guard. Surfaced here so the one config source owns it.\npub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\n\n/// Default orphan-watch grace window (B7): how long the supervised-crash teardown\n/// waits after observing a hosted Self's harness owner gone before it rechecks\n/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\n/// — a binary handoff relaunch or a brief stale poll — without delaying a real\n/// teardown for long. Configurable per machine via `crash_grace_ms`.\npub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\n\n/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n/// registry/notif feeds to trusted peers and pulls context syncs from them.\n/// Conservative defaults — every loop is a thin call over an idempotent\n/// driver, so a missed tick costs only latency, never correctness.\npub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\npub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\npub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\npub const DEFAULT_UPDATE_CHECK_PERIOD: Duration = Duration::from_secs(3_600);\n\n/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n/// authored by a node not heard within this window are evicted on the next\n/// registry pump tick. Sized to ~10 default pump cadences — generous against\n/// transient drops (a peer re-advertises every cadence, so one admitted feed\n/// inside the window keeps its rows), short enough that a dead node identity\n/// stops poisoning bare-id resolution within minutes.\npub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n\n/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n/// `Daemon::run`; kept as a local enum so the config layer stays transport-free.\n/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL\n/// list and gets its config plumbing when a self-host consumer exists.\n// [impl->REQ-NET-2]\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum RelayChoice {\n    /// n0's public relays — the zero-config default (`\"relay\": \"n0\"`).\n    N0,\n    /// No relays: direct/LAN only (`\"relay\": \"disabled\"`).\n    Disabled,\n}\n\n/// One saved `endpoint run --save` startup default (F-038 W5,\n/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\n/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\n/// design: the adapter option is re-resolved fresh at each replay (an adapter\n/// update between restarts is honored, an uninstalled one loud-skips), and the\n/// endpoint's home subnet is NOT recorded (home is durable on the perch —\n/// REQ-INST-15). A replay is always a FRESH session (shape (c)\n/// restore-what-was-up was rejected — this is a startup default, not a\n/// session restore).\n// [impl->REQ-ENDPOINT-AUTOSTART]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct StartupEndpoint {\n    /// The endpoint id to bring up.\n    pub id: String,\n    /// The adapter option (`<adapter>[:profile]`), resolved at replay time.\n    pub adapter: String,\n    /// The project cwd the harness PTY lands in (captured at `--save`).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub cwd: Option<String>,\n}\n\n/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a\n/// partial file still loads (forward/back-compat) — a missing field takes the\n/// default. Stored as plain integers (ms) to keep the file human-editable.\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\nstruct DaemonConfigFile {\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pulse_period_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    protection_window_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    crash_grace_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    full_auto_update: Option<bool>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    relay: Option<String>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    peer_pump: Option<bool>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    registry_pump_period_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    registry_evict_after_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    notif_pump_period_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    sync_pull_period_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    update_check_period_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    auto_suspend_after_ms: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    resources_blurb: Option<String>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    detached_subnets: Option<Vec<String>>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    worker_reap_ttl_secs: Option<u64>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    startup_endpoints: Option<Vec<StartupEndpoint>>,\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    docs_port: Option<u16>,\n}\n\n/// Resolved daemon configuration. The brain's scheduled loops read their cadence\n\n[Showing lines 1-128 of 652. Use :129 to continue]"}],"details":{"truncation":{"content":"//! Durable daemon configuration (B3) — the configurable pulse period that\r\n//! replaces the interim 5s constant.\r\n//!\r\n//! In the no-daemon interim model the pulse cadence was a hard-coded\r\n//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\r\n//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\r\n//! period is **durable and configurable**, sourced from a small JSON file under\r\n//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\r\n//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\r\n//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\r\n//! from config, never a constant.\r\n// [impl->REQ-DAEMON-1]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::time::Duration;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\n/// The interim default pulse period carried forward as the config default. The\r\n/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may\r\n/// override it per machine.\r\npub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\r\n\r\n/// Default drop-file precedence protection window (epoch-ms), mirroring the\r\n/// interim `spt_live` guard. Surfaced here so the one config source owns it.\r\npub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\r\n\r\n/// Default orphan-watch grace window (B7): how long the supervised-crash teardown\r\n/// waits after observing a hosted Self's harness owner gone before it rechecks\r\n/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\r\n/// — a binary handoff relaunch or a brief stale poll — without delaying a real\r\n/// teardown for long. Configurable per machine via `crash_grace_ms`.\r\npub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\r\n\r\n/// Default peer-pump cadences (D9-1): how often the daemon pushes its\r\n/// registry/notif feeds to trusted peers and pulls context syncs from them.\r\n/// Conservative defaults — every loop is a thin call over an idempotent\r\n/// driver, so a missed tick costs only latency, never correctness.\r\npub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\r\npub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\r\npub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\r\npub const DEFAULT_UPDATE_CHECK_PERIOD: Duration = Duration::from_secs(3_600);\r\n\r\n/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\r\n/// authored by a node not heard within this window are evicted on the next\r\n/// registry pump tick. Sized to ~10 default pump cadences — generous against\r\n/// transient drops (a peer re-advertises every cadence, so one admitted feed\r\n/// inside the window keeps its rows), short enough that a dead node identity\r\n/// stops poisoning bare-id resolution within minutes.\r\npub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\r\n\r\n/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\r\n/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\r\n/// `Daemon::run`; kept as a local enum so the config layer stays transport-free.\r\n/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL\r\n/// list and gets its config plumbing when a self-host consumer exists.\r\n// [impl->REQ-NET-2]\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum RelayChoice {\r\n    /// n0's public relays — the zero-config default (`\"relay\": \"n0\"`).\r\n    N0,\r\n    /// No relays: direct/LAN only (`\"relay\": \"disabled\"`).\r\n    Disabled,\r\n}\r\n\r\n/// One saved `endpoint run --save` startup default (F-038 W5,\r\n/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\r\n/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\r\n/// design: the adapter option is re-resolved fresh at each replay (an adapter\r\n/// update between restarts is honored, an uninstalled one loud-skips), and the\r\n/// endpoint's home subnet is NOT recorded (home is durable on the perch —\r\n/// REQ-INST-15). A replay is always a FRESH session (shape (c)\r\n/// restore-what-was-up was rejected — this is a startup default, not a\r\n/// session restore).\r\n// [impl->REQ-ENDPOINT-AUTOSTART]\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct StartupEndpoint {\r\n    /// The endpoint id to bring up.\r\n    pub id: String,\r\n    /// The adapter option (`<adapter>[:profile]`), resolved at replay time.\r\n    pub adapter: String,\r\n    /// The project cwd the harness PTY lands in (captured at `--save`).\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n}\r\n\r\n/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a\r\n/// partial file still loads (forward/back-compat) — a missing field takes the\r\n/// default. Stored as plain integers (ms) to keep the file human-editable.\r\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\r\nstruct DaemonConfigFile {\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pulse_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    protection_window_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    crash_grace_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    full_auto_update: Option<bool>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    relay: Option<String>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    peer_pump: Option<bool>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    registry_pump_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    registry_evict_after_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    notif_pump_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    sync_pull_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    update_check_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    auto_suspend_after_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    resources_blurb: Option<String>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    detached_subnets: Option<Vec<String>>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    worker_reap_ttl_secs: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    startup_endpoints: Option<Vec<StartupEndpoint>>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    docs_port: Option<u16>,\r\n}\r\n\r\n/// Resolved daemon configuration. The brain's scheduled loops read their cadence\r","truncated":true,"truncatedBy":"lines","totalLines":652,"totalBytes":6825,"outputLines":128,"outputBytes":6825,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Durable daemon configuration (B3) — the configurable pulse period that\r\n//! replaces the interim 5s constant.\r\n//!\r\n//! In the no-daemon interim model the pulse cadence was a hard-coded\r\n//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\r\n//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\r\n//! period is **durable and configurable**, sourced from a small JSON file under\r\n//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\r\n//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\r\n//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\r\n//! from config, never a constant.\r\n// [impl->REQ-DAEMON-1]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::time::Duration;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\n/// The interim default pulse period carried forward as the config default. The\r\n/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may\r\n/// override it per machine.\r\npub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\r\n\r\n/// Default drop-file precedence protection window (epoch-ms), mirroring the\r\n/// interim `spt_live` guard. Surfaced here so the one config source owns it.\r\npub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\r\n\r\n/// Default orphan-watch grace window (B7): how long the supervised-crash teardown\r\n/// waits after observing a hosted Self's harness owner gone before it rechecks\r\n/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery\r\n/// — a binary handoff relaunch or a brief stale poll — without delaying a real\r\n/// teardown for long. Configurable per machine via `crash_grace_ms`.\r\npub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);\r\n\r\n/// Default peer-pump cadences (D9-1): how often the daemon pushes its\r\n/// registry/notif feeds to trusted peers and pulls context syncs from them.\r\n/// Conservative defaults — every loop is a thin call over an idempotent\r\n/// driver, so a missed tick costs only latency, never correctness.\r\npub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\r\npub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\r\npub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\r\npub const DEFAULT_UPDATE_CHECK_PERIOD: Duration = Duration::from_secs(3_600);\r\n\r\n/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\r\n/// authored by a node not heard within this window are evicted on the next\r\n/// registry pump tick. Sized to ~10 default pump cadences — generous against\r\n/// transient drops (a peer re-advertises every cadence, so one admitted feed\r\n/// inside the window keeps its rows), short enough that a dead node identity\r\n/// stops poisoning bare-id resolution within minutes.\r\npub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\r\n\r\n/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\r\n/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\r\n/// `Daemon::run`; kept as a local enum so the config layer stays transport-free.\r\n/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL\r\n/// list and gets its config plumbing when a self-host consumer exists.\r\n// [impl->REQ-NET-2]\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum RelayChoice {\r\n    /// n0's public relays — the zero-config default (`\"relay\": \"n0\"`).\r\n    N0,\r\n    /// No relays: direct/LAN only (`\"relay\": \"disabled\"`).\r\n    Disabled,\r\n}\r\n\r\n/// One saved `endpoint run --save` startup default (F-038 W5,\r\n/// REQ-ENDPOINT-AUTOSTART): the daemon REPLAYS these at start — the endpoint\r\n/// analog of the shipped `subnet attach/detach --save` precedent. Minimal by\r\n/// design: the adapter option is re-resolved fresh at each replay (an adapter\r\n/// update between restarts is honored, an uninstalled one loud-skips), and the\r\n/// endpoint's home subnet is NOT recorded (home is durable on the perch —\r\n/// REQ-INST-15). A replay is always a FRESH session (shape (c)\r\n/// restore-what-was-up was rejected — this is a startup default, not a\r\n/// session restore).\r\n// [impl->REQ-ENDPOINT-AUTOSTART]\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct StartupEndpoint {\r\n    /// The endpoint id to bring up.\r\n    pub id: String,\r\n    /// The adapter option (`<adapter>[:profile]`), resolved at replay time.\r\n    pub adapter: String,\r\n    /// The project cwd the harness PTY lands in (captured at `--save`).\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n}\r\n\r\n/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a\r\n/// partial file still loads (forward/back-compat) — a missing field takes the\r\n/// default. Stored as plain integers (ms) to keep the file human-editable.\r\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\r\nstruct DaemonConfigFile {\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pulse_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    protection_window_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    crash_grace_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    full_auto_update: Option<bool>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    relay: Option<String>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    peer_pump: Option<bool>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    registry_pump_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    registry_evict_after_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    notif_pump_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    sync_pull_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    update_check_period_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    auto_suspend_after_ms: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    resources_blurb: Option<String>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    detached_subnets: Option<Vec<String>>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    worker_reap_ttl_secs: Option<u64>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    startup_endpoints: Option<Vec<StartupEndpoint>>,\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    docs_port: Option<u16>,\r\n}\r\n\r\n/// Resolved daemon configuration. The brain's scheduled loops read their cadence\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\config.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":652,"totalBytes":6825,"outputLines":128,"outputBytes":6825,"shownRange":{"start":1,"end":128},"nextOffset":129}}},"isError":false,"timestamp":1784271684498}}
{"type":"message","id":"a6bf94da","parentId":"e6a617a3","timestamp":"2026-07-17T07:01:24.516Z","message":{"role":"toolResult","toolCallId":"call_EiN7XZJgiV4mcc3EKzeGxnST|fc_0e15f89f5b7b4062016a59d343bed88194b28b435ea69132b3","toolName":"read","content":[{"type":"text","text":"fn env_ms(name: &str, default: u64) -> u64 {\n    std::env::var(name)\n…\n        .and_then(|v| v.parse().ok())\n        .unwrap_or(default)\n}\n\n/// Spawn the brain's index-writer host: ONE thread (single-flight by\n/// construction) that loads the persisted index, runs the boot reconcile\n/// (fingerprint-gated: a warm start with an unchanged generation scans\n/// nothing), then drains + debounces + coalesces the invalidation queue and\n/// runs the periodic full backstop until `stop`. Mirrors\n/// `livehost::spawn_live_host`; the thread dies with the brain and the next\n/// brain rehydrates from the persisted index (ready without warm).\n// [impl->REQ-PROJECT-INDEX-WRITER]\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\n        // untouched): rigs that need a writer-silent daemon set it — the\n        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\n        if std::env::var_os(\"SPT_PROJINDEX_DISABLE\").is_some() {\n            return;\n        }\n        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\n        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\n        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\n\n        let mut engine = WriterEngine::new(WriterPaths::production());\n        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\n\n        // Boot reconcile: global but NOT forced — a warm start over an\n        // unchanged store exits without a scan (the cold/warm start gate).\n        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n        let boot = Coalesced { global: true, ..boot };\n        if let Err(e) = engine.reconcile(&boot, false) {\n            engine.record_failure(e);\n        }\n\n        let mut since_periodic = Duration::ZERO;\n        while !stop.load(Ordering::SeqCst) {\n            // Sleep one poll slice (sliced so a stop lands promptly).\n            let mut left = poll.as_millis() as u64;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n            if stop.load(Ordering::SeqCst) {\n                break;\n            }\n            since_periodic += poll;\n\n            let due_periodic = since_periodic >= periodic;\n            let has_events = projinval::pending_at(&engine.paths.invalidations_dir);\n            if !has_events && !due_periodic {\n                continue;\n            }\n\n            if has_events {\n                // Debounce: let the burst land, then drain it ALL — the\n                // coalescing window (multiple invalidations → one refresh).\n                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n                std::thread::sleep(debounce);\n                engine.stats.pending_refresh = true;\n                engine.write_stats();\n            }\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n            if due_periodic {\n                since_periodic = Duration::ZERO;\n            }\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\n            if req.is_empty() && !due_periodic {\n                continue; // a racer consumed it (never happens in production)\n            }\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\n                engine.record_failure(e);\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_store::contextstore::ContextStore;\n    use spt_store::projinval::InvalScope;\n    use std::time::Instant;\n…\n}\n\n[Showing lines 609-693 of 1270. Use :694 to continue]"}],"details":{"truncation":{"content":"        .and_then(|v| v.parse().ok())\r\n        .unwrap_or(default)\r\n}\r\n\r\n/// Spawn the brain's index-writer host: ONE thread (single-flight by\r\n/// construction) that loads the persisted index, runs the boot reconcile\r\n/// (fingerprint-gated: a warm start with an unchanged generation scans\r\n/// nothing), then drains + debounces + coalesces the invalidation queue and\r\n/// runs the periodic full backstop until `stop`. Mirrors\r\n/// `livehost::spawn_live_host`; the thread dies with the brain and the next\r\n/// brain rehydrates from the persisted index (ready without warm).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\r\n        // untouched): rigs that need a writer-silent daemon set it — the\r\n        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\r\n        if std::env::var_os(\"SPT_PROJINDEX_DISABLE\").is_some() {\r\n            return;\r\n        }\r\n        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\r\n        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\r\n        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\r\n\r\n        let mut engine = WriterEngine::new(WriterPaths::production());\r\n        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\r\n\r\n        // Boot reconcile: global but NOT forced — a warm start over an\r\n        // unchanged store exits without a scan (the cold/warm start gate).\r\n        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n        let boot = Coalesced { global: true, ..boot };\r\n        if let Err(e) = engine.reconcile(&boot, false) {\r\n            engine.record_failure(e);\r\n        }\r\n\r\n        let mut since_periodic = Duration::ZERO;\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // Sleep one poll slice (sliced so a stop lands promptly).\r\n            let mut left = poll.as_millis() as u64;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n                left -= step;\r\n            }\r\n            if stop.load(Ordering::SeqCst) {\r\n                break;\r\n            }\r\n            since_periodic += poll;\r\n\r\n            let due_periodic = since_periodic >= periodic;\r\n            let has_events = projinval::pending_at(&engine.paths.invalidations_dir);\r\n            if !has_events && !due_periodic {\r\n                continue;\r\n            }\r\n\r\n            if has_events {\r\n                // Debounce: let the burst land, then drain it ALL — the\r\n                // coalescing window (multiple invalidations → one refresh).\r\n                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n                std::thread::sleep(debounce);\r\n                engine.stats.pending_refresh = true;\r\n                engine.write_stats();\r\n            }\r\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n            if due_periodic {\r\n                since_periodic = Duration::ZERO;\r\n            }\r\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\r\n            if req.is_empty() && !due_periodic {\r\n                continue; // a racer consumed it (never happens in production)\r\n            }\r\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\r\n                engine.record_failure(e);\r\n            }\r\n        }\r\n    })\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use spt_store::contextstore::ContextStore;\r\n    use spt_store::projinval::InvalScope;\r\n    use std::time::Instant;\r","truncated":true,"truncatedBy":"lines","totalLines":662,"totalBytes":3872,"outputLines":85,"outputBytes":3872,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn env_ms(name: &str, default: u64) -> u64 {\n    std::env::var(name)\n…\n        .and_then(|v| v.parse().ok())\r\n        .unwrap_or(default)\r\n}\r\n\r\n/// Spawn the brain's index-writer host: ONE thread (single-flight by\r\n/// construction) that loads the persisted index, runs the boot reconcile\r\n/// (fingerprint-gated: a warm start with an unchanged generation scans\r\n/// nothing), then drains + debounces + coalesces the invalidation queue and\r\n/// runs the periodic full backstop until `stop`. Mirrors\r\n/// `livehost::spawn_live_host`; the thread dies with the brain and the next\r\n/// brain rehydrates from the persisted index (ready without warm).\r\n// [impl->REQ-PROJECT-INDEX-WRITER]\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn spawn_index_writer(stop: Arc<AtomicBool>) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        // TEST-ONLY isolation valve (runtime env, DEFAULT unset = production\r\n        // untouched): rigs that need a writer-silent daemon set it — the\r\n        // SPT_LIVEHOST_RECONCILE_DISABLE precedent.\r\n        if std::env::var_os(\"SPT_PROJINDEX_DISABLE\").is_some() {\r\n            return;\r\n        }\r\n        let poll = Duration::from_millis(env_ms(\"SPT_PROJINDEX_POLL_MS\", POLL_MS));\r\n        let debounce = Duration::from_millis(env_ms(\"SPT_PROJINDEX_DEBOUNCE_MS\", DEBOUNCE_MS));\r\n        let periodic = Duration::from_millis(env_ms(\"SPT_PROJINDEX_PERIODIC_MS\", PERIODIC_MS));\r\n\r\n        let mut engine = WriterEngine::new(WriterPaths::production());\r\n        engine.write_stats(); // the loaded-not-yet-reconciled truth is visible immediately\r\n\r\n        // Boot reconcile: global but NOT forced — a warm start over an\r\n        // unchanged store exits without a scan (the cold/warm start gate).\r\n        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n        let boot = Coalesced { global: true, ..boot };\r\n        if let Err(e) = engine.reconcile(&boot, false) {\r\n            engine.record_failure(e);\r\n        }\r\n\r\n        let mut since_periodic = Duration::ZERO;\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // Sleep one poll slice (sliced so a stop lands promptly).\r\n            let mut left = poll.as_millis() as u64;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n                left -= step;\r\n            }\r\n            if stop.load(Ordering::SeqCst) {\r\n                break;\r\n            }\r\n            since_periodic += poll;\r\n\r\n            let due_periodic = since_periodic >= periodic;\r\n            let has_events = projinval::pending_at(&engine.paths.invalidations_dir);\r\n            if !has_events && !due_periodic {\r\n                continue;\r\n            }\r\n\r\n            if has_events {\r\n                // Debounce: let the burst land, then drain it ALL — the\r\n                // coalescing window (multiple invalidations → one refresh).\r\n                // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n                std::thread::sleep(debounce);\r\n                engine.stats.pending_refresh = true;\r\n                engine.write_stats();\r\n            }\r\n            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\r\n            if due_periodic {\r\n                since_periodic = Duration::ZERO;\r\n            }\r\n            let req = if due_periodic { Coalesced { global: true, ..req } } else { req };\r\n            if req.is_empty() && !due_periodic {\r\n                continue; // a racer consumed it (never happens in production)\r\n            }\r\n            if let Err(e) = engine.reconcile(&req, due_periodic) {\r\n                engine.record_failure(e);\r\n            }\r\n        }\r\n    })\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use spt_store::contextstore::ContextStore;\r\n    use spt_store::projinval::InvalScope;\r\n    use std::time::Instant;\r\n…\n}","startLine":606,"lineNumbers":[606,607,null,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,null,1269]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\projwriter.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1270,"totalBytes":3872,"outputLines":85,"outputBytes":3872,"shownRange":{"start":609,"end":693},"nextOffset":694}}},"isError":false,"timestamp":1784271684516}}
{"type":"message","id":"87a298f3","parentId":"a6bf94da","timestamp":"2026-07-17T07:01:24.534Z","message":{"role":"toolResult","toolCallId":"call_g141o2nxc61pn4lGpcwkWAEx|fc_0e15f89f5b7b4062016a59d343bf0c8194a9f8f978e530fbc8","toolName":"read","content":[{"type":"text","text":"impl PeerIo<'_> {\n…\n    /// Mint the next durable open-op from the pump's sole counter (exactly-once\n    /// open identity — a restart can never reuse one), tagged with the `pump`\n    /// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,\n    /// so its ops namespace under `pump` and never collide with cli/rc/shell ops.\n    fn open_op(&mut self) -> io::Result<MintedOp> {\n        Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))\n    }\n}\n\n/// Run the pump until `stop` is set. Owns one brain IPC connection and a\n/// per-peer connection cache (an IO failure against a peer drops its cache\n/// row; the next tick redials — conns are rebuilt freely, nothing leaks\n/// because the broker's closed-watcher reaps dead conns).\n///\n/// The pure scheduler + fan-out (pump-seam P4, all four legs behind the seam):\n/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\n/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\n/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\n/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\n/// logic lives here — the shell owns only *when* and *toward whom*.\npub fn run_peer_pump(\n    broker_name: &str,\n    registry: Arc<RegistryHost>,\n    paths: &PumpPaths,\n    resolver: PeerResolver,\n    config: PumpConfig,\n    hooks: PumpHooks,\n    stop: &AtomicBool,\n) -> io::Result<()> {\n    let PumpConfig {\n        cadence,\n        full_auto_update,\n    } = config;\n    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\n    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\n    // black-holed peer can never wedge this single-threaded loop (the 2.2h\n    // hfenduleam stall). A timed-out read escalates to a supervised restart\n    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    let mut brain = connect(broker_name)?;\n    // The DEDICATED event-drain carrier (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION):\n    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\n    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\n    // whose reply-reads consume every event kind off their carrier; sharing one\n    // carrier would make a pull-read swallow a presence frame (advancing the\n    // exactly-once cursor while dropping the outcome — the D4c break). Two\n    // carriers keep the drain and the pulls from swallowing each other by\n    // construction. Tail-follow (no history replay, baseline on first live event)\n    // so a long-lived broker's evicted ring can never gap-trip a false restart.\n    let mut events = connect(broker_name)?;\n    events.net_presence_subscribe_tail()?;\n    // Startup reconcile + invariant migration (ADR-0039 Decisions 3+4,\n    // REQ-PEER-ROUTE-CHAIN / REQ-PEERADDR-INVARIANT): validated roster\n    // addresses reconcile into the dial cache BEFORE the first round —\n    // recovery is connection-independent (a node restarted mid-strand heals\n    // here, no operator surgery) — and rows violating `outer key ==\n    // address.id` are repaired from the roster or dropped loudly (the first\n    // post-upgrade load is the migration; the file is never bare-deleted —\n    // valid rows and the id-only fallback behind them stay untouched).\n    // Idempotent, so re-running on every supervised pump restart is free.\n    startup_reconcile_peeraddrs(paths, registry.node_hex());\n    // Reachability health (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): loaded\n    // so the last-good stamps survive a restart; the live counts re-prime as\n    // rounds run. Persisted beside the heartbeat; the status CLI reads it.\n    let health_path = PumpHealth::path_beside(&paths.heartbeat);\n    let mut health = PumpHealth::load_from(&health_path);\n    health.note_disconnected(0); // no conns exist yet in this pump generation\n    let mut ops = EpochSource::load_from(&paths.ops);\n    // The live per-peer connection cache (updated by presence events) + the\n    // per-peer dial schedule (G5 backoff). Both RAM-only, re-primed on restart.\n    let mut conns: HashMap<String, u64> = HashMap::new();\n    let mut sched: HashMap<String, PeerSched> = HashMap::new();\n    // The four cadenced legs behind the worker seam, in fan-out order\n    // (registry → notif → sync → update). The shell drives them generically;\n    // each leg's *what* lives in its module (§Frozen). Construction capture\n    // (§Frozen-4): each worker owns the registry Arc / path slices / config\n    // knob it needs — the sync worker additionally owns the conflict-surface\n    // hooks (ADR-0013).\n    let mut registry_worker = RegistryWorker::new(Arc::clone(&registry), paths, &cadence);\n    let mut notif_worker = NotifWorker::new(paths, &cadence);\n    let mut sync_worker = SyncWorker::new(Arc::clone(&registry), paths, &cadence, hooks);\n    let mut update_worker = UpdateWorker::new(Arc::clone(&registry), paths, &cadence, full_auto_update);\n    let mut workers: [&mut dyn PumpWorker; 4] = [\n        &mut registry_worker,\n        &mut notif_worker,\n        &mut sync_worker,\n        &mut update_worker,\n    ];\n    // WorkerLasts (ADR-0018 §V4): RAM-only last-ran instants, parallel to\n    // `workers`. `None` = never run yet → due immediately, so the first tick\n    // primes every leg. Never `Instant - Duration` — the underflow panic on a\n    // sub-uptime monotonic clock (KH 5.9) lives nowhere in this path.\n    let mut last: [Option<Instant>; 4] = [None; 4];\n    let mut last_hb: Option<Instant> = None;\n\n    while !stop.load(Ordering::Relaxed) {\n        let now = Instant::now();\n        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\n        // while the loop is ALIVE — including idle ticks — never per work round,\n        // so a wedged step and a healthy quiet pump are distinguishable.\n        // Throttled; per-tick, never a worker (REQ-DAEMON-5), no wake.\n        // [impl->REQ-DAEMON-5]\n        if due(last_hb, false, now, HEARTBEAT_PERIOD) {\n            write_heartbeat(&paths.heartbeat);\n            last_hb = Some(now);\n        }\n        // Poll every worker's wake and decide due (the pure scheduler step):\n        // every wake is consumed EVERY tick so a marker is never stranded (the\n        // registry advertise-now, REQ-CONV-2; the sync freshness pull,\n        // REQ-INST-3). The take IS the remove — one wake claims exactly one\n        // forced round.\n        let (due_flags, any_due) = compute_due(&mut workers, &last, now);\n        if !any_due {\n            thread::sleep(TICK);\n            continue;\n        }\n\n        // Per-round shared loads (§Frozen-3): subnets / attachment /\n        // roster-backed policy, read ONCE and handed to every worker.\n        let ctx = RoundCtx::load(paths);\n\n        // Each due worker's node-local pre_round, once per round before ANY\n        // peer_step (only the registry leg does work here — its sweep +\n        // advertise; the rest default to a no-op).\n        pre_rounds(&mut workers, &due_flags, &ctx);\n\n        // ── PUMP-TRUTH W2 round (REQ-PUMP-PEER-ISOLATION) — concurrent dials +\n        // per-peer fault isolation. Group this round's fan-out targets by peer (a\n        // peer may target several subnets; one conn serves them all).\n        let mut peer_subnets: HashMap<String, Vec<String>> = HashMap::new();\n        for (sub_name, peer_hex) in fan_targets(&ctx, registry.node_hex()) {\n            peer_subnets.entry(peer_hex).or_default().push(sub_name);\n        }\n        // Health targets = this round's fan-out set; snapshot the registry\n        // host's last-admitted stamp while here (REQ-PUMP-STAGE-TRUTH).\n        health.set_targets(peer_subnets.keys().cloned(), now_ms());\n        health.note_registry_admit(registry.last_admit_ms());\n        let round_start = Instant::now();\n        let mut pending: HashSet<String> = HashSet::new();\n\n        // Submit NON-BLOCKING dials for eligible unconnected peers; run the due\n        // legs NOW for already-connected ones. The live peer advertises without\n        // waiting on a dead one (no head-of-line block) — a dead peer is only a\n        // pending outcome, never a serial blocker.\n        for (peer_hex, subs) in &peer_subnets {\n            if let Some(&conn_id) = conns.get(peer_hex) {\n                run_peer_subnets(\n                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n                    conn_id, subs, peer_hex, &ctx, round_start,\n                )?;\n            } else if peer_eligible(&sched, peer_hex, round_start) {\n                if let Some(addr) =\n                    resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs, &ctx.policy.roster)\n                {\n                    match brain.submit_dial(addr) {\n                        Ok(()) => {\n                            pending.insert(peer_hex.clone());\n                        }\n                        // A raw carrier-op TimedOut on the submit IPC itself is a\n                        // genuine broker-IPC desync → bubble → supervised restart.\n                        Err(e) if e.kind() == io::ErrorKind::TimedOut => return Err(e),\n                        // The broker refused the submit (net disabled / bad addr):\n                        // an ordinary per-peer failure — back off, round continues.\n                        Err(e) => {\n                            eprintln!(\n                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\n                                now_ms(),\n                                mono_ms()\n                            );\n                            health.note_failed(peer_hex, \"submit\", conns.len(), now_ms());\n                            let prev = sched.get(peer_hex).copied();\n                            sched.insert(peer_hex.clone(), next_peer_backoff(prev, round_start));\n                        }\n                    }\n                } else {\n                    // No route on any chain leg (REQ-PUMP-STAGE-TRUTH stage\n                    // `address-resolution`). Transition-logged: the first\n                    // unroutable round per peer is loud, repeats are quiet —\n                    // the peer retries on its own cadence like before.\n                    if health.note_failed(\n                        peer_hex,\n                        crate::seedproofx::STAGE_RESOLVE,\n                        conns.len(),\n                        now_ms(),\n                    ) {\n                        eprintln!(\n                            \"PUMP_PEER_FAIL:{peer_hex}:stage={}: no route (cache/roster/discovery all empty) wall_ms={} mono_ms={}\",\n                            crate::seedproofx::STAGE_RESOLVE,\n                            now_ms(),\n                            mono_ms()\n                        );\n                    }\n                }\n            }\n        }\n…\n    }\n…\n}\n\n[Showing lines 499-693 of 1982. Use :694 to continue]"}],"details":{"truncation":{"content":"    /// Mint the next durable open-op from the pump's sole counter (exactly-once\r\n    /// open identity — a restart can never reuse one), tagged with the `pump`\r\n    /// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,\r\n    /// so its ops namespace under `pump` and never collide with cli/rc/shell ops.\r\n    fn open_op(&mut self) -> io::Result<MintedOp> {\r\n        Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))\r\n    }\r\n}\r\n\r\n/// Run the pump until `stop` is set. Owns one brain IPC connection and a\r\n/// per-peer connection cache (an IO failure against a peer drops its cache\r\n/// row; the next tick redials — conns are rebuilt freely, nothing leaks\r\n/// because the broker's closed-watcher reaps dead conns).\r\n///\r\n/// The pure scheduler + fan-out (pump-seam P4, all four legs behind the seam):\r\n/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\r\n/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\r\n/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\r\n/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\r\n/// logic lives here — the shell owns only *when* and *toward whom*.\r\npub fn run_peer_pump(\r\n    broker_name: &str,\r\n    registry: Arc<RegistryHost>,\r\n    paths: &PumpPaths,\r\n    resolver: PeerResolver,\r\n    config: PumpConfig,\r\n    hooks: PumpHooks,\r\n    stop: &AtomicBool,\r\n) -> io::Result<()> {\r\n    let PumpConfig {\r\n        cadence,\r\n        full_auto_update,\r\n    } = config;\r\n    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\r\n    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\r\n    // black-holed peer can never wedge this single-threaded loop (the 2.2h\r\n    // hfenduleam stall). A timed-out read escalates to a supervised restart\r\n    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\r\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\r\n    let mut brain = connect(broker_name)?;\r\n    // The DEDICATED event-drain carrier (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION):\r\n    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\r\n    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\r\n    // whose reply-reads consume every event kind off their carrier; sharing one\r\n    // carrier would make a pull-read swallow a presence frame (advancing the\r\n    // exactly-once cursor while dropping the outcome — the D4c break). Two\r\n    // carriers keep the drain and the pulls from swallowing each other by\r\n    // construction. Tail-follow (no history replay, baseline on first live event)\r\n    // so a long-lived broker's evicted ring can never gap-trip a false restart.\r\n    let mut events = connect(broker_name)?;\r\n    events.net_presence_subscribe_tail()?;\r\n    // Startup reconcile + invariant migration (ADR-0039 Decisions 3+4,\r\n    // REQ-PEER-ROUTE-CHAIN / REQ-PEERADDR-INVARIANT): validated roster\r\n    // addresses reconcile into the dial cache BEFORE the first round —\r\n    // recovery is connection-independent (a node restarted mid-strand heals\r\n    // here, no operator surgery) — and rows violating `outer key ==\r\n    // address.id` are repaired from the roster or dropped loudly (the first\r\n    // post-upgrade load is the migration; the file is never bare-deleted —\r\n    // valid rows and the id-only fallback behind them stay untouched).\r\n    // Idempotent, so re-running on every supervised pump restart is free.\r\n    startup_reconcile_peeraddrs(paths, registry.node_hex());\r\n    // Reachability health (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): loaded\r\n    // so the last-good stamps survive a restart; the live counts re-prime as\r\n    // rounds run. Persisted beside the heartbeat; the status CLI reads it.\r\n    let health_path = PumpHealth::path_beside(&paths.heartbeat);\r\n    let mut health = PumpHealth::load_from(&health_path);\r\n    health.note_disconnected(0); // no conns exist yet in this pump generation\r\n    let mut ops = EpochSource::load_from(&paths.ops);\r\n    // The live per-peer connection cache (updated by presence events) + the\r\n    // per-peer dial schedule (G5 backoff). Both RAM-only, re-primed on restart.\r\n    let mut conns: HashMap<String, u64> = HashMap::new();\r\n    let mut sched: HashMap<String, PeerSched> = HashMap::new();\r\n    // The four cadenced legs behind the worker seam, in fan-out order\r\n    // (registry → notif → sync → update). The shell drives them generically;\r\n    // each leg's *what* lives in its module (§Frozen). Construction capture\r\n    // (§Frozen-4): each worker owns the registry Arc / path slices / config\r\n    // knob it needs — the sync worker additionally owns the conflict-surface\r\n    // hooks (ADR-0013).\r\n    let mut registry_worker = RegistryWorker::new(Arc::clone(&registry), paths, &cadence);\r\n    let mut notif_worker = NotifWorker::new(paths, &cadence);\r\n    let mut sync_worker = SyncWorker::new(Arc::clone(&registry), paths, &cadence, hooks);\r\n    let mut update_worker = UpdateWorker::new(Arc::clone(&registry), paths, &cadence, full_auto_update);\r\n    let mut workers: [&mut dyn PumpWorker; 4] = [\r\n        &mut registry_worker,\r\n        &mut notif_worker,\r\n        &mut sync_worker,\r\n        &mut update_worker,\r\n    ];\r\n    // WorkerLasts (ADR-0018 §V4): RAM-only last-ran instants, parallel to\r\n    // `workers`. `None` = never run yet → due immediately, so the first tick\r\n    // primes every leg. Never `Instant - Duration` — the underflow panic on a\r\n    // sub-uptime monotonic clock (KH 5.9) lives nowhere in this path.\r\n    let mut last: [Option<Instant>; 4] = [None; 4];\r\n    let mut last_hb: Option<Instant> = None;\r\n\r\n    while !stop.load(Ordering::Relaxed) {\r\n        let now = Instant::now();\r\n        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\r\n        // while the loop is ALIVE — including idle ticks — never per work round,\r\n        // so a wedged step and a healthy quiet pump are distinguishable.\r\n        // Throttled; per-tick, never a worker (REQ-DAEMON-5), no wake.\r\n        // [impl->REQ-DAEMON-5]\r\n        if due(last_hb, false, now, HEARTBEAT_PERIOD) {\r\n            write_heartbeat(&paths.heartbeat);\r\n            last_hb = Some(now);\r\n        }\r\n        // Poll every worker's wake and decide due (the pure scheduler step):\r\n        // every wake is consumed EVERY tick so a marker is never stranded (the\r\n        // registry advertise-now, REQ-CONV-2; the sync freshness pull,\r\n        // REQ-INST-3). The take IS the remove — one wake claims exactly one\r\n        // forced round.\r\n        let (due_flags, any_due) = compute_due(&mut workers, &last, now);\r\n        if !any_due {\r\n            thread::sleep(TICK);\r\n            continue;\r\n        }\r\n\r\n        // Per-round shared loads (§Frozen-3): subnets / attachment /\r\n        // roster-backed policy, read ONCE and handed to every worker.\r\n        let ctx = RoundCtx::load(paths);\r\n\r\n        // Each due worker's node-local pre_round, once per round before ANY\r\n        // peer_step (only the registry leg does work here — its sweep +\r\n        // advertise; the rest default to a no-op).\r\n        pre_rounds(&mut workers, &due_flags, &ctx);\r\n\r\n        // ── PUMP-TRUTH W2 round (REQ-PUMP-PEER-ISOLATION) — concurrent dials +\r\n        // per-peer fault isolation. Group this round's fan-out targets by peer (a\r\n        // peer may target several subnets; one conn serves them all).\r\n        let mut peer_subnets: HashMap<String, Vec<String>> = HashMap::new();\r\n        for (sub_name, peer_hex) in fan_targets(&ctx, registry.node_hex()) {\r\n            peer_subnets.entry(peer_hex).or_default().push(sub_name);\r\n        }\r\n        // Health targets = this round's fan-out set; snapshot the registry\r\n        // host's last-admitted stamp while here (REQ-PUMP-STAGE-TRUTH).\r\n        health.set_targets(peer_subnets.keys().cloned(), now_ms());\r\n        health.note_registry_admit(registry.last_admit_ms());\r\n        let round_start = Instant::now();\r\n        let mut pending: HashSet<String> = HashSet::new();\r\n\r\n        // Submit NON-BLOCKING dials for eligible unconnected peers; run the due\r\n        // legs NOW for already-connected ones. The live peer advertises without\r\n        // waiting on a dead one (no head-of-line block) — a dead peer is only a\r\n        // pending outcome, never a serial blocker.\r\n        for (peer_hex, subs) in &peer_subnets {\r\n            if let Some(&conn_id) = conns.get(peer_hex) {\r\n                run_peer_subnets(\r\n                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\r\n                    conn_id, subs, peer_hex, &ctx, round_start,\r\n                )?;\r\n            } else if peer_eligible(&sched, peer_hex, round_start) {\r\n                if let Some(addr) =\r\n                    resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs, &ctx.policy.roster)\r\n                {\r\n                    match brain.submit_dial(addr) {\r\n                        Ok(()) => {\r\n                            pending.insert(peer_hex.clone());\r\n                        }\r\n                        // A raw carrier-op TimedOut on the submit IPC itself is a\r\n                        // genuine broker-IPC desync → bubble → supervised restart.\r\n                        Err(e) if e.kind() == io::ErrorKind::TimedOut => return Err(e),\r\n                        // The broker refused the submit (net disabled / bad addr):\r\n                        // an ordinary per-peer failure — back off, round continues.\r\n                        Err(e) => {\r\n                            eprintln!(\r\n                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\r\n                                now_ms(),\r\n                                mono_ms()\r\n                            );\r\n                            health.note_failed(peer_hex, \"submit\", conns.len(), now_ms());\r\n                            let prev = sched.get(peer_hex).copied();\r\n                            sched.insert(peer_hex.clone(), next_peer_backoff(prev, round_start));\r\n                        }\r\n                    }\r\n                } else {\r\n                    // No route on any chain leg (REQ-PUMP-STAGE-TRUTH stage\r\n                    // `address-resolution`). Transition-logged: the first\r\n                    // unroutable round per peer is loud, repeats are quiet —\r\n                    // the peer retries on its own cadence like before.\r\n                    if health.note_failed(\r\n                        peer_hex,\r\n                        crate::seedproofx::STAGE_RESOLVE,\r\n                        conns.len(),\r\n                        now_ms(),\r\n                    ) {\r\n                        eprintln!(\r\n                            \"PUMP_PEER_FAIL:{peer_hex}:stage={}: no route (cache/roster/discovery all empty) wall_ms={} mono_ms={}\",\r\n                            crate::seedproofx::STAGE_RESOLVE,\r\n                            now_ms(),\r\n                            mono_ms()\r\n                        );\r\n                    }\r\n                }\r\n            }\r\n        }\r","truncated":true,"truncatedBy":"lines","totalLines":1484,"totalBytes":11151,"outputLines":195,"outputBytes":11151,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl PeerIo<'_> {\n…\n    /// Mint the next durable open-op from the pump's sole counter (exactly-once\r\n    /// open identity — a restart can never reuse one), tagged with the `pump`\r\n    /// journal minter (ADR-0034): the pump is the sole writer of `pump-ops.json`,\r\n    /// so its ops namespace under `pump` and never collide with cli/rc/shell ops.\r\n    fn open_op(&mut self) -> io::Result<MintedOp> {\r\n        Ok(MintedOp::new(Minter::Pump, self.ops.next_epoch()?))\r\n    }\r\n}\r\n\r\n/// Run the pump until `stop` is set. Owns one brain IPC connection and a\r\n/// per-peer connection cache (an IO failure against a peer drops its cache\r\n/// row; the next tick redials — conns are rebuilt freely, nothing leaks\r\n/// because the broker's closed-watcher reaps dead conns).\r\n///\r\n/// The pure scheduler + fan-out (pump-seam P4, all four legs behind the seam):\r\n/// heartbeat → poll every worker's wake → the [`due`] kernel → each due\r\n/// worker's `pre_round` (once, before any peer_step) → per-subnet × per-peer\r\n/// (`ensure_conn` → each due worker's `peer_step` in fan-out order,\r\n/// abort-on-first-fail) → mark-after-round with a POST-round instant. No leg\r\n/// logic lives here — the shell owns only *when* and *toward whom*.\r\npub fn run_peer_pump(\r\n    broker_name: &str,\r\n    registry: Arc<RegistryHost>,\r\n    paths: &PumpPaths,\r\n    resolver: PeerResolver,\r\n    config: PumpConfig,\r\n    hooks: PumpHooks,\r\n    stop: &AtomicBool,\r\n) -> io::Result<()> {\r\n    let PumpConfig {\r\n        cadence,\r\n        full_auto_update,\r\n    } = config;\r\n    // PUMP MODE: a brain whose carrier is SPLIT at construction (reader thread +\r\n    // channel) so every reply read is bounded by PUMP_PEER_IO_TIMEOUT — a\r\n    // black-holed peer can never wedge this single-threaded loop (the 2.2h\r\n    // hfenduleam stall). A timed-out read escalates to a supervised restart\r\n    // below, never a per-peer retry (REQ-HAZARD-PUMP-IPC-DEADLINE).\r\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\r\n    let mut brain = connect(broker_name)?;\r\n    // The DEDICATED event-drain carrier (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION):\r\n    // a SECOND pump-mode brain that OWNS the presence subscription. It is split\r\n    // from `brain` deliberately — `brain` runs the per-peer `peer_step` pulls\r\n    // whose reply-reads consume every event kind off their carrier; sharing one\r\n    // carrier would make a pull-read swallow a presence frame (advancing the\r\n    // exactly-once cursor while dropping the outcome — the D4c break). Two\r\n    // carriers keep the drain and the pulls from swallowing each other by\r\n    // construction. Tail-follow (no history replay, baseline on first live event)\r\n    // so a long-lived broker's evicted ring can never gap-trip a false restart.\r\n    let mut events = connect(broker_name)?;\r\n    events.net_presence_subscribe_tail()?;\r\n    // Startup reconcile + invariant migration (ADR-0039 Decisions 3+4,\r\n    // REQ-PEER-ROUTE-CHAIN / REQ-PEERADDR-INVARIANT): validated roster\r\n    // addresses reconcile into the dial cache BEFORE the first round —\r\n    // recovery is connection-independent (a node restarted mid-strand heals\r\n    // here, no operator surgery) — and rows violating `outer key ==\r\n    // address.id` are repaired from the roster or dropped loudly (the first\r\n    // post-upgrade load is the migration; the file is never bare-deleted —\r\n    // valid rows and the id-only fallback behind them stay untouched).\r\n    // Idempotent, so re-running on every supervised pump restart is free.\r\n    startup_reconcile_peeraddrs(paths, registry.node_hex());\r\n    // Reachability health (ADR-0039 Decision 5, REQ-PUMP-STAGE-TRUTH): loaded\r\n    // so the last-good stamps survive a restart; the live counts re-prime as\r\n    // rounds run. Persisted beside the heartbeat; the status CLI reads it.\r\n    let health_path = PumpHealth::path_beside(&paths.heartbeat);\r\n    let mut health = PumpHealth::load_from(&health_path);\r\n    health.note_disconnected(0); // no conns exist yet in this pump generation\r\n    let mut ops = EpochSource::load_from(&paths.ops);\r\n    // The live per-peer connection cache (updated by presence events) + the\r\n    // per-peer dial schedule (G5 backoff). Both RAM-only, re-primed on restart.\r\n    let mut conns: HashMap<String, u64> = HashMap::new();\r\n    let mut sched: HashMap<String, PeerSched> = HashMap::new();\r\n    // The four cadenced legs behind the worker seam, in fan-out order\r\n    // (registry → notif → sync → update). The shell drives them generically;\r\n    // each leg's *what* lives in its module (§Frozen). Construction capture\r\n    // (§Frozen-4): each worker owns the registry Arc / path slices / config\r\n    // knob it needs — the sync worker additionally owns the conflict-surface\r\n    // hooks (ADR-0013).\r\n    let mut registry_worker = RegistryWorker::new(Arc::clone(&registry), paths, &cadence);\r\n    let mut notif_worker = NotifWorker::new(paths, &cadence);\r\n    let mut sync_worker = SyncWorker::new(Arc::clone(&registry), paths, &cadence, hooks);\r\n    let mut update_worker = UpdateWorker::new(Arc::clone(&registry), paths, &cadence, full_auto_update);\r\n    let mut workers: [&mut dyn PumpWorker; 4] = [\r\n        &mut registry_worker,\r\n        &mut notif_worker,\r\n        &mut sync_worker,\r\n        &mut update_worker,\r\n    ];\r\n    // WorkerLasts (ADR-0018 §V4): RAM-only last-ran instants, parallel to\r\n    // `workers`. `None` = never run yet → due immediately, so the first tick\r\n    // primes every leg. Never `Instant - Duration` — the underflow panic on a\r\n    // sub-uptime monotonic clock (KH 5.9) lives nowhere in this path.\r\n    let mut last: [Option<Instant>; 4] = [None; 4];\r\n    let mut last_hb: Option<Instant> = None;\r\n\r\n    while !stop.load(Ordering::Relaxed) {\r\n        let now = Instant::now();\r\n        // Shell-owned liveness heartbeat (M8 decision 23, REQ-DAEMON-5): written\r\n        // while the loop is ALIVE — including idle ticks — never per work round,\r\n        // so a wedged step and a healthy quiet pump are distinguishable.\r\n        // Throttled; per-tick, never a worker (REQ-DAEMON-5), no wake.\r\n        // [impl->REQ-DAEMON-5]\r\n        if due(last_hb, false, now, HEARTBEAT_PERIOD) {\r\n            write_heartbeat(&paths.heartbeat);\r\n            last_hb = Some(now);\r\n        }\r\n        // Poll every worker's wake and decide due (the pure scheduler step):\r\n        // every wake is consumed EVERY tick so a marker is never stranded (the\r\n        // registry advertise-now, REQ-CONV-2; the sync freshness pull,\r\n        // REQ-INST-3). The take IS the remove — one wake claims exactly one\r\n        // forced round.\r\n        let (due_flags, any_due) = compute_due(&mut workers, &last, now);\r\n        if !any_due {\r\n            thread::sleep(TICK);\r\n            continue;\r\n        }\r\n\r\n        // Per-round shared loads (§Frozen-3): subnets / attachment /\r\n        // roster-backed policy, read ONCE and handed to every worker.\r\n        let ctx = RoundCtx::load(paths);\r\n\r\n        // Each due worker's node-local pre_round, once per round before ANY\r\n        // peer_step (only the registry leg does work here — its sweep +\r\n        // advertise; the rest default to a no-op).\r\n        pre_rounds(&mut workers, &due_flags, &ctx);\r\n\r\n        // ── PUMP-TRUTH W2 round (REQ-PUMP-PEER-ISOLATION) — concurrent dials +\r\n        // per-peer fault isolation. Group this round's fan-out targets by peer (a\r\n        // peer may target several subnets; one conn serves them all).\r\n        let mut peer_subnets: HashMap<String, Vec<String>> = HashMap::new();\r\n        for (sub_name, peer_hex) in fan_targets(&ctx, registry.node_hex()) {\r\n            peer_subnets.entry(peer_hex).or_default().push(sub_name);\r\n        }\r\n        // Health targets = this round's fan-out set; snapshot the registry\r\n        // host's last-admitted stamp while here (REQ-PUMP-STAGE-TRUTH).\r\n        health.set_targets(peer_subnets.keys().cloned(), now_ms());\r\n        health.note_registry_admit(registry.last_admit_ms());\r\n        let round_start = Instant::now();\r\n        let mut pending: HashSet<String> = HashSet::new();\r\n\r\n        // Submit NON-BLOCKING dials for eligible unconnected peers; run the due\r\n        // legs NOW for already-connected ones. The live peer advertises without\r\n        // waiting on a dead one (no head-of-line block) — a dead peer is only a\r\n        // pending outcome, never a serial blocker.\r\n        for (peer_hex, subs) in &peer_subnets {\r\n            if let Some(&conn_id) = conns.get(peer_hex) {\r\n                run_peer_subnets(\r\n                    &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\r\n                    conn_id, subs, peer_hex, &ctx, round_start,\r\n                )?;\r\n            } else if peer_eligible(&sched, peer_hex, round_start) {\r\n                if let Some(addr) =\r\n                    resolve_submit_addr(&resolver, peer_hex, &paths.peer_addrs, &ctx.policy.roster)\r\n                {\r\n                    match brain.submit_dial(addr) {\r\n                        Ok(()) => {\r\n                            pending.insert(peer_hex.clone());\r\n                        }\r\n                        // A raw carrier-op TimedOut on the submit IPC itself is a\r\n                        // genuine broker-IPC desync → bubble → supervised restart.\r\n                        Err(e) if e.kind() == io::ErrorKind::TimedOut => return Err(e),\r\n                        // The broker refused the submit (net disabled / bad addr):\r\n                        // an ordinary per-peer failure — back off, round continues.\r\n                        Err(e) => {\r\n                            eprintln!(\r\n                                \"PUMP_PEER_FAIL:{peer_hex}:{e} wall_ms={} mono_ms={}\",\r\n                                now_ms(),\r\n                                mono_ms()\r\n                            );\r\n                            health.note_failed(peer_hex, \"submit\", conns.len(), now_ms());\r\n                            let prev = sched.get(peer_hex).copied();\r\n                            sched.insert(peer_hex.clone(), next_peer_backoff(prev, round_start));\r\n                        }\r\n                    }\r\n                } else {\r\n                    // No route on any chain leg (REQ-PUMP-STAGE-TRUTH stage\r\n                    // `address-resolution`). Transition-logged: the first\r\n                    // unroutable round per peer is loud, repeats are quiet —\r\n                    // the peer retries on its own cadence like before.\r\n                    if health.note_failed(\r\n                        peer_hex,\r\n                        crate::seedproofx::STAGE_RESOLVE,\r\n                        conns.len(),\r\n                        now_ms(),\r\n                    ) {\r\n                        eprintln!(\r\n                            \"PUMP_PEER_FAIL:{peer_hex}:stage={}: no route (cache/roster/discovery all empty) wall_ms={} mono_ms={}\",\r\n                            crate::seedproofx::STAGE_RESOLVE,\r\n                            now_ms(),\r\n                            mono_ms()\r\n                        );\r\n                    }\r\n                }\r\n            }\r\n        }\r\n…\n    }\n…\n}","startLine":492,"lineNumbers":[492,null,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,null,730,null,732]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\pump\\mod.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1982,"totalBytes":11151,"outputLines":195,"outputBytes":11151,"shownRange":{"start":499,"end":693},"nextOffset":694}}},"isError":false,"timestamp":1784271684534}}
{"type":"message","id":"ea28af9b","parentId":"87a298f3","timestamp":"2026-07-17T07:01:24.550Z","message":{"role":"toolResult","toolCallId":"call_St4sOaQTQpCl0RFmsdqlbOdp|fc_0e15f89f5b7b4062016a59d343befc81948960a26677441d40","toolName":"read","content":[{"type":"text","text":"/// image-lock exposure now turn-bounded), so a cross-version upgrade leaves a multi-MB\n/// `.live-bin` per pre-W5 endpoint forever. Runs ONCE at brain start AFTER\n/// [`sweep_legacy_resident_psyches`] (which kills any wrapper running FROM the image →\n/// the dir then unlocks). A still-image-locked dir → F-028 C1 DISPLACE (a locked exe\n/// can be MOVED even when it can't be unlinked): rename the dir aside to a fresh\n/// `.live-bin.old-N` sibling + best-effort remove; a still-locked displaced dir stays\n/// uniquely-named for a later boot's GC. NEVER delete-fail-abort. Also sweeps stale\n/// `.live-bin.old*` litter a prior locked displace left behind.\n// [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\npub fn gc_live_bin_dirs(owlery: &Path) {\n    gc_live_bin_dirs_with(owlery, &|d| std::fs::remove_dir_all(d));\n}\n\n/// [`gc_live_bin_dirs`] over an injected `remove` (the crc_swap injectable-seam\n/// pattern) so the image-locked DISPLACE path is unit-testable deterministically\n/// without a real OS lock: a unit feeds a `remove` that fails for `.live-bin` and\n/// asserts the dir is DISPLACED (renamed aside), never errored/aborted.\nfn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\n    for id in perch::list_self_perch_ids(owlery) {\n        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n        let Ok(entries) = std::fs::read_dir(&perch) else {\n            continue;\n        };\n        for entry in entries.flatten() {\n            let name = entry.file_name();\n            let name = name.to_string_lossy();\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\n                continue;\n            }\n            let dir = entry.path();\n            if !dir.is_dir() {\n                continue;\n            }\n            if remove(&dir).is_ok() {\n                continue;\n            }\n            // Still image-locked. An already-displaced `.old*` just waits for the next\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\n            // perch no longer carries the stale own-copy in place. NEVER abort.\n            if name == \".live-bin\" {\n                let displaced = fresh_live_bin_old(&dir);\n                match std::fs::rename(&dir, &displaced) {\n                    Ok(()) => {\n                        let _ = remove(&displaced);\n                    }\n                    Err(e) => eprintln!(\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\n                    ),\n                }\n            }\n        }\n    }\n}\n\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n    let base = live_bin.with_file_name(\".live-bin.old\");\n    if !base.exists() {\n        return base;\n    }\n    for n in 1u64.. {\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\n        if !candidate.exists() {\n            return candidate;\n        }\n    }\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\n}\n\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n/// config are re-read each sweep (adapter / pulse-period changes land between\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n/// for symmetry — the threads die with the brain process and the next brain\n/// rehydrates from disk.\n// [impl->REQ-DAEMON-1]\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(LiveSet::new());\n        {\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\n            // residency machinery's corpse-burial, not its resurrection.\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\n            // running FROM the own-copy image is killed first (its dir then unlocks).\n            gc_live_bin_dirs(&owlery);\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            let cfg = DaemonConfig::load();\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\n            // latch for any spt-hosted endpoint whose broker session is gone — and\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\n            // so the same-tick stop-side un-hosts the now-offline perch and the\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\n            // cold start after an unclean stop from spawning N phantom Psyches for N\n            // dead-but-latched perches. Skipped only when the broker is unreachable\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\n            // run_brain connects to the broker BEFORE this host spawns, so the query\n            // returns the REAL session set: empty at a cold start (every online\n            // controllable perch is a phantom → offlined), the surviving sessions at\n            // a brain restart (live agents stay, only the dead are offlined). The\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\n            if let Some(live) = query_live_session_endpoints() {\n                reconcile_hosted_liveness(&owlery, &live);\n            }\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\n            // Sleep in slices so a stop lands promptly.\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n                left -= step;\n            }\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_home::with_home;\n    use std::time::{Duration, Instant};\n\n…\n}\n\n[Showing lines 899-1063 of 2157. Use :1064 to continue]"}],"details":{"truncation":{"content":"/// image-lock exposure now turn-bounded), so a cross-version upgrade leaves a multi-MB\r\n/// `.live-bin` per pre-W5 endpoint forever. Runs ONCE at brain start AFTER\r\n/// [`sweep_legacy_resident_psyches`] (which kills any wrapper running FROM the image →\r\n/// the dir then unlocks). A still-image-locked dir → F-028 C1 DISPLACE (a locked exe\r\n/// can be MOVED even when it can't be unlinked): rename the dir aside to a fresh\r\n/// `.live-bin.old-N` sibling + best-effort remove; a still-locked displaced dir stays\r\n/// uniquely-named for a later boot's GC. NEVER delete-fail-abort. Also sweeps stale\r\n/// `.live-bin.old*` litter a prior locked displace left behind.\r\n// [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\npub fn gc_live_bin_dirs(owlery: &Path) {\r\n    gc_live_bin_dirs_with(owlery, &|d| std::fs::remove_dir_all(d));\r\n}\r\n\r\n/// [`gc_live_bin_dirs`] over an injected `remove` (the crc_swap injectable-seam\r\n/// pattern) so the image-locked DISPLACE path is unit-testable deterministically\r\n/// without a real OS lock: a unit feeds a `remove` that fails for `.live-bin` and\r\n/// asserts the dir is DISPLACED (renamed aside), never errored/aborted.\r\nfn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\r\n    for id in perch::list_self_perch_ids(owlery) {\r\n        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\r\n        let Ok(entries) = std::fs::read_dir(&perch) else {\r\n            continue;\r\n        };\r\n        for entry in entries.flatten() {\r\n            let name = entry.file_name();\r\n            let name = name.to_string_lossy();\r\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\r\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\r\n                continue;\r\n            }\r\n            let dir = entry.path();\r\n            if !dir.is_dir() {\r\n                continue;\r\n            }\r\n            if remove(&dir).is_ok() {\r\n                continue;\r\n            }\r\n            // Still image-locked. An already-displaced `.old*` just waits for the next\r\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\r\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\r\n            // perch no longer carries the stale own-copy in place. NEVER abort.\r\n            if name == \".live-bin\" {\r\n                let displaced = fresh_live_bin_old(&dir);\r\n                match std::fs::rename(&dir, &displaced) {\r\n                    Ok(()) => {\r\n                        let _ = remove(&displaced);\r\n                    }\r\n                    Err(e) => eprintln!(\r\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\r\n                    ),\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\r\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\r\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\r\n    let base = live_bin.with_file_name(\".live-bin.old\");\r\n    if !base.exists() {\r\n        return base;\r\n    }\r\n    for n in 1u64.. {\r\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\r\n        if !candidate.exists() {\r\n            return candidate;\r\n        }\r\n    }\r\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\r\n}\r\n\r\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\r\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\r\n/// config are re-read each sweep (adapter / pulse-period changes land between\r\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\r\n/// for symmetry — the threads die with the brain process and the next brain\r\n/// rehydrates from disk.\r\n// [impl->REQ-DAEMON-1]\r\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        let set = Arc::new(LiveSet::new());\r\n        {\r\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\r\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\r\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\r\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\r\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\r\n            // residency machinery's corpse-burial, not its resurrection.\r\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\r\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\r\n            // running FROM the own-copy image is killed first (its dir then unlocks).\r\n            gc_live_bin_dirs(&owlery);\r\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\r\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\r\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\r\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\r\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\r\n            // (its sessions are live, not orphaned). Belts inside the gate.\r\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n        }\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\r\n            // unset = normal production, untouched): skip the reconcile poll+body so an\r\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\r\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\r\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\r\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\r\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\r\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\r\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\r\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\r\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\r\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\r\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\r\n                std::thread::sleep(Duration::from_millis(100));\r\n                continue;\r\n            }\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            let cfg = DaemonConfig::load();\r\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\r\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\r\n            // latch for any spt-hosted endpoint whose broker session is gone — and\r\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\r\n            // so the same-tick stop-side un-hosts the now-offline perch and the\r\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\r\n            // cold start after an unclean stop from spawning N phantom Psyches for N\r\n            // dead-but-latched perches. Skipped only when the broker is unreachable\r\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\r\n            // run_brain connects to the broker BEFORE this host spawns, so the query\r\n            // returns the REAL session set: empty at a cold start (every online\r\n            // controllable perch is a phantom → offlined), the surviving sessions at\r\n            // a brain restart (live agents stay, only the dead are offlined). The\r\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\r\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\r\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\r\n            if let Some(live) = query_live_session_endpoints() {\r\n                reconcile_hosted_liveness(&owlery, &live);\r\n            }\r\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\r\n            // Sleep in slices so a stop lands promptly.\r\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n                left -= step;\r\n            }\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 std::time::{Duration, Instant};\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":1259,"totalBytes":9471,"outputLines":165,"outputBytes":9471,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// image-lock exposure now turn-bounded), so a cross-version upgrade leaves a multi-MB\r\n/// `.live-bin` per pre-W5 endpoint forever. Runs ONCE at brain start AFTER\r\n/// [`sweep_legacy_resident_psyches`] (which kills any wrapper running FROM the image →\r\n/// the dir then unlocks). A still-image-locked dir → F-028 C1 DISPLACE (a locked exe\r\n/// can be MOVED even when it can't be unlinked): rename the dir aside to a fresh\r\n/// `.live-bin.old-N` sibling + best-effort remove; a still-locked displaced dir stays\r\n/// uniquely-named for a later boot's GC. NEVER delete-fail-abort. Also sweeps stale\r\n/// `.live-bin.old*` litter a prior locked displace left behind.\r\n// [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\npub fn gc_live_bin_dirs(owlery: &Path) {\r\n    gc_live_bin_dirs_with(owlery, &|d| std::fs::remove_dir_all(d));\r\n}\r\n\r\n/// [`gc_live_bin_dirs`] over an injected `remove` (the crc_swap injectable-seam\r\n/// pattern) so the image-locked DISPLACE path is unit-testable deterministically\r\n/// without a real OS lock: a unit feeds a `remove` that fails for `.live-bin` and\r\n/// asserts the dir is DISPLACED (renamed aside), never errored/aborted.\r\nfn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\r\n    for id in perch::list_self_perch_ids(owlery) {\r\n        let perch = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\r\n        let Ok(entries) = std::fs::read_dir(&perch) else {\r\n            continue;\r\n        };\r\n        for entry in entries.flatten() {\r\n            let name = entry.file_name();\r\n            let name = name.to_string_lossy();\r\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\r\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\r\n                continue;\r\n            }\r\n            let dir = entry.path();\r\n            if !dir.is_dir() {\r\n                continue;\r\n            }\r\n            if remove(&dir).is_ok() {\r\n                continue;\r\n            }\r\n            // Still image-locked. An already-displaced `.old*` just waits for the next\r\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\r\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\r\n            // perch no longer carries the stale own-copy in place. NEVER abort.\r\n            if name == \".live-bin\" {\r\n                let displaced = fresh_live_bin_old(&dir);\r\n                match std::fs::rename(&dir, &displaced) {\r\n                    Ok(()) => {\r\n                        let _ = remove(&displaced);\r\n                    }\r\n                    Err(e) => eprintln!(\r\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\r\n                    ),\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\r\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\r\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\r\n    let base = live_bin.with_file_name(\".live-bin.old\");\r\n    if !base.exists() {\r\n        return base;\r\n    }\r\n    for n in 1u64.. {\r\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\r\n        if !candidate.exists() {\r\n            return candidate;\r\n        }\r\n    }\r\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\r\n}\r\n\r\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\r\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\r\n/// config are re-read each sweep (adapter / pulse-period changes land between\r\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\r\n/// for symmetry — the threads die with the brain process and the next brain\r\n/// rehydrates from disk.\r\n// [impl->REQ-DAEMON-1]\r\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        let set = Arc::new(LiveSet::new());\r\n        {\r\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\r\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\r\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\r\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\r\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\r\n            // residency machinery's corpse-burial, not its resurrection.\r\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\r\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\r\n            // running FROM the own-copy image is killed first (its dir then unlocks).\r\n            gc_live_bin_dirs(&owlery);\r\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\r\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\r\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\r\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\r\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\r\n            // (its sessions are live, not orphaned). Belts inside the gate.\r\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n        }\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\r\n            // unset = normal production, untouched): skip the reconcile poll+body so an\r\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\r\n            // reap-driver in ISOLATION. This loop's own `query_live_session_endpoints()`\r\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\r\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\r\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\r\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\r\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\r\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\r\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\r\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\r\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\r\n                std::thread::sleep(Duration::from_millis(100));\r\n                continue;\r\n            }\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            let cfg = DaemonConfig::load();\r\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\r\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\r\n            // latch for any spt-hosted endpoint whose broker session is gone — and\r\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\r\n            // so the same-tick stop-side un-hosts the now-offline perch and the\r\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\r\n            // cold start after an unclean stop from spawning N phantom Psyches for N\r\n            // dead-but-latched perches. Skipped only when the broker is unreachable\r\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\r\n            // run_brain connects to the broker BEFORE this host spawns, so the query\r\n            // returns the REAL session set: empty at a cold start (every online\r\n            // controllable perch is a phantom → offlined), the surviving sessions at\r\n            // a brain restart (live agents stay, only the dead are offlined). The\r\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\r\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\r\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\r\n            if let Some(live) = query_live_session_endpoints() {\r\n                reconcile_hosted_liveness(&owlery, &live);\r\n            }\r\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\r\n            // Sleep in slices so a stop lands promptly.\r\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n                left -= step;\r\n            }\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 std::time::{Duration, Instant};\r\n\r\n…\n}","startLine":899,"lineNumbers":[899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,null,2156]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\livehost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2157,"totalBytes":9471,"outputLines":165,"outputBytes":9471,"shownRange":{"start":899,"end":1063},"nextOffset":1064}}},"isError":false,"timestamp":1784271684550}}
{"type":"message","id":"20865e06","parentId":"ea28af9b","timestamp":"2026-07-17T07:01:24.571Z","message":{"role":"toolResult","toolCallId":"call_eODamssMjFrg74Wb4MQBf7cQ|fc_0e15f89f5b7b4062016a59d343bef481948bc86363e1253bac","toolName":"read","content":[{"type":"text","text":"    /// D5 makes its *phase* durable too.\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-1]\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n        &self,\n        session_id: Option<&str>,\n        stop: &AtomicBool,\n        reason: StartReason,\n        mut on_tick: F,\n    ) {\n        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\n        // cross-clobber. `None` (the single daemon pulse) → the \"pulse\" key.\n        let key = session_id.unwrap_or(\"pulse\");\n        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\n        // A disk error must not stop the pulse: degrade to an in-memory anchor at\n        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\n        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\n            .unwrap_or(DeadlineAnchor {\n                anchor_ms: now_ms(),\n                interval_ms: interval_ms.max(1),\n            });\n        // The next fire at/after now. A fresh anchor (cold/crash) sits on-grid at\n        // `now` → fires immediately (the old loop's tick-first behavior); an\n        // update anchor lands on the next grid point (phase preserved).\n        let mut deadline = anchor.next_fire(now_ms());\n        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\n        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\n        // the initial state.\n        let mut manifest_hash = self.manifest_disk_hash();\n        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\n        // in the loop's local state — the loop is per-endpoint + single-threaded, so\n        // this is the simplest correct home (no lock, no cross-endpoint bleed).\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n        let mut turn_strikes: u32 = 0;\n        while !stop.load(Ordering::Relaxed) {\n            let now = now_ms();\n            if now < deadline {\n                // Sleep toward the deadline in small slices so a stop is honored\n                // promptly rather than after a full (possibly multi-second) wait.\n                sleep_interruptible(Duration::from_millis(deadline - now), stop);\n                continue;\n            }\n            // Pull any live-update manifest swap from disk before the tick so the\n            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\n            self.reload_manifest_if_changed(&mut manifest_hash);\n            match self.pulse_tick(session_id) {\n                Ok(report) => {\n                    // F-030 W1: feed a fired turn's outcome to the failure budget.\n                    if let Some(outcome) = &report.turn_outcome {\n                        self.note_turn_outcome(&mut turn_strikes, outcome);\n                    }\n                    on_tick(&report);\n                }\n                Err(_) => { /* a transient tick error must not kill the loop */ }\n            }\n            // Advance past the FIRED grid point AND past `now` (doyle amendment\n            // 7): strictly-after-fired keeps the double-fire guard (a same-ms tick\n            // steps exactly one grid); skip-past-now collapses a stall (an\n            // overrunning tick, or a suspend/resume that left `deadline` many\n            // grids behind) to ONE catch-up fire instead of a back-to-back storm.\n            deadline = anchor.advance_after_fire(deadline, now_ms());\n        }\n    }\n\n    /// Fetch history per the manifest `[history]` strategy (empty if none).\n    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\n        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\n        // history fetch below), so a live-update refresh never waits on it.\n        let manifest = self.cell.manifest_snapshot();\n        let Some(h) = &manifest.history else {\n            return Vec::new();\n        };\n        fetch_history(\n            h,\n            &self.base_keys(session_id),\n            &self.id,\n            ParentHint::Infer,\n            LIVE_BOUNDED_TIMEOUT,\n        )\n        .unwrap_or_default()\n    }\n\n    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the\n    /// production transition host wiring [`crate::resting::apply_event`]'s two\n    /// effect seams to the real machinery:\n    ///\n    /// - **active → (dormant|suspended)**: the transition echo commune\n    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n    ///   outgoing instance's final context delta, BEFORE the flip persists\n    ///   (KH 3.3) — it syncs to whichever instance activates next.\n    /// - **wake (→ active)**: re-activation in place catches up immediately —\n    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8\n    ///   deferral, real now that the edge exists) + the freshness-pull marker\n    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not\n    ///   at the next anti-entropy cadence).\n    ///\n    /// Registry advertisement follows on the pump's next `advertise_local`\n    /// scan — the persisted record is what it reads, epoch-bumped per row.\n    // [impl->REQ-INST-3]\n    // [impl->REQ-INST-4]\n    pub fn rest_event(\n        &self,\n        session_id: Option<&str>,\n        event: crate::resting::RestEvent,\n    ) -> Result<Option<crate::resting::EdgeReport>, String> {\n…\n    }\n\n[Showing lines 689-793 of 2890. Use :794 to continue]"}],"details":{"truncation":{"content":"    /// D5 makes its *phase* durable too.\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-1]\r\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\r\n        &self,\r\n        session_id: Option<&str>,\r\n        stop: &AtomicBool,\r\n        reason: StartReason,\r\n        mut on_tick: F,\r\n    ) {\r\n        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\r\n        // cross-clobber. `None` (the single daemon pulse) → the \"pulse\" key.\r\n        let key = session_id.unwrap_or(\"pulse\");\r\n        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\r\n        // A disk error must not stop the pulse: degrade to an in-memory anchor at\r\n        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\r\n        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\r\n            .unwrap_or(DeadlineAnchor {\r\n                anchor_ms: now_ms(),\r\n                interval_ms: interval_ms.max(1),\r\n            });\r\n        // The next fire at/after now. A fresh anchor (cold/crash) sits on-grid at\r\n        // `now` → fires immediately (the old loop's tick-first behavior); an\r\n        // update anchor lands on the next grid point (phase preserved).\r\n        let mut deadline = anchor.next_fire(now_ms());\r\n        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\r\n        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\r\n        // the initial state.\r\n        let mut manifest_hash = self.manifest_disk_hash();\r\n        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\r\n        // in the loop's local state — the loop is per-endpoint + single-threaded, so\r\n        // this is the simplest correct home (no lock, no cross-endpoint bleed).\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let mut turn_strikes: u32 = 0;\r\n        while !stop.load(Ordering::Relaxed) {\r\n            let now = now_ms();\r\n            if now < deadline {\r\n                // Sleep toward the deadline in small slices so a stop is honored\r\n                // promptly rather than after a full (possibly multi-second) wait.\r\n                sleep_interruptible(Duration::from_millis(deadline - now), stop);\r\n                continue;\r\n            }\r\n            // Pull any live-update manifest swap from disk before the tick so the\r\n            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\r\n            self.reload_manifest_if_changed(&mut manifest_hash);\r\n            match self.pulse_tick(session_id) {\r\n                Ok(report) => {\r\n                    // F-030 W1: feed a fired turn's outcome to the failure budget.\r\n                    if let Some(outcome) = &report.turn_outcome {\r\n                        self.note_turn_outcome(&mut turn_strikes, outcome);\r\n                    }\r\n                    on_tick(&report);\r\n                }\r\n                Err(_) => { /* a transient tick error must not kill the loop */ }\r\n            }\r\n            // Advance past the FIRED grid point AND past `now` (doyle amendment\r\n            // 7): strictly-after-fired keeps the double-fire guard (a same-ms tick\r\n            // steps exactly one grid); skip-past-now collapses a stall (an\r\n            // overrunning tick, or a suspend/resume that left `deadline` many\r\n            // grids behind) to ONE catch-up fire instead of a back-to-back storm.\r\n            deadline = anchor.advance_after_fire(deadline, now_ms());\r\n        }\r\n    }\r\n\r\n    /// Fetch history per the manifest `[history]` strategy (empty if none).\r\n    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\r\n        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\r\n        // history fetch below), so a live-update refresh never waits on it.\r\n        let manifest = self.cell.manifest_snapshot();\r\n        let Some(h) = &manifest.history else {\r\n            return Vec::new();\r\n        };\r\n        fetch_history(\r\n            h,\r\n            &self.base_keys(session_id),\r\n            &self.id,\r\n            ParentHint::Infer,\r\n            LIVE_BOUNDED_TIMEOUT,\r\n        )\r\n        .unwrap_or_default()\r\n    }\r\n\r\n    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the\r\n    /// production transition host wiring [`crate::resting::apply_event`]'s two\r\n    /// effect seams to the real machinery:\r\n    ///\r\n    /// - **active → (dormant|suspended)**: the transition echo commune\r\n    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\r\n    ///   outgoing instance's final context delta, BEFORE the flip persists\r\n    ///   (KH 3.3) — it syncs to whichever instance activates next.\r\n    /// - **wake (→ active)**: re-activation in place catches up immediately —\r\n    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8\r\n    ///   deferral, real now that the edge exists) + the freshness-pull marker\r\n    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not\r\n    ///   at the next anti-entropy cadence).\r\n    ///\r\n    /// Registry advertisement follows on the pump's next `advertise_local`\r\n    /// scan — the persisted record is what it reads, epoch-bumped per row.\r\n    // [impl->REQ-INST-3]\r\n    // [impl->REQ-INST-4]\r\n    pub fn rest_event(\r\n        &self,\r\n        session_id: Option<&str>,\r\n        event: crate::resting::RestEvent,\r\n    ) -> Result<Option<crate::resting::EdgeReport>, String> {\r","truncated":true,"truncatedBy":"lines","totalLines":2202,"totalBytes":5482,"outputLines":105,"outputBytes":5482,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    /// D5 makes its *phase* durable too.\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-1]\r\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\r\n        &self,\r\n        session_id: Option<&str>,\r\n        stop: &AtomicBool,\r\n        reason: StartReason,\r\n        mut on_tick: F,\r\n    ) {\r\n        // Keyed anchor (amd 6): per-session file so per-agent pulse drivers never\r\n        // cross-clobber. `None` (the single daemon pulse) → the \"pulse\" key.\r\n        let key = session_id.unwrap_or(\"pulse\");\r\n        let interval_ms = self.cfg.pulse_period.as_millis() as u64;\r\n        // A disk error must not stop the pulse: degrade to an in-memory anchor at\r\n        // `now` (fresh-phase, unpersisted) so cadence continues regardless.\r\n        let anchor = DeadlineAnchor::open(key, interval_ms, reason, now_ms())\r\n            .unwrap_or(DeadlineAnchor {\r\n                anchor_ms: now_ms(),\r\n                interval_ms: interval_ms.max(1),\r\n            });\r\n        // The next fire at/after now. A fresh anchor (cold/crash) sits on-grid at\r\n        // `now` → fires immediately (the old loop's tick-first behavior); an\r\n        // update anchor lands on the next grid point (phase preserved).\r\n        let mut deadline = anchor.next_fire(now_ms());\r\n        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a\r\n        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not\r\n        // the initial state.\r\n        let mut manifest_hash = self.manifest_disk_hash();\r\n        // F-030 W1: consecutive per-event turn-failure count (C3(b) budget). Lives\r\n        // in the loop's local state — the loop is per-endpoint + single-threaded, so\r\n        // this is the simplest correct home (no lock, no cross-endpoint bleed).\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let mut turn_strikes: u32 = 0;\r\n        while !stop.load(Ordering::Relaxed) {\r\n            let now = now_ms();\r\n            if now < deadline {\r\n                // Sleep toward the deadline in small slices so a stop is honored\r\n                // promptly rather than after a full (possibly multi-second) wait.\r\n                sleep_interruptible(Duration::from_millis(deadline - now), stop);\r\n                continue;\r\n            }\r\n            // Pull any live-update manifest swap from disk before the tick so the\r\n            // tick (psyche turn / echo) runs against the fresh manifest. (W3c)\r\n            self.reload_manifest_if_changed(&mut manifest_hash);\r\n            match self.pulse_tick(session_id) {\r\n                Ok(report) => {\r\n                    // F-030 W1: feed a fired turn's outcome to the failure budget.\r\n                    if let Some(outcome) = &report.turn_outcome {\r\n                        self.note_turn_outcome(&mut turn_strikes, outcome);\r\n                    }\r\n                    on_tick(&report);\r\n                }\r\n                Err(_) => { /* a transient tick error must not kill the loop */ }\r\n            }\r\n            // Advance past the FIRED grid point AND past `now` (doyle amendment\r\n            // 7): strictly-after-fired keeps the double-fire guard (a same-ms tick\r\n            // steps exactly one grid); skip-past-now collapses a stall (an\r\n            // overrunning tick, or a suspend/resume that left `deadline` many\r\n            // grids behind) to ONE catch-up fire instead of a back-to-back storm.\r\n            deadline = anchor.advance_after_fire(deadline, now_ms());\r\n        }\r\n    }\r\n\r\n    /// Fetch history per the manifest `[history]` strategy (empty if none).\r\n    fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {\r\n        // W3c gate (i): snapshot the manifest (guard dropped before the bounded\r\n        // history fetch below), so a live-update refresh never waits on it.\r\n        let manifest = self.cell.manifest_snapshot();\r\n        let Some(h) = &manifest.history else {\r\n            return Vec::new();\r\n        };\r\n        fetch_history(\r\n            h,\r\n            &self.base_keys(session_id),\r\n            &self.id,\r\n            ParentHint::Infer,\r\n            LIVE_BOUNDED_TIMEOUT,\r\n        )\r\n        .unwrap_or_default()\r\n    }\r\n\r\n    /// Apply a resting-state event for this endpoint (D9-2, REQ-INST-3) — the\r\n    /// production transition host wiring [`crate::resting::apply_event`]'s two\r\n    /// effect seams to the real machinery:\r\n    ///\r\n    /// - **active → (dormant|suspended)**: the transition echo commune\r\n    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\r\n    ///   outgoing instance's final context delta, BEFORE the flip persists\r\n    ///   (KH 3.3) — it syncs to whichever instance activates next.\r\n    /// - **wake (→ active)**: re-activation in place catches up immediately —\r\n    ///   [`crate::notif::resurface_at_boundary`] at the Wake boundary (the D8\r\n    ///   deferral, real now that the edge exists) + the freshness-pull marker\r\n    ///   the peer pump consumes (the D9-1 seam: pull the latest mind NOW, not\r\n    ///   at the next anti-entropy cadence).\r\n    ///\r\n    /// Registry advertisement follows on the pump's next `advertise_local`\r\n    /// scan — the persisted record is what it reads, epoch-bumped per row.\r\n    // [impl->REQ-INST-3]\r\n    // [impl->REQ-INST-4]\r\n    pub fn rest_event(\r\n        &self,\r\n        session_id: Option<&str>,\r\n        event: crate::resting::RestEvent,\r\n    ) -> Result<Option<crate::resting::EdgeReport>, String> {\r\n…\n    }","startLine":689,"lineNumbers":[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,null,816]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\lifecycle.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2890,"totalBytes":5482,"outputLines":105,"outputBytes":5482,"shownRange":{"start":689,"end":793},"nextOffset":794}}},"isError":false,"timestamp":1784271684571}}
{"type":"message","id":"ace470cf","parentId":"20865e06","timestamp":"2026-07-17T07:01:53.156Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing pulse tick thread activity**\n\n**Investigating registry stream subscriber poison**\n\n**Tracing serve_registry_feed subscription flow**"},{"type":"thinking","thinking":"**Analyzing writer thread blocking on channel**\n\n**Inspecting shared send timeout and thread naming**"},{"type":"toolCall","id":"call_Q4OxsceoP04pojO1BnU5EPFr|fc_0e15f89f5b7b4062016a59d3606e0481949a729cefca6b1b43","name":"grep","arguments":{"i":"Locating write deadline","pattern":"BRAIN_WRITE_DEADLINE|write_timeout|set_write_timeout|15","path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_FcirWeY2ErnEAQxalIMHnBIw|fc_0e15f89f5b7b4062016a59d3606e1881949eb00623f8d108f5","name":"read","arguments":{"i":"Reading subscriber writer","path":"crates/spt-daemon/src/nethost.rs","selector":"380-575"}},{"type":"toolCall","id":"call_ylR8A2xYVoHlCirj8nFCSkqq|fc_0e15f89f5b7b4062016a59d3606e248194bdf65a1d406d124c","name":"read","arguments":{"i":"Reading registry serve arm","path":"crates/spt-daemon/src/dispatch.rs","selector":"760-850,970-1055"}},{"type":"toolCall","id":"call_5eEQhAnkIc91KCx4DOTspoU1|fc_0e15f89f5b7b4062016a59d3606e28819487dec1d47e06a29b","name":"read","arguments":{"i":"Reading dispatcher constants","path":"crates/spt-daemon/src/dispatch.rs","selector":"35-105,1185-1215"}},{"type":"toolCall","id":"call_7RK6hAMTH7alA6FB358eM3eu|fc_0e15f89f5b7b4062016a59d3606e308194a1145ca6a7871178","name":"read","arguments":{"i":"Reading shared send deadline","path":"crates/spt-daemon/src/broker.rs","selector":"190-270"}},{"type":"toolCall","id":"call_SsNztx9P4Bhvjrcjch1O8J0b|fc_0e15f89f5b7b4062016a59d3606e40819484e158a9bc357c63","name":"grep","arguments":{"i":"Tracing transcript history reads","pattern":"Path B|history.log|read_to_string|locate|fetch_history|normalize_command","path":"crates/spt-live/src/history.rs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14099,"output":1139,"cacheRead":76288,"cacheWrite":0,"totalTokens":91526,"reasoningTokens":813,"cost":{"input":0.070495,"output":0.03417,"cacheRead":0.038144,"cacheWrite":0,"total":0.14280900000000002}},"stopReason":"toolUse","timestamp":1784271684613,"responseId":"resp_0e15f89f5b7b4062016a59d344f0fc8194a1d303cd729687c0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d3462c648194a6c6a7809f25992f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdNTmMW8-PBJrRyMzFsnr9R9VUIf_eX28tCgVr29WuCAgJl-rVVCRuOREN2st9dBakx1Cf_63e-rkVJT-C409IRKE7vYSqmTDb2HeR8BxOP2mur5n-PGkPQ8aKb6cyAuY90BRgKTq7_WDFvon6xtGN3p5WUxrkt8iyih0kOvAAP2k2ricbpqiFsKjs_qUzk133PJKSDeMCenV_SJBizT1dcUeRf2UbsB_DuFwNHxh-wX6ZcUYvzPs4r-mVokzC-x-389xGPpI8pVuuF7GXY_3bi-cHKjx048QMhse-y8-2ppS74McNKul904T7wOxBxgp6N9HCoS5FyhR-yV7kCNSu_RHnDo6ye4nFvuWJQBIgJr6-HBQ4AWiIVRvLpjqMoJe4IMxQavPVIwVI_7-xzDCt0lkt-E8rkG02HtfB0wjRuQ_sM1GyH_Ki2ZVOxDsgIbjdDLWE4i0B7cLlU38z5shc6XyjgJjvuwwI40rnxCb2NKtt75lW_0ymg1AKWFSkAVYUDGDCbYAf_qfbfKsO0iJCWfFYYh-VVlC0fpFbSsNxtHzYR2L5cWATzdlfO9_BPBqpM_W7d2DWuQ60EtIQL2gVJn7dsn8ghE3lOJirjHKZwp7eDxdC8k4OHM_VrLiTcG_1ubkaC1PM__hAs90rNagKSLiKuLI6RDcENJtuiIwBw5u7QwY71Yc1gsPEI_mXJ6WL68HONhaRSAJMrgzZRZblMzjLpB5Tc2YF7I6e8KsNcv_vw13kOL9ia8cdM38oa9nmUmIg55arPq6XGV2BtKsK3zRgehEPCYjIjnMdQY-YlXhpRZY0p32IbZdpnHS_uhUzR1rPKRQy8Og-nuHPXldwA2J0RWQgIPS3SN7ZlgLzaCjipYHd9kcpSL7YfYPcNI1f6VyUviifHm7wGjp5u2fHWL8t3PBwfzec7SA6ct-96EoFqaA6x7t5reprB4axzJkUh9V6CK-8SjcFKVmhYPzafrxI3qznSG74Gl3rA5uk6Qc_yvvxXeD5ZG-QbtqouZv6cTyqhTSyZwcorzGeGTOZVhTJtXq8l7AP8f7_W1Bkdi9XRnIv-mRvVdPYi8fn_PfDmH--XEOTPPN-f9frFWaNYwnSWocgHc5IxU7WJ6FGfOX5LT5tSB-VtoeMHqqn4BPmuNWZ7WTH9in5mtV1WX0Rm0HEYjuj1DTjRTEYISJ3POY5J6TntCbZ0AxgODJJIe7OPEjdoIx-FS4zkXC1HzlmkgDntx8B3sXDNjd0IbVZypuUTZpXSs4bvyMsgWWV1CJzujUAeS4pJUKa1oQ9YQsTRSLmGftbmzo5al6cvpgb_wmbThr-5LyUSgn8PvEhflv0KiH4PWj4Mh_C5-uzqSDy968b59GGd-UimBQqq011GpEWkIUihe9nudxcKvRsfkxR4Ei_J3NpzXwEDccRAOPERjoCcFWCEaol0y4a5iAKMvcsan3T6KgIPTq79mIThSo_dhIT_QCC5b-WqnINg7D9PtAuc0hVA2Ve3mJdUm7eD70_fAMe0abL-UuSnR4Sr90Q-Bqttus_gJWDquBtINw7v4ryob1nCvbRJoZbXeT_6e6_RbOLlQfon0u8CzjH-hycHc43Gq_2paNS2MyZXEDxSyw4q4lY2szdxyNX_4R_fxZvtnOGrnhnCVuc7mdMaPqua5s8BW3h1XEWJfbbwiNLxipDqgOJiQrv5uelBaHLJP58_zST3ndJEtci_z6pF-D2PPK6vMDbq3Ub9PbLvh8N2ePLJMDpCzCScZ-HeM_Jz_tzBikM9ebLxl0CyFomPKf6NxiZiV7Xt6NlPbJUSZhdtOP3ECWHLVJL2JXW19z9NQu3D23aj2woQt14NNKdtz7FedrlkypLn24sFwodbc2QAVId9gRf7ypb1cBP02oaDix7w_4SeF6CtXqtPxTIN4JS0IcWjidjmGoDkwXWV-MW4F8vzaRyGcYUCh7QU88OzZUC0r_WZN-mGQ2f-i14Kh_PnvEsMAlnN7j3hpC23shhZquq5NRX46KYmppQOgFnu69UCB2CfQ7tmocOOkxkHqboId2XtcBc848zOh1IqUCgddhu0fYckPb4-CMf3zUQlJhsMTyX2j2GCKYHBnhn1C4t1N3xAQRUaGnaKm7iXC-1nT0zy97YRhuvMPvppQRU_xYfrb7KYJ2cwjhoXEGFuqGAoU2VFHZlqRVe7OvpiyKY86HwPIJxDesYn75sLhBiBu6gpBiS1yYR0XJf4WPTwF5nQE2zRSecs78UIEeY-zgPARLRxj2PGC2GvXLlFfYsk0c_wGuVMs-jyNRLKH1rkqOtkGaTy2SO2YNrXb22tpZ2oFDSb5amkuTZMMf0jZLvNnvNhld8arCR_qkSknnkYn3Mw5P-GOAAG3V9w9xJ2g-_frH5jZS-Q8HCGyCl3pd1oRABDpO0Rctx_eAlKw_4lWDwfePhsJbUJjBdytU3pMeKZSTPBW23aMlepIvX4NBEneK3N2mpbiSXOfirK_6NM22C9V03ljDNbWqBqSnhGBZyR60VFMJPbUAYKkKTypLtldZMukvXvM-7H2ZCrD7RjQoNRPlCCzGhloHImyTOsGoBUHGiH7LjHBT0pnsgQEQmxNqu45KPXNPIjQbitN0swaBUgid9nZxdxQkyw4diBC_NcL5Z-kjn_mw4wI4Ny9f-kOM-EDI7kUPQcMHiA5rmcKsAXK8WKWRmMy01yz1owGpXhH7UR4hfYoccwEKk9b2fRPnFxUaXgBmBZ3L3MExwfVAaMzjUX2f0A7FKPVEGz6geVAGWc8NSnPw6F5LXalU0cYc3x9o9Zohke2YSqi0lVl8xEvZtHdWggUKXXsyvmKxJu5PMFFVF2jUnbtCxhPOEyj5uA4Y32orc9O6cTTC0l9hSLQrQzUKOolcgPvs7hA7xUl0OzkEZX5eg_G1x7XbJVN0xVUk6ZLojX5_rlcBw6y7QcrzMkgLxg1i9LdLLvFPylVtM-QzBmRvHvKbzFHcFEeZJ53Qgamfl4IPBU6PlgvnSpk-NNMpEuWR_jr4UulP_CcsCeFE3U0TM4VG3_NeC47F0oPb5KuG0GCCna87novDxT-iFRZRagsLjEZ5NIdy1IX3qupf03_HbQvVwwpoFdR4WtmGqWm2yUchuev2yq4zmj2Y-ohgZRiZt7vcIBhCtzqdDxy07OQU6gd6lr5Ax6N8921kcW1kdyvdQPEBh9hla0mODby0SOnfTdmpXeHECN39A8prmzXmPbt9XA_7pWN0b0tN_S0VnlXbdQ9LVZdWwxHUChPm0PhG3LbFvr3yauybTESVT9a619O3FjnCGHFx1YUQ57jRyZiyz1ej2ttiI1v9Ohg-IT82e3bKXfBEFca63KRpTwk9b8lxtVhqBhPL6XEGRqi73U3ckYFLAiRhiehUdDT2YgV2lp8Icru4H5hig-sYt3NUmRN1ThtajxBQ0wiCz9mBTb50BoPvuaT3eAHFDEB0SYBOpAMT0Mgy82cDDiPI1_nGuASxr9LQNWV0sj4uRV-0DXkqGeEUkZEUDGIbEV4WcS8c09rOmnhg_iFJFKQpmBd6i5yU52dHPKyPI7k_QKsqpRblmr1qnqodkLVry_1QHMDrgoDZGpcuBgCdADzBN-zYIH_Gfhn3MiFqP5PMTSey-80PgublnAsYccBA326QaHTOS2faERORKEL0mO6SwVkIkEryjjkWkmh7qDYEGJuNdAAIil6eLfM-e1fgYwMWXQVW8RnSRo9_9CWAVzQC6eh6q0nAC4DPmJ-GB_yhFLXgTLexo8aIzH5A7dXAuN1hSGnQYWAOY7MSpoaeUlk8HeS2j0B9vRlZM7Jb3FKWAmMjTlPn8gQRS28cKl6pxlOuexJ9iTRW1HzwAeTPW6hTdgugzAwf4aCAQQA-t9riYSvUon9_t4ieuEEjnZO6ZAOqU5kFCPE-jYPTlXrPS7quOmWTZcrptfZtv3cUoMQHbnemdykDBevarFtmW3VNdwy-79ezIkslRKl_zF18ythAHO7GLwsQdRXugzKz6m3mRr-cWgPXYDvbbWZnAsdw_23hQptlyN8K_oQm4JT5dACBvReD1-DEZRI4HJOab4oMjeOxa20lTffij9-Dz4M-hbXUr8743NLOFotN8cjwCypBTvPfearPIvZCRqsOnUAMbJTjOBK91TDcCwQDLCTfnIA_cks8cpS16E-Tm1maSFf0MyO2ATjFQ7AZ58OBTq6PgOhWjkgdfXkGB1CVM1lDVG-cgYP572WdcyzunLzjiB66glOvnjYsMBybicwqz5U3ecLfOP1ErHm2hqZmTEg","summary":[{"type":"summary_text","text":"**Analyzing pulse tick thread activity**"},{"type":"summary_text","text":"**Investigating registry stream subscriber poison**"},{"type":"summary_text","text":"**Tracing serve_registry_feed subscription flow**"}]},{"id":"rs_0e15f89f5b7b4062016a59d353e9d48194ba97263b2c2e1abc","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdNg73b56iX581e3FcQccvCOBzDCmXfjVCBct46h3RQxxFj1Z2rwvn5DBCgfaQOMP3jNLd1Y5POIwcH2OhawUKQ1R6BoLXA_4rjBFhbXgJNo7pm9AhyJtjNpkWibh1SosTNnxBDpMpKVlFs3KWCxRpu0hzWKBwuWX4Sh0ID8rgHw1ZjzXMzdoT72RTwBpoersMaC05trvHmuKblSaArjNM7as2wB6VZEMGc0Ael9wQZMmL2aq1Bq2ttE5F6yjJjYN21HmX0HsPF4569NBuB8Gk7grLndz_sXfaGNvkz9n-0vwDnPjMzBhJ8NaiB25t8FErCaudk4_iBQ6Fo4MlaIfeTJwsL3R2prY1zil1EhkXtMv9LxqMzGpFOnrFYGJwPiAOKmbZZJcKxm_rHpvptJ8PGExrZlF36cqrZWbaB3PWYMoDcPl6Me24-7XroBIO_DN98YaNuvVSHQhmeqGkRAzi5syxRX0tDSRNMEFWJs6agZtqsyFvDPrNyiEUt0KQ0TvhVjn-jCT345BhHFEmL2H5-6rjIEQgRKInnIXhap0OiQfpddDGsi-ntYZ-lycMvw8i7lhqD0VrWMzQI1LOkZVxFx4_wUnyiRi_0xYHxx5yYfusmEBjafllijBQ9gdlpJ1NlXZQPqViabwg9agCJ7mMNexF42Y6acwgREQAJ8HAd9-a0L3NuUIU0NH2KTgZNHo-0NMqCCGf1GFx2yx5V3QnrtDJYvvZRuDKmggrC6_b0G9YFomBWyA9w3q19IriU2XMH7733-_a-Iff4_qtMnqO38ut3bADruWkx92pcBYHkilnDFqRgJzi9XmFjTCuK24OU82S1B7MIWWjEwP1pv7P2Ti2gtQTyExjqNloBDMEzLG8qp1S7260pcAuYFFvd0MXJeK6C7NXtgZQuRSPkQiyyg2nFhzaOd3vGF0TC1SDIjsbHBjgeqxgmwh-Tp5_NJB1SyHFBk9Ox5uHBAdSEyN5Wxqnuez7GvZEQj1omz_0MChvqDk_sm0sQU-6P8rUeMzihh042xw0A7ls9SzwPAm9lbjJsZyh60Sz-sQ5iiUrT8JxT6uC39OqdMPv7WOMi8y93SPMDaUywYWvxIEDwQhcI7IM1OueXn_fMsS7kwHWcMqGbfQ42PyZFUBTAw2WL-OXcQDnDbxrNnYTeA6t_2h0V5AuCC0c9rqJPkX9hkUoj9xfenojA7PMHKmzbtivZ2KM9R_G6SeqSlZ8EwAWDW2DguELrhdfjh3Gs93DT7USwPbWnNE3qY9L-mv_4niXQwBTVOb4E8-i_BH0S1ei7n-cyg9HwBWBl-HbC2OX_0Nfkeg4Ug4fhbo2q_W6dy2sPBMCcKZx7AhSHvMYhIiuuuTGYhvLETlhFHhHOBzXAWa5dhw4LVqVsa4KpuGPDEs2V4AvIFMqNK141kpaKevTGFyjzp7lEUhz5ywT3uztrWs_4eDwB6RSD6SK5ooIkJxxPB7QFcmHQEIUhjq7VX-E7rO1jiO7prf_wkTkMs2w06n5i4T2kGN8l0MdfDQGftSVpgzZWcnTEDIReyE0ifqmzQUlT69X_0At0Nba_h75tGFRpNtcWqBtQwhGrEr-bDuzMVovC57Uh1toeVy0qGPl3lNRyS25dqlqRUassXmh4NFEHJQlrpROPAZFNCQS2Xvr-MQNvXug-_xD8pcBPLDiGrIPldlPv2Ebaix_Vpmn_Wi6k1zjFpSolTWx2hp3TMbcmuOlTg6ZrHNMZBEgr4yhw5dKvSpFZjNKJkZ0l5_4acbnwSNij6yKJrPYPWPbQWSbj-OmRkr-CHUwdyfWqGR2n51DGsbYu9Kmmt0pvHhc5vy_sLeVLLAS9I85OngjWRQw7DXXYXsC4gVlh19iVkKUIBfRFXYwNlfJ2lDleoo5u8Iu0yofiN20d63RGvq-naPCkVHcXWAgPVDd7TrvivL2vZUEsAiPPmmzdVoeHuctM5Odu79U54MNOUJ7gHH-PjkCcGfZilCdL3n_8_0NBrQsxBhjCo06L2nQaIoTWrTmWrhDK2CDpTzcokufHyUglfZT3ThoqeLIzSnIHNv8MTac5W_OWMszfuqyotbt7z8Qw6GTN6RHVyY-9urtl7DVCEhvD8Qi9HkcNWJQjs85erMirunYklAor7Kn2TOw2V-_5acmYpOGYrS0IisoJV3UVqU2_NgRQ8UdBtNq-K9-qmTnXJSDJAsbvb1tIIL9PdFbxPoqlflaaytAp79wk9VDJNfebIvtjkDC-RJVJoJi7xheWuTZXjQ4EsQLAEk4LYymZXKFwyeV5aGQlihFJXJoB8X6DWiTOlmvvdnTHDzw0ecZvxkKVPdMj-YrU60HFW-b0yfvtxb6tCaPWU50ul9UEm-6u55Q-O-f26i6PWDZ_NfXZ4oi85bOpWDHkeWZ9ZJjCtKCB1L0AEgWWFijxGBsg-MQulVSz0rZ3TC9xuIkLxMkyAxDcrvnkIfaOTYbBTtpvc4pUSMFs9-fFk5o0_l30mqJ8rOk_5GB1Jp3slFXFds7y3S5Z3TPxF1aeKXKM7Lehmhz9giR6-3Vvp_kOUfGdzD8fgJm3DGqDpBfkC-X6_xdov7ZANkQMvAO0Ord9ePbZmTZJxSyHfQr0e3joVqemq_-rUnSvgx-BOayishn0RjkVD8mOJfjLegWafT_hDLaYMZGKd6o2nATvjoVLwUWvLFfwz69QvQ1x3P7sqNViqpvHoHqAY3Fpn70qGWc41adhOx8nh68IER_dOsQk5l1aqdWu7RNOn8baD5H1ZjK07GRFWjt4SdaCViE0jhYyCtp4WNyYR2TsbScEMKtXbOMkFcNaoGZ-0fNZB7KtLSO4u2lHvg7Awtk_LkH7Z-_JRbQeAxwmb4fuhfIhmSd9xcCrESuf03ky-3b3j_ol_jbUCjpFtccQWP9jfqS1HjJjAWjBa2F989s7NCn-QYjFJA13Qby9AAVlkoqNm9E2t8ELMSIJe6W-edV4E8AAriqLC579NSx5QzxU3uh7bsLfWtXKpVcEnR8Dk","summary":[{"type":"summary_text","text":"**Analyzing writer thread blocking on channel**"},{"type":"summary_text","text":"**Inspecting shared send timeout and thread naming**"}]},{"id":"fc_0e15f89f5b7b4062016a59d3606e0481949a729cefca6b1b43","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating write deadline\",\"pattern\":\"BRAIN_WRITE_DEADLINE|write_timeout|set_write_timeout|15\",\"path\":\"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Q4OxsceoP04pojO1BnU5EPFr","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d3606e1881949eb00623f8d108f5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading subscriber writer\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"380-575\"}","call_id":"call_FcirWeY2ErnEAQxalIMHnBIw","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3606e248194bdf65a1d406d124c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry serve arm\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"760-850,970-1055\"}","call_id":"call_ylR8A2xYVoHlCirj8nFCSkqq","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3606e28819487dec1d47e06a29b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatcher constants\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"35-105,1185-1215\"}","call_id":"call_5eEQhAnkIc91KCx4DOTspoU1","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3606e308194a1145ca6a7871178","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading shared send deadline\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"190-270\"}","call_id":"call_7RK6hAMTH7alA6FB358eM3eu","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3606e40819484e158a9bc357c63","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing transcript history reads\",\"pattern\":\"Path B|history.log|read_to_string|locate|fetch_history|normalize_command\",\"path\":\"crates/spt-live/src/history.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_SsNztx9P4Bhvjrcjch1O8J0b","name":"grep"}]},"duration":28540.630400002003,"ttft":1572.1393000036478,"contextSnapshot":{"promptTokens":90387,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Q4OxsceoP04pojO1BnU5EPFr|fc_0e15f89f5b7b4062016a59d3606e0481949a729cefca6b1b43","toolName":"grep","startedAt":"2026-07-17T07:01:53.156Z","args":{"path":"crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/nethost.rs"},"intent":"Locating write deadline"},"id":"4ab71864","parentId":"ace470cf","timestamp":"2026-07-17T07:01:53.156Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FcirWeY2ErnEAQxalIMHnBIw|fc_0e15f89f5b7b4062016a59d3606e1881949eb00623f8d108f5","toolName":"read","startedAt":"2026-07-17T07:01:53.157Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading subscriber writer"},"id":"87973ef9","parentId":"4ab71864","timestamp":"2026-07-17T07:01:53.157Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ylR8A2xYVoHlCirj8nFCSkqq|fc_0e15f89f5b7b4062016a59d3606e248194bdf65a1d406d124c","toolName":"read","startedAt":"2026-07-17T07:01:53.157Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading registry serve arm"},"id":"7d964eeb","parentId":"87973ef9","timestamp":"2026-07-17T07:01:53.157Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5eEQhAnkIc91KCx4DOTspoU1|fc_0e15f89f5b7b4062016a59d3606e28819487dec1d47e06a29b","toolName":"read","startedAt":"2026-07-17T07:01:53.157Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatcher constants"},"id":"986723a8","parentId":"7d964eeb","timestamp":"2026-07-17T07:01:53.157Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_7RK6hAMTH7alA6FB358eM3eu|fc_0e15f89f5b7b4062016a59d3606e308194a1145ca6a7871178","toolName":"read","startedAt":"2026-07-17T07:01:53.157Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading shared send deadline"},"id":"55c9ba5a","parentId":"986723a8","timestamp":"2026-07-17T07:01:53.157Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_SsNztx9P4Bhvjrcjch1O8J0b|fc_0e15f89f5b7b4062016a59d3606e40819484e158a9bc357c63","toolName":"grep","startedAt":"2026-07-17T07:01:53.157Z","args":{"path":"crates/spt-live/src/history.rs"},"intent":"Tracing transcript history reads"},"id":"35798901","parentId":"55c9ba5a","timestamp":"2026-07-17T07:01:53.157Z"}
{"type":"message","id":"f1bc67f1","parentId":"35798901","timestamp":"2026-07-17T07:01:53.166Z","message":{"role":"toolResult","toolCallId":"call_SsNztx9P4Bhvjrcjch1O8J0b|fc_0e15f89f5b7b4062016a59d3606e40819484e158a9bc357c63","toolName":"grep","content":[{"type":"text","text":" 7|//!   history.\n*8|//! - **`locate_normalize`** — spt-core *locates* the raw transcript\n*9|//!   (`locate_template`), reads it, then pipes it through `normalize_command`\n 10|//!   bounded (raw transcript on stdin → normalized stdout).\n 11|//! - **`native`** — Path-B store: the adapter *pushed* records via\n*12|//!   `api history-log`; spt-core reads the perch's `history.log` (the T7/M2a\n 13|//!   sink in `spt::api::reporting::append_history`).\n 14|//!\n 15|//! A record is one non-empty line of normalized output. spt-core treats each as\n...\n 46|    },\n*47|    /// A `fetcher` / `normalize_command` spawn, fill, or timeout failure.\n 48|    Runtime(RuntimeError),\n 49|    /// The bounded command exited non-zero (its stderr, for the operator).\n 50|    NonZero { code: Option<i32>, stderr: String },\n*51|    /// Reading the raw transcript (`locate_normalize`) or the native log failed.\n 52|    Read(std::io::Error),\n 53|}\n 54|\n...\n 79|/// strategy. `keys` supplies the substitution values (`session_id`, etc.) for\n*80|/// the `fetcher` / `locate_template` / `normalize_command` templates; `id` +\n 81|/// `hint` resolve the native Path-B store. A bounded command that hangs is\n 82|/// killed (never an indefinite block); a missing native log yields *no* records\n 83|/// (a fresh perch), not an error.\n*84|pub fn fetch_history(\n 85|    history: &History,\n 86|    keys: &BTreeMap<String, String>,\n 87|    id: &str,\n...\n 102|        }\n*103|        HistoryStrategy::LocateNormalize => {\n*104|            let locate = history\n*105|                .locate_template\n 106|                .as_deref()\n 107|                .ok_or(HistoryError::MissingField {\n*108|                    strategy: HistoryStrategy::LocateNormalize,\n*109|                    field: \"locate_template\",\n 110|                })?;\n 111|            let normalize =\n 112|                history\n*113|                    .normalize_command\n 114|                    .as_deref()\n 115|                    .ok_or(HistoryError::MissingField {\n*116|                        strategy: HistoryStrategy::LocateNormalize,\n*117|                        field: \"normalize_command\",\n 118|                    })?;\n*119|            // Fill the locate template to find the raw transcript, read it, then\n 120|            // hand the bytes to the normalizer on stdin.\n*121|            let path = spt_runtime::runtime::fill_template(locate, keys)?;\n 122|            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\n 123|            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\n 124|            ok_or_nonzero(out).map(|s| records(&s))\n...\n 126|        HistoryStrategy::Native => {\n*127|            let log = resolve_perch_path(id, hint).join(\"history.log\");\n*128|            match std::fs::read_to_string(&log) {\n 129|                Ok(s) => Ok(records(&s)),\n 130|                // No log yet ⇒ a fresh perch with no recorded history.\n 131|                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n...\n 203|            fetcher: Some(two_line_cmd().to_string()),\n*204|            locate_template: None,\n*205|            normalize_command: None,\n 206|        };\n*207|        let recs = fetch_history(\n 208|            &h,\n 209|            &no_keys(),\n 210|            \"alice\",\n...\n 218|\n*219|    #[test] // [unit->REQ-SEAM-HISTORY] locate_normalize: read raw transcript, pipe through normalizer.\n*220|    fn locate_normalize_reads_and_normalizes() {\n 221|        let raw = tempfile::NamedTempFile::new().unwrap();\n 222|        std::fs::write(raw.path(), \"line-a\\nline-b\\n\").unwrap();\n*223|        // The locate template resolves to a path read directly (not tokenized),\n 224|        // so spaces need no quoting.\n*225|        let locate = raw.path().display().to_string();\n 226|        let h = History {\n*227|            strategy: HistoryStrategy::LocateNormalize,\n 228|            fetcher: None,\n*229|            locate_template: Some(locate),\n*230|            normalize_command: Some(passthrough().to_string()),\n 231|        };\n*232|        let recs = fetch_history(\n 233|            &h,\n 234|            &no_keys(),\n 235|            \"alice\",\n...\n 244|    #[test] // [unit->REQ-SEAM-HISTORY] native Path-B store: read the perch's\n*245|            // history.log. The lines stay OPAQUE to the history subsystem (carried\n 246|            // verbatim); the sample shape is the published digest-record contract\n 247|            // (one schema across the tree — no second discriminator).\n 248|    fn native_reads_logged_records() {\n...\n 252|            std::fs::write(\n*253|                perch.join(\"history.log\"),\n 254|                \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"hi\\\"}\\n{\\\"role\\\":\\\"agent\\\",\\\"text\\\":\\\"yo\\\"}\\n\",\n 255|            )\n 256|            .unwrap();\n...\n 259|                fetcher: None,\n*260|                locate_template: None,\n*261|                normalize_command: None,\n 262|            };\n*263|            let recs = fetch_history(\n 264|                &h,\n 265|                &no_keys(),\n 266|                \"alice\",\n...\n 281|                fetcher: None,\n*282|                locate_template: None,\n*283|                normalize_command: None,\n 284|            };\n*285|            let recs = fetch_history(\n 286|                &h,\n 287|                &no_keys(),\n 288|                \"fresh\",\n...\n 301|            fetcher: Some(sleeper().to_string()),\n*302|            locate_template: None,\n*303|            normalize_command: None,\n 304|        };\n 305|        let start = std::time::Instant::now();\n*306|        let e = fetch_history(\n 307|            &h,\n 308|            &no_keys(),\n 309|            \"alice\",\n...\n 328|            fetcher: None,\n*329|            locate_template: None,\n*330|            normalize_command: None,\n 331|        };\n*332|        let e = fetch_history(\n 333|            &h,\n 334|            &no_keys(),\n 335|            \"alice\","}],"details":{"scopePath":"crates/spt-live/src/history.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-live\\src\\history.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":44,"fileCount":1,"files":["crates/spt-live/src/history.rs"],"fileMatches":[{"path":"crates/spt-live/src/history.rs","count":44}],"truncated":false,"displayContent":"   7│//!   history.\n  *8│//! - **`locate_normalize`** — spt-core *locates* the raw transcript\n  *9│//!   (`locate_template`), reads it, then pipes it through `normalize_command`\n  10│//!   bounded (raw transcript on stdin → normalized stdout).\n  11│//! - **`native`** — Path-B store: the adapter *pushed* records via\n *12│//!   `api history-log`; spt-core reads the perch's `history.log` (the T7/M2a\n  13│//!   sink in `spt::api::reporting::append_history`).\n  14│//!\n  15│//! A record is one non-empty line of normalized output. spt-core treats each as\n    │...\n  46│    },\n *47│    /// A `fetcher` / `normalize_command` spawn, fill, or timeout failure.\n  48│    Runtime(RuntimeError),\n  49│    /// The bounded command exited non-zero (its stderr, for the operator).\n  50│    NonZero { code: Option<i32>, stderr: String },\n *51│    /// Reading the raw transcript (`locate_normalize`) or the native log failed.\n  52│    Read(std::io::Error),\n  53│}\n  54│\n    │...\n  79│/// strategy. `keys` supplies the substitution values (`session_id`, etc.) for\n *80│/// the `fetcher` / `locate_template` / `normalize_command` templates; `id` +\n  81│/// `hint` resolve the native Path-B store. A bounded command that hangs is\n  82│/// killed (never an indefinite block); a missing native log yields *no* records\n  83│/// (a fresh perch), not an error.\n *84│pub fn fetch_history(\n  85│    history: &History,\n  86│    keys: &BTreeMap<String, String>,\n  87│    id: &str,\n    │...\n 102│        }\n*103│        HistoryStrategy::LocateNormalize => {\n*104│            let locate = history\n*105│                .locate_template\n 106│                .as_deref()\n 107│                .ok_or(HistoryError::MissingField {\n*108│                    strategy: HistoryStrategy::LocateNormalize,\n*109│                    field: \"locate_template\",\n 110│                })?;\n 111│            let normalize =\n 112│                history\n*113│                    .normalize_command\n 114│                    .as_deref()\n 115│                    .ok_or(HistoryError::MissingField {\n*116│                        strategy: HistoryStrategy::LocateNormalize,\n*117│                        field: \"normalize_command\",\n 118│                    })?;\n*119│            // Fill the locate template to find the raw transcript, read it, then\n 120│            // hand the bytes to the normalizer on stdin.\n*121│            let path = spt_runtime::runtime::fill_template(locate, keys)?;\n 122│            let raw = std::fs::read(&path).map_err(HistoryError::Read)?;\n 123│            let out = run_bounded_command(normalize, keys, timeout, Some(&raw))?;\n 124│            ok_or_nonzero(out).map(|s| records(&s))\n    │...\n 126│        HistoryStrategy::Native => {\n*127│            let log = resolve_perch_path(id, hint).join(\"history.log\");\n*128│            match std::fs::read_to_string(&log) {\n 129│                Ok(s) => Ok(records(&s)),\n 130│                // No log yet ⇒ a fresh perch with no recorded history.\n 131│                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),\n    │...\n 203│            fetcher: Some(two_line_cmd().to_string()),\n*204│            locate_template: None,\n*205│            normalize_command: None,\n 206│        };\n*207│        let recs = fetch_history(\n 208│            &h,\n 209│            &no_keys(),\n 210│            \"alice\",\n    │...\n 218│\n*219│    #[test] // [unit->REQ-SEAM-HISTORY] locate_normalize: read raw transcript, pipe through normalizer.\n*220│    fn locate_normalize_reads_and_normalizes() {\n 221│        let raw = tempfile::NamedTempFile::new().unwrap();\n 222│        std::fs::write(raw.path(), \"line-a\\nline-b\\n\").unwrap();\n*223│        // The locate template resolves to a path read directly (not tokenized),\n 224│        // so spaces need no quoting.\n*225│        let locate = raw.path().display().to_string();\n 226│        let h = History {\n*227│            strategy: HistoryStrategy::LocateNormalize,\n 228│            fetcher: None,\n*229│            locate_template: Some(locate),\n*230│            normalize_command: Some(passthrough().to_string()),\n 231│        };\n*232│        let recs = fetch_history(\n 233│            &h,\n 234│            &no_keys(),\n 235│            \"alice\",\n    │...\n 244│    #[test] // [unit->REQ-SEAM-HISTORY] native Path-B store: read the perch's\n*245│            // history.log. The lines stay OPAQUE to the history subsystem (carried\n 246│            // verbatim); the sample shape is the published digest-record contract\n 247│            // (one schema across the tree — no second discriminator).\n 248│    fn native_reads_logged_records() {\n    │...\n 252│            std::fs::write(\n*253│                perch.join(\"history.log\"),\n 254│                \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"hi\\\"}\\n{\\\"role\\\":\\\"agent\\\",\\\"text\\\":\\\"yo\\\"}\\n\",\n 255│            )\n 256│            .unwrap();\n    │...\n 259│                fetcher: None,\n*260│                locate_template: None,\n*261│                normalize_command: None,\n 262│            };\n*263│            let recs = fetch_history(\n 264│                &h,\n 265│                &no_keys(),\n 266│                \"alice\",\n    │...\n 281│                fetcher: None,\n*282│                locate_template: None,\n*283│                normalize_command: None,\n 284│            };\n*285│            let recs = fetch_history(\n 286│                &h,\n 287│                &no_keys(),\n 288│                \"fresh\",\n    │...\n 301│            fetcher: Some(sleeper().to_string()),\n*302│            locate_template: None,\n*303│            normalize_command: None,\n 304│        };\n 305│        let start = std::time::Instant::now();\n*306│        let e = fetch_history(\n 307│            &h,\n 308│            &no_keys(),\n 309│            \"alice\",\n    │...\n 328│            fetcher: None,\n*329│            locate_template: None,\n*330│            normalize_command: None,\n 331│        };\n*332│        let e = fetch_history(\n 333│            &h,\n 334│            &no_keys(),\n 335│            \"alice\","},"isError":false,"timestamp":1784271713165}}
{"type":"message","id":"b77ff43a","parentId":"f1bc67f1","timestamp":"2026-07-17T07:01:53.176Z","message":{"role":"toolResult","toolCallId":"call_5eEQhAnkIc91KCx4DOTspoU1|fc_0e15f89f5b7b4062016a59d3606e28819487dec1d47e06a29b","toolName":"read","content":[{"type":"text","text":"//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n//! | anything else | unknown — dropped, loud |\n//!\n//! A family whose production wiring lands in a later slice is still\n//! classified here (the table is complete) but routed to a typed\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n//!\n//! ## Worker model\n//!\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\n//! connection (the broker is multi-client by design; the probe-brain pattern\n//! every E2E test uses). The peek and the serve use **separate** connections:\n//! the peek brain subscribes from 0 to read the first line and is dropped;\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n//! stream's event feed alone (no double-subscription interleave). Origin is\n//! the table's `remote_id_hex` — handshake-proven, never payload\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n// [impl->REQ-NET-1]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n\nuse std::collections::HashMap;\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::notif::NotifDecoder;\nuse spt_net::net::replicate::{\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n};\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\nuse spt_store::contextstore::ContextStore;\nuse spt_store::notif::NotifStore;\nuse spt_store::perch;\nuse spt_store::roster::RosterStore;\n\nuse crate::attach::serve_attach;\nuse crate::brain::{Brain, BrokerEvent};\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\nuse crate::propagate::serve_update;\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\nuse crate::relcache::ReleaseCache;\nuse crate::sync::{serve_sync, SyncPolicy};\nuse crate::wan::receive_wan;\nuse crate::xfer::serve_xfer;\n\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n/// workers block on their stream's event feed).\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum StreamFamily {\n    /// A context sync pull (`SyncRecord::Request`).\n    Sync,\n    /// An update pull (`UpdRecord::Query`).\n    Update,\n    /// A notif replication feed (`NotifRecord::Row`).\n    Notif,\n    /// A WAN message feed (untagged `WanMessage` lines).\n    WanMsg,\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\n    Registry,\n    /// A remote-drive attach (`AttachRecord::Request`).\n    Attach,\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\n    Xfer,\n…\n}\n…\nfn serve_wan_feed(\n…\n) -> DispatchOutcome {\n…\n    loop {\n        match brain.read_event() {\n…\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Spawn the dispatcher on its own thread against the canonical broker\n/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\npub fn spawn_dispatcher(\n    broker_name: String,\n    registry: Arc<RegistryHost>,\n    stop: Arc<AtomicBool>,\n) -> thread::JoinHandle<()> {\n    thread::spawn(move || {\n        let paths = DispatchPaths::production();\n        if let Err(e) =\n            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n        {\n            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n…\n}"}],"details":{"displayContent":{"text":"//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n//! | anything else | unknown — dropped, loud |\n//!\n//! A family whose production wiring lands in a later slice is still\n//! classified here (the table is complete) but routed to a typed\n//! [`Unwired`](DispatchOutcome::Unwired) drop — loud, never silent.\n//!\n//! ## Worker model\n//!\n//! Each claimed stream gets its own worker thread with its **own** brain IPC\n//! connection (the broker is multi-client by design; the probe-brain pattern\n//! every E2E test uses). The peek and the serve use **separate** connections:\n//! the peek brain subscribes from 0 to read the first line and is dropped;\n//! the serve loop then subscribes from 0 on a fresh connection so it owns its\n//! stream's event feed alone (no double-subscription interleave). Origin is\n//! the table's `remote_id_hex` — handshake-proven, never payload\n//! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.\n// [impl->REQ-NET-1]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n\nuse std::collections::HashMap;\nuse std::io;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse spt_net::net::notif::NotifDecoder;\nuse spt_net::net::replicate::{\n    FeedDecoder as RegistryDecoder, NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate,\n};\nuse spt_net::net::wanmsg::{WanDecoder, WanReply};\nuse spt_store::contextstore::ContextStore;\nuse spt_store::notif::NotifStore;\nuse spt_store::perch;\nuse spt_store::roster::RosterStore;\n\nuse crate::attach::serve_attach;\nuse crate::brain::{Brain, BrokerEvent};\nuse crate::notifsync::{apply_notif_feed, NotifPolicy};\nuse crate::propagate::serve_update;\nuse crate::registryhost::{RegistryGatePolicy, RegistryHost};\nuse crate::relcache::ReleaseCache;\nuse crate::sync::{serve_sync, SyncPolicy};\nuse crate::wan::receive_wan;\nuse crate::xfer::serve_xfer;\n\n/// Poll cadence for the stream-table watch (the dispatcher's only busy-wait;\n/// workers block on their stream's event feed).\npub const DEFAULT_DISPATCH_POLL: Duration = Duration::from_millis(100);\n\n/// What protocol a peer-initiated stream speaks, by its first record's shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum StreamFamily {\n    /// A context sync pull (`SyncRecord::Request`).\n    Sync,\n    /// An update pull (`UpdRecord::Query`).\n    Update,\n    /// A notif replication feed (`NotifRecord::Row`).\n    Notif,\n    /// A WAN message feed (untagged `WanMessage` lines).\n    WanMsg,\n    /// A registry replication feed (untagged `RegistryUpdate` lines).\n    Registry,\n    /// A remote-drive attach (`AttachRecord::Request`).\n    Attach,\n    /// A file transfer (`XferRecord::Fetch`/`Push`).\n    Xfer,\n…\n}\n…\nfn serve_wan_feed(\n…\n) -> DispatchOutcome {\n…\n    loop {\n        match brain.read_event() {\n…\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"wan feed: {handled} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Spawn the dispatcher on its own thread against the canonical broker\n/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).\npub fn spawn_dispatcher(\n    broker_name: String,\n    registry: Arc<RegistryHost>,\n    stop: Arc<AtomicBool>,\n) -> thread::JoinHandle<()> {\n    thread::spawn(move || {\n        let paths = DispatchPaths::production();\n        if let Err(e) =\n            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)\n        {\n            eprintln!(\"DISPATCH_LOOP_FAIL: {e}\");\n        }\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n…\n}","startLine":35,"lineNumbers":[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,null,117,null,1142,null,1148,null,1155,1156,null,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,null,1557]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784271713176}}
{"type":"message","id":"7a9f89c5","parentId":"b77ff43a","timestamp":"2026-07-17T07:01:53.178Z","message":{"role":"toolResult","toolCallId":"call_Q4OxsceoP04pojO1BnU5EPFr|fc_0e15f89f5b7b4062016a59d3606e0481949a729cefca6b1b43","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 167|/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n*168|/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n 169|/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n 170|/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 171|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*172|const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 173|\n*174|/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n*175|/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 176|/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n*177|/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 178|/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n 179|/// ONLY — never relaxes an assertion.\n 180|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 181|fn brain_write_deadline() -> Duration {\n*182|    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 183|        Ok(ms) => ms\n 184|            .parse::<u64>()\n 185|            .map(Duration::from_millis)\n*186|            .unwrap_or(BRAIN_WRITE_DEADLINE),\n*187|        Err(_) => BRAIN_WRITE_DEADLINE,\n 188|    }\n 189|}\n 190|\n...\n 354|// PTY whose input reader is not yet attached/settled are swallowed mid-stream (the\n*355|// pre-settle race — 1854B once landed intact, ~1150B usually swallowed = a timing\n 356|// class, not a size cap). A live-SENT inject leaves NO spool copy, so a swallow is\n 357|// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n 358|// byte (positive readiness probe, not a blind timer); (2) MUST echo-verify the head\n...\n 521|    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n*522|    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n 523|    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n 524|    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n 525|    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n...\n 995|    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n*996|    /// deliberately NOT built here. KH 7.15 held by construction: only ever CLEARS,\n 997|    /// never latches `driven_by`.\n 998|    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n 999|    fn reap_dead_controller(&mut self) -> bool {\n...\n 1011|        // writer is BLOCKED (not exited) on a suspended brain conn past\n*1012|        // BRAIN_WRITE_DEADLINE is also reaped here, so the honest `KIND_SESSIONS`\n 1013|        // truth + the off-lock converge_perch_stamps release its stale `driven_by`\n 1014|        // stamp even when nobody take/reattaches. Deferred stamp: the reap runs in\n 1015|        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n...\n 1058|\n*1059|    /// Stall-evict a controller wedged past [`BRAIN_WRITE_DEADLINE`]: clear the slot\n 1060|    /// and re-stamp `driven_by`/`controlled` (the BROKER-side release — no brain\n 1061|    /// round-trip, so `controlled-by` can never latch behind a dead transit) and\n 1062|    /// record the evict for observability (broker log now; W3 persists the stderr,\n...\n 1175|        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n*1176|        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n 1177|        // stall-evicted HERE — the take/reattach self-heals against broker truth\n 1178|        // without waiting for new output (the output-driven mark_controller_gone\n 1179|        // never fires on an idle session). Inline stamp: this path already writes the\n...\n 1356|        // controller (its `by` is `None`), and it MUST stay that way — a local\n*1357|        // controller latching driven_by trips REQ-HAZARD-DRIVEN-BY-SELFHEAL (KH 7.15).\n 1358|        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());\n 1359|        // #3 (REQ-GOSSIP-CONTROLLED-ANY): stamp a SEPARATE any-controller datum so a\n 1360|        // LOCALLY-driven endpoint (driven_by = None) still gossips as controlled to\n...\n 2679|            // Parent-match: a profile-composite `<adapter>:<profile>` session belongs\n*2680|            // to the update's parent record name (F015B — no exact-match skew).\n 2681|            spt_runtime::profile::adapter_parent_matches(&adp, adapter)\n 2682|                .then_some(dir.map(|d| (endpoint, d)))?\n 2683|        })\n...\n 2971|    /// generation whose local controller conn is black-holed reads wedged until the W2\n*2972|    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\n 2973|    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\n 2974|    /// closed old conn is simply absent → `false` at once (the normal fast promote).\n 2975|    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\n...\n 3777|            Some(mut translation) => {\n*3778|                // ACTIVITY GATE (ADR-0028, v0.15.0) + NATIVE channel override (W3):\n 3779|                // route by the endpoint's `.idle` sentinel, read ONCE here so the\n 3780|                // caller spools the right deferred-ness off this single read (no\n 3781|                // caller-side TOCTOU). An endpoint is ACTIVE until it declares idle.\n...\n 4498|            map.iter()\n*4499|                // Parent-match (F015B): a `--adapter cc:prof` session stores the\n 4500|                // composite `cc:prof`; the apply carries the parent `cc`. An exact\n 4501|                // `==` here dropped every :profile endpoint → affected=[] → the\n 4502|                // silent no-op below. Match on the parent via the shared matcher.\n...\n 4508|        };\n*4509|        // NOTE (F015B): NO early \"affected.is_empty() → report success\" return here.\n 4510|        // Once the CLI delegates the apply to the daemon there is NO CLI-side fallback\n 4511|        // swap, so returning success without swapping was a SILENT no-op (an endpoint\n 4512|        // that just exited, or — pre-fix — every :profile endpoint the exact-matcher\n## brain.rs\n 1657|    /// ORPHANS the prior local controller (the broker stops writing its output but\n*1658|    /// does not close the conn), FREEZING the session immediately; a >15s\n 1659|    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n 1660|    /// (only an actively-producing stolen session blocks long enough to evict).\n 1661|    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n## nethost.rs\n 134|enum SeatFail {\n*135|    /// The writer thread died on a failed conn write (the 15s bounded-write\n 136|    /// poison class) — halt-at-first-failure already happened writer-side.\n 137|    Poisoned,\n 138|    /// The bounded queue is full — the subscriber cannot keep up.\n...\n 144|/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\n*145|/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\n 146|/// OUTSIDE every lock. The field stall was exactly the old shape's serial\n*147|/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\n 148|///\n 149|/// Writer discipline: the thread HALTS at the FIRST failed write (no\n 150|/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\n...\n 521|            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\n*522|            // 15s bounded write — installing a replacement now would hand the\n 523|            // retry machinery a fresh replay victim. A HEALTHY prior seat is\n 524|            // DISPLACED as before (the newer-brain-takes-over contract).\n 525|            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\n...\n 2317|        let host = NetHost::start(hermetic(&Identity::generate())).expect(\"host start\");\n*2318|        host.set_quic_op_timeout(Duration::from_millis(150));\n 2319|\n 2320|        // A never-completing await (the black-holed-peer shape) fails at the bound.\n 2321|        let started = std::time::Instant::now();\n...\n 2332|        assert!(\n*2333|            elapsed >= Duration::from_millis(150),\n 2334|            \"it waited at least the bound ({elapsed:?})\"\n 2335|        );\n 2336|        assert!(\n...\n 2432|    // requester got torn-EOF instead of its reply (wansend NoReply, CI red\n*2433|    // @2a110ee, 30/150 under contention vs 0/150 with retire disabled).\n 2434|    #[test]\n 2435|    fn retired_row_still_flushes_a_late_reply() {\n 2436|        let host = NetHost::start(hermetic(&Identity::generate())).expect(\"host start\");\n...\n 2558|    // the pre-Amendment shape iterated the ring with `let _ = sub.write()`\n*2559|    // discarded errors, serially eating a 15s bounded write per frame.\n 2560|    #[test]\n 2561|    fn append_halts_at_first_failed_write_removes_the_seat_and_cancels_the_lease() {\n 2562|        let (sub, client, _recv) = seat_socket_pair(Duration::from_secs(5));\n...\n 2634|    // (flags on a live parked writer) — the deterministic stand-in for a\n*2635|    // writer stuck mid-15s bounded write; T5's int covers the composition.\n 2636|    #[test]\n 2637|    fn begin_attach_refuses_only_while_a_poisoned_prior_still_drains() {\n 2638|        let (a, _ca, _ra) = seat_socket_pair(Duration::from_secs(5));"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/brain.rs, crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":30,"fileCount":3,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-daemon/src/nethost.rs","count":9}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n  167│/// blocked). The brain aggregates every session + psyche + WAN feed, so its drain\n *168│/// is bursty; 15 s tolerates a normal burst-stall while still bounding the wedge.\n  169│/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n  170│/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n  171│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n *172│const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n  173│\n *174│/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n *175│/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n  176│/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n *177│/// the slot) is provable in ~1 s instead of 15 (mirrors the\n  178│/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n  179│/// ONLY — never relaxes an assertion.\n  180│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n  181│fn brain_write_deadline() -> Duration {\n *182│    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n  183│        Ok(ms) => ms\n  184│            .parse::<u64>()\n  185│            .map(Duration::from_millis)\n *186│            .unwrap_or(BRAIN_WRITE_DEADLINE),\n *187│        Err(_) => BRAIN_WRITE_DEADLINE,\n  188│    }\n  189│}\n  190│\n     │...\n  354│// PTY whose input reader is not yet attached/settled are swallowed mid-stream (the\n *355│// pre-settle race — 1854B once landed intact, ~1150B usually swallowed = a timing\n  356│// class, not a size cap). A live-SENT inject leaves NO spool copy, so a swallow is\n  357│// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n  358│// byte (positive readiness probe, not a blind timer); (2) MUST echo-verify the head\n     │...\n  521│    /// ([`OutputLog::controller_stalled`]) sees a `Some` older than\n *522│    /// [`BRAIN_WRITE_DEADLINE`] as a writer WEDGED on a suspended/black-holed brain\n  523│    /// conn and stall-evicts it — the signal `last_ok` (channel-handoff time) cannot\n  524│    /// give: an IDLE controller (writer parked on `rx.recv()`, `None` here) and a\n  525│    /// BLOCKED one (writer stuck in the socket write, `Some` here) both leave\n     │...\n  995│    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n *996│    /// deliberately NOT built here. KH 7.15 held by construction: only ever CLEARS,\n  997│    /// never latches `driven_by`.\n  998│    // [impl->REQ-CONTROLLER-LIVENESS-REAP]\n  999│    fn reap_dead_controller(&mut self) -> bool {\n     │...\n 1011│        // writer is BLOCKED (not exited) on a suspended brain conn past\n*1012│        // BRAIN_WRITE_DEADLINE is also reaped here, so the honest `KIND_SESSIONS`\n 1013│        // truth + the off-lock converge_perch_stamps release its stale `driven_by`\n 1014│        // stamp even when nobody take/reattaches. Deferred stamp: the reap runs in\n 1015│        // the multi-session snapshot closure (no I/O under the shared sessions lock).\n     │...\n 1058│\n*1059│    /// Stall-evict a controller wedged past [`BRAIN_WRITE_DEADLINE`]: clear the slot\n 1060│    /// and re-stamp `driven_by`/`controlled` (the BROKER-side release — no brain\n 1061│    /// round-trip, so `controlled-by` can never latch behind a dead transit) and\n 1062│    /// record the evict for observability (broker log now; W3 persists the stderr,\n     │...\n 1175│        // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): a controller WEDGED\n*1176│        // on a suspended/black-holed brain conn past BRAIN_WRITE_DEADLINE is\n 1177│        // stall-evicted HERE — the take/reattach self-heals against broker truth\n 1178│        // without waiting for new output (the output-driven mark_controller_gone\n 1179│        // never fires on an idle session). Inline stamp: this path already writes the\n     │...\n 1356│        // controller (its `by` is `None`), and it MUST stay that way — a local\n*1357│        // controller latching driven_by trips REQ-HAZARD-DRIVEN-BY-SELFHEAL (KH 7.15).\n 1358│        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());\n 1359│        // #3 (REQ-GOSSIP-CONTROLLED-ANY): stamp a SEPARATE any-controller datum so a\n 1360│        // LOCALLY-driven endpoint (driven_by = None) still gossips as controlled to\n     │...\n 2679│            // Parent-match: a profile-composite `<adapter>:<profile>` session belongs\n*2680│            // to the update's parent record name (F015B — no exact-match skew).\n 2681│            spt_runtime::profile::adapter_parent_matches(&adp, adapter)\n 2682│                .then_some(dir.map(|d| (endpoint, d)))?\n 2683│        })\n     │...\n 2971│    /// generation whose local controller conn is black-holed reads wedged until the W2\n*2972│    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\n 2973│    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\n 2974│    /// closed old conn is simply absent → `false` at once (the normal fast promote).\n 2975│    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\n     │...\n 3777│            Some(mut translation) => {\n*3778│                // ACTIVITY GATE (ADR-0028, v0.15.0) + NATIVE channel override (W3):\n 3779│                // route by the endpoint's `.idle` sentinel, read ONCE here so the\n 3780│                // caller spools the right deferred-ness off this single read (no\n 3781│                // caller-side TOCTOU). An endpoint is ACTIVE until it declares idle.\n     │...\n 4498│            map.iter()\n*4499│                // Parent-match (F015B): a `--adapter cc:prof` session stores the\n 4500│                // composite `cc:prof`; the apply carries the parent `cc`. An exact\n 4501│                // `==` here dropped every :profile endpoint → affected=[] → the\n 4502│                // silent no-op below. Match on the parent via the shared matcher.\n     │...\n 4508│        };\n*4509│        // NOTE (F015B): NO early \"affected.is_empty() → report success\" return here.\n 4510│        // Once the CLI delegates the apply to the daemon there is NO CLI-side fallback\n 4511│        // swap, so returning success without swapping was a SILENT no-op (an endpoint\n 4512│        // that just exited, or — pre-fix — every :profile endpoint the exact-matcher\n## brain.rs\n 1657│    /// ORPHANS the prior local controller (the broker stops writing its output but\n*1658│    /// does not close the conn), FREEZING the session immediately; a >15s\n 1659│    /// `stall_evict_controller` is a separate, later, output-dependent symptom\n 1660│    /// (only an actively-producing stolen session blocks long enough to evict).\n 1661│    /// That is correct for `attach` (a serve loop that WILL drive the session), but\n## nethost.rs\n  134│enum SeatFail {\n *135│    /// The writer thread died on a failed conn write (the 15s bounded-write\n  136│    /// poison class) — halt-at-first-failure already happened writer-side.\n  137│    Poisoned,\n  138│    /// The bounded queue is full — the subscriber cannot keep up.\n     │...\n  144│/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\n *145│/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\n  146│/// OUTSIDE every lock. The field stall was exactly the old shape's serial\n *147│/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\n  148│///\n  149│/// Writer discipline: the thread HALTS at the FIRST failed write (no\n  150│/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\n     │...\n  521│            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\n *522│            // 15s bounded write — installing a replacement now would hand the\n  523│            // retry machinery a fresh replay victim. A HEALTHY prior seat is\n  524│            // DISPLACED as before (the newer-brain-takes-over contract).\n  525│            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\n     │...\n 2317│        let host = NetHost::start(hermetic(&Identity::generate())).expect(\"host start\");\n*2318│        host.set_quic_op_timeout(Duration::from_millis(150));\n 2319│\n 2320│        // A never-completing await (the black-holed-peer shape) fails at the bound.\n 2321│        let started = std::time::Instant::now();\n     │...\n 2332│        assert!(\n*2333│            elapsed >= Duration::from_millis(150),\n 2334│            \"it waited at least the bound ({elapsed:?})\"\n 2335│        );\n 2336│        assert!(\n     │...\n 2432│    // requester got torn-EOF instead of its reply (wansend NoReply, CI red\n*2433│    // @2a110ee, 30/150 under contention vs 0/150 with retire disabled).\n 2434│    #[test]\n 2435│    fn retired_row_still_flushes_a_late_reply() {\n 2436│        let host = NetHost::start(hermetic(&Identity::generate())).expect(\"host start\");\n     │...\n 2558│    // the pre-Amendment shape iterated the ring with `let _ = sub.write()`\n*2559│    // discarded errors, serially eating a 15s bounded write per frame.\n 2560│    #[test]\n 2561│    fn append_halts_at_first_failed_write_removes_the_seat_and_cancels_the_lease() {\n 2562│        let (sub, client, _recv) = seat_socket_pair(Duration::from_secs(5));\n     │...\n 2634│    // (flags on a live parked writer) — the deterministic stand-in for a\n*2635│    // writer stuck mid-15s bounded write; T5's int covers the composition.\n 2636│    #[test]\n 2637│    fn begin_attach_refuses_only_while_a_poisoned_prior_still_drains() {\n 2638│        let (a, _ca, _ra) = seat_socket_pair(Duration::from_secs(5));"},"isError":false,"timestamp":1784271713178}}
{"type":"message","id":"3cb5be5a","parentId":"7a9f89c5","timestamp":"2026-07-17T07:01:53.194Z","message":{"role":"toolResult","toolCallId":"call_ylR8A2xYVoHlCirj8nFCSkqq|fc_0e15f89f5b7b4062016a59d3606e248194bdf65a1d406d124c","toolName":"read","content":[{"type":"text","text":"    let line = match first_line(broker_name, stream_id, floor_seq) {\n…\n        Err(e) if e.to_string().contains(\"no such stream\") => {\n            return WorkerReport::bare(DispatchOutcome::Served(\n                \"terminal pre-serve: row already gone\".into(),\n            ))\n        }\n        Err(e) => return WorkerReport::bare(DispatchOutcome::Failed(e.to_string())),\n    };\n    let family = classify_first_line(&line);\n    let endpoint = line_endpoint(family, &line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return WorkerReport {\n            outcome: DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into()),\n            family: Some(family),\n            endpoint,\n        };\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => {\n            return WorkerReport {\n                outcome: DispatchOutcome::Failed(e.to_string()),\n                family: Some(family),\n                endpoint,\n            }\n        }\n    };\n\n    let outcome = match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => {\n                    return WorkerReport {\n                        outcome: DispatchOutcome::Failed(e.to_string()),\n                        family: Some(family),\n                        endpoint,\n                    }\n                }\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n…\n        }\n…\n    };\n…\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n                // (a perch with a record); everything else is a no-op here.\n                // [impl->REQ-INST-3]\n                for id in flips {\n                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);\n                    if local.join(\"info.json\").exists() {\n                        if let Err(e) = crate::resting::daemon_rest_event(\n                            &id,\n                            crate::resting::RestEvent::AttentionShift,\n                            None,\n                        ) {\n                            eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");\n                        }\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Notif feed arm: decode rows as they arrive, apply each batch under the\n/// fail-closed gate, until the peer finishes. The policy re-loads per feed\n/// (not per record) — the same freshness the other arms get per request.\n///\n/// A **freshly inserted** undismissed row additionally gets a first-fire here"}],"details":{"displayContent":{"text":"    let line = match first_line(broker_name, stream_id, floor_seq) {\n…\n        Err(e) if e.to_string().contains(\"no such stream\") => {\n            return WorkerReport::bare(DispatchOutcome::Served(\n                \"terminal pre-serve: row already gone\".into(),\n            ))\n        }\n        Err(e) => return WorkerReport::bare(DispatchOutcome::Failed(e.to_string())),\n    };\n    let family = classify_first_line(&line);\n    let endpoint = line_endpoint(family, &line);\n\n    // Gate round 1 (retire-on-sight, KNOWN-HAZARDS 7.41): a finished ATTACH\n    // row's exchange is over — the operator detached — and replaying it would\n    // take the live controller (pre-existing backlog + brain-down-detach\n    // orderings, which the Served-report path can never see). Report Served\n    // WITHOUT serving: the loop's completion path retires the row, so the\n    // backlog also drains from the table. Finished request/reply rows fall\n    // through — their FIN is protocol, not completion (see\n    // `finished_row_is_terminal`).\n    if finished_row_is_terminal(family, finished) {\n        return WorkerReport {\n            outcome: DispatchOutcome::Served(\"finished attach: terminal, retired unserved\".into()),\n            family: Some(family),\n            endpoint,\n        };\n    }\n\n    let mut brain = match connect(broker_name) {\n        Ok(b) => b,\n        Err(e) => {\n            return WorkerReport {\n                outcome: DispatchOutcome::Failed(e.to_string()),\n                family: Some(family),\n                endpoint,\n            }\n        }\n    };\n\n    let outcome = match family {\n        StreamFamily::Sync => {\n            let cs = match ContextStore::open_or_init() {\n                Ok(cs) => cs,\n                Err(e) => {\n                    return WorkerReport {\n                        outcome: DispatchOutcome::Failed(e.to_string()),\n                        family: Some(family),\n                        endpoint,\n                    }\n                }\n            };\n            let policy = SyncPolicy::load();\n            match serve_sync(\n                &mut brain,\n                stream_id,\n                origin,\n                0,\n                &cs,\n                &policy,\n                &paths.scratch,\n            ) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Update => {\n            let cache = ReleaseCache::open(&paths.releases);\n            let roster = RosterStore::load();\n            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {\n                Ok(out) => DispatchOutcome::Served(format!(\"{out:?}\")),\n                Err(e) => DispatchOutcome::Failed(e.to_string()),\n            }\n        }\n        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),\n        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),\n        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),\n        StreamFamily::Attach => {\n            // The gate subject is the session's OWNING endpoint, resolved\n            // from the broker's table (the spawn's label) — never from wire\n            // bytes. An unlabeled or unknown session refuses, fail-closed.\n            //\n            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n            // cannot know our local session id, so it names the ENDPOINT. When the\n            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n            // we actually host a live session for it (D5 session-presence gate;\n            // D6: a stale registry row that named a gone endpoint resolves to no\n            // session → refuse cleanly, never a false attach). The endpoint id is\n            // WHICH endpoint the operator asks for (client-supplied, like Xfer's\n            // `endpoint`); the origin subject stays the handshake identity. The\n            // LOCAL path sends no endpoint_id and its session_id → endpoint mapping\n            // is unchanged.\n            // `endpoint` is the gate subject; `remote_session` carries the\n            // dispatcher-resolved session id on the cross-node leg (resolved HERE,\n            // before serve_attach subscribes — never mid-serve, which would swallow\n            // interleaved attach records) and `None` on the local path.\n…\n        }\n…\n    };\n…\n/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into\n/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧\n/// origin trusted in that subnet), until the peer finishes.\n///\n/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer\n/// instance of an id observed transitioning to `Active` — its node's daemon\n/// just gave it a driver, so a *locally hosted* instance of the same id\n/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`\n/// dormant). The rest edge's transition echo is gate-armed\n/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse\n/// loop runs the bounded summarizer call, never this worker (the KH 7.4\n/// non-blocking posture: N flips in one feed are N gate files, not N LLM\n/// calls serialized here).\n// [impl->REQ-INST-7]\nfn serve_registry_feed(\n    brain: &mut Brain,\n    stream_id: u64,\n    origin: &str,\n    registry: &RegistryHost,\n) -> DispatchOutcome {\n    let policy = RegistryGatePolicy::load();\n    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {\n        return DispatchOutcome::Failed(e.to_string());\n    }\n    let mut decoder = RegistryDecoder::new();\n    let mut merged = 0usize;\n    loop {\n        match brain.read_event() {\n            Ok(BrokerEvent::NetStreamData {\n                stream_id: sid,\n                bytes,\n                ..\n            }) if sid == stream_id => {\n                let records = decoder.push(&bytes);\n                if records.is_empty() {\n                    continue;\n                }\n                // Split the feed into instance rows and node-level labels\n                // (REQ-SUBNET-3) — each goes through its own gated apply path.\n                let mut updates: Vec<RegistryUpdate> = Vec::new();\n                let mut labels: Vec<NodeLabelUpdate> = Vec::new();\n                for rec in records {\n                    match rec {\n                        RegistryFeedRecord::Instance(u) => updates.push(u),\n                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),\n                    }\n                }\n                if !labels.is_empty() {\n                    merged += registry.apply_node_labels(origin, &labels, &policy).len();\n                }\n                if updates.is_empty() {\n                    continue;\n                }\n                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);\n                merged += verdicts.len();\n                // Attention shift (D9-2): only ids this node actually hosts\n                // (a perch with a record); everything else is a no-op here.\n                // [impl->REQ-INST-3]\n                for id in flips {\n                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);\n                    if local.join(\"info.json\").exists() {\n                        if let Err(e) = crate::resting::daemon_rest_event(\n                            &id,\n                            crate::resting::RestEvent::AttentionShift,\n                            None,\n                        ) {\n                            eprintln!(\"REST_SHIFT_FAIL:{id}:{e}\");\n                        }\n                    }\n                }\n            }\n            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {\n                return DispatchOutcome::Served(format!(\"registry feed: {merged} record(s)\"));\n            }\n            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),\n            Ok(_) => {}\n            Err(e) => return DispatchOutcome::Failed(e.to_string()),\n        }\n    }\n}\n\n/// Notif feed arm: decode rows as they arrive, apply each batch under the\n/// fail-closed gate, until the peer finishes. The policy re-loads per feed\n/// (not per record) — the same freshness the other arms get per request.\n///\n/// A **freshly inserted** undismissed row additionally gets a first-fire here","startLine":749,"lineNumbers":[749,null,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,null,902,null,962,null,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"}}},"isError":false,"timestamp":1784271713194}}
{"type":"message","id":"fe2beb45","parentId":"3cb5be5a","timestamp":"2026-07-17T07:01:53.213Z","message":{"role":"toolResult","toolCallId":"call_FcirWeY2ErnEAQxalIMHnBIw|fc_0e15f89f5b7b4062016a59d3606e1881949eb00623f8d108f5","toolName":"read","content":[{"type":"text","text":"    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n…\n            ring: VecDeque::new(),\n            cap_chunks,\n            subscriber: None,\n            lease: Arc::new(StreamLease::default()),\n            finished: false,\n            retentive_cap: 0,\n            opener: OpenerCapture::Buffering(Vec::new()),\n        }\n    }\n\n    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\n    /// `None` while still buffering or when capture gave up (`Oversize`).\n    fn opener_line(&self) -> Option<Vec<u8>> {\n        match &self.opener {\n            OpenerCapture::Pinned(line) => Some(line.clone()),\n            _ => None,\n        }\n    }\n\n    /// The oldest retained ring seq — the safe subscribe cursor after the ring\n    /// rolled (a `from_seq` below this floor makes the brain's gap check\n    /// reject the resume). `next_seq` when the ring is empty.\n    fn floor_seq(&self) -> u64 {\n        self.ring.front().map(|(s, _)| *s).unwrap_or(self.next_seq)\n    }\n\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\n    fn set_retentive(&mut self, cap: usize) {\n        self.retentive_cap = cap;\n    }\n\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\n    fn append(&mut self, bytes: &[u8]) {\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\n        // `Oversize` are terminal states.\n        // [impl->REQ-STREAM-OPENER-DURABLE]\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\n            match bytes.iter().position(|&b| b == b'\\n') {\n                Some(nl) => {\n                    buf.extend_from_slice(&bytes[..nl]);\n                    let line = std::mem::take(buf);\n                    self.opener = if line.len() > OPENER_PIN_MAX {\n                        OpenerCapture::Oversize\n                    } else {\n                        OpenerCapture::Pinned(line)\n                    };\n                }\n                None => {\n                    buf.extend_from_slice(bytes);\n                    if buf.len() > OPENER_PIN_MAX {\n                        self.opener = OpenerCapture::Oversize;\n                    }\n                }\n            }\n        }\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        if self.retentive_cap == 0 {\n            while self.ring.len() > self.cap_chunks {\n                self.ring.pop_front();\n            }\n        }\n        if let Some(seat) = &self.subscriber {\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\n            // ENQUEUE only (ADR-0038 Amendment fix 3): the conn I/O runs on\n            // the seat's writer thread, outside this mutex — a wedged\n            // subscriber can no longer park this producer OR the lock.\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n            if let Err(fail) = seat.enqueue(frame) {\n                self.drop_seat(fail);\n            }\n        }\n    }\n\n    /// Remove the current seat after an enqueue failure (halt-at-first-\n    /// failure, all producer sites): a POISON also cancels the stream's\n    /// lease so the paired serve/wire worker terminates (Amendment fix 4);\n    /// an OVERFLOW just detaches — the brain resumes from its cursor.\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn drop_seat(&mut self, fail: SeatFail) {\n        if let Some(seat) = self.subscriber.take() {\n            seat.sub.lifecycle_event(\n                \"stream-sub-halt\",\n                &format!(\"stream={} reason={fail:?}\", self.stream_id),\n            );\n            if fail == SeatFail::Poisoned {\n                self.lease.cancel();\n            }\n        }\n    }\n\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\n    /// pending yet.\n    fn drain(&mut self) -> (Vec<u8>, bool) {\n        let mut out = Vec::new();\n        while let Some((_, chunk)) = self.ring.pop_front() {\n            out.extend_from_slice(&chunk);\n        }\n        (out, self.finished)\n    }\n\n    /// Mark the read side finished and tell the subscriber (with the final\n    /// cursor, so it can verify completeness).\n    fn finish(&mut self) {\n        self.finished = true;\n        if let Some(seat) = &self.subscriber {\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\n            // Enqueue only — same discipline as append (Amendment fix 2:\n            // ALL producer sites halt-and-remove, finish included).\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n            if let Err(fail) = seat.enqueue(frame) {\n                self.drop_seat(fail);\n            }\n        }\n    }\n\n    /// Install `sub` as the subscriber with the replay snapshot (retained\n    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\n    /// its seat's WRITER THREAD, which drains it before any live frame — the\n    /// replay-then-live seq order is structural (see\n    /// [`SubscriberSeat::install`]), while every conn write still happens\n    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\n    /// under the lock with discarded write errors, the exact serial-poison\n    /// engine of the v0.34 field stalls). Returns the replay frame count.\n    ///\n    /// Refuses while a prior seat writer is still alive (`WouldBlock`):\n    /// a replacement subscriber is never installed until the prior one is\n    /// fully gone (REQ-DISPATCH-FALLBACK-CIRCUIT no-reinstall rule).\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\n        if let Some(prior) = &self.subscriber {\n            // No-reinstall-until-gone applies to the POISON class only\n            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\n            // 15s bounded write — installing a replacement now would hand the\n            // retry machinery a fresh replay victim. A HEALTHY prior seat is\n            // DISPLACED as before (the newer-brain-takes-over contract).\n            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\n                return Err(io::Error::new(\n                    io::ErrorKind::WouldBlock,\n                    format!(\n                        \"stream {} subscriber busy: prior subscriber still draining\",\n                        self.stream_id\n                    ),\n                ));\n            }\n            // Healthy displacement, prior fully gone, or same sub re-attach.\n            self.subscriber = None;\n        }\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\n        // record — this is one of the two RCA-pinned shared-carrier classes.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\n        sub.lifecycle_event(\n            \"stream-sub-attach\",\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\n        );\n        let mut replay: Vec<Envelope> = self\n            .ring\n            .iter()\n            .filter(|(s, _)| *s >= from_seq)\n            .map(|(seq, bytes)| net_stream_data_envelope(self.stream_id, *seq, bytes))\n            .collect();\n        if self.finished {\n            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\n        }\n        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\n        // on the fix PR): each subscriber generation gets its OWN Arc, and a\n        // prior writer — poisoned OR healthily displaced — holds only its own\n        // generation's handle. Renewal-on-canceled alone was WRONG: a healthy\n        // brain-swap displacement left the displaced writer and the successor\n        // SHARING one Arc, so the displaced writer's eventual queued-frame\n        // write failure (its conn dying — every daemon refresh) canceled the\n        // SUCCESSOR's lease: a spurious serve-kill + breaker trip per\n        // displaced stream on exactly the post-refresh path this wave calms.\n        // One poison never dead-ends the stream (T5 recovery): the next\n        // generation's mint IS the renewal; `send_stream` reads this current\n        // Arc, old workers' handles stay theirs (C2 consumes via\n        // [`NetHost::stream_lease`]).\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n        self.lease = Arc::new(StreamLease::default());\n        let replay_len = replay.len();\n        self.subscriber = Some(SubscriberSeat::install(\n            sub,\n            replay,\n            Some(Arc::clone(&self.lease)),\n        ));\n        Ok(replay_len)\n    }\n\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\n\n[Showing lines 379-578 of 2865. Use :579 to continue]"}],"details":{"truncation":{"content":"            ring: VecDeque::new(),\r\n            cap_chunks,\r\n            subscriber: None,\r\n            lease: Arc::new(StreamLease::default()),\r\n            finished: false,\r\n            retentive_cap: 0,\r\n            opener: OpenerCapture::Buffering(Vec::new()),\r\n        }\r\n    }\r\n\r\n    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\r\n    /// `None` while still buffering or when capture gave up (`Oversize`).\r\n    fn opener_line(&self) -> Option<Vec<u8>> {\r\n        match &self.opener {\r\n            OpenerCapture::Pinned(line) => Some(line.clone()),\r\n            _ => None,\r\n        }\r\n    }\r\n\r\n    /// The oldest retained ring seq — the safe subscribe cursor after the ring\r\n    /// rolled (a `from_seq` below this floor makes the brain's gap check\r\n    /// reject the resume). `next_seq` when the ring is empty.\r\n    fn floor_seq(&self) -> u64 {\r\n        self.ring.front().map(|(s, _)| *s).unwrap_or(self.next_seq)\r\n    }\r\n\r\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\r\n    fn set_retentive(&mut self, cap: usize) {\r\n        self.retentive_cap = cap;\r\n    }\r\n\r\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\r\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\r\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\r\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\r\n    fn append(&mut self, bytes: &[u8]) {\r\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\r\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\r\n        // `Oversize` are terminal states.\r\n        // [impl->REQ-STREAM-OPENER-DURABLE]\r\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\r\n            match bytes.iter().position(|&b| b == b'\\n') {\r\n                Some(nl) => {\r\n                    buf.extend_from_slice(&bytes[..nl]);\r\n                    let line = std::mem::take(buf);\r\n                    self.opener = if line.len() > OPENER_PIN_MAX {\r\n                        OpenerCapture::Oversize\r\n                    } else {\r\n                        OpenerCapture::Pinned(line)\r\n                    };\r\n                }\r\n                None => {\r\n                    buf.extend_from_slice(bytes);\r\n                    if buf.len() > OPENER_PIN_MAX {\r\n                        self.opener = OpenerCapture::Oversize;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        let seq = self.next_seq;\r\n        self.next_seq += 1;\r\n        self.ring.push_back((seq, bytes.to_vec()));\r\n        if self.retentive_cap == 0 {\r\n            while self.ring.len() > self.cap_chunks {\r\n                self.ring.pop_front();\r\n            }\r\n        }\r\n        if let Some(seat) = &self.subscriber {\r\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\r\n            // ENQUEUE only (ADR-0038 Amendment fix 3): the conn I/O runs on\r\n            // the seat's writer thread, outside this mutex — a wedged\r\n            // subscriber can no longer park this producer OR the lock.\r\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n            if let Err(fail) = seat.enqueue(frame) {\r\n                self.drop_seat(fail);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Remove the current seat after an enqueue failure (halt-at-first-\r\n    /// failure, all producer sites): a POISON also cancels the stream's\r\n    /// lease so the paired serve/wire worker terminates (Amendment fix 4);\r\n    /// an OVERFLOW just detaches — the brain resumes from its cursor.\r\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n    fn drop_seat(&mut self, fail: SeatFail) {\r\n        if let Some(seat) = self.subscriber.take() {\r\n            seat.sub.lifecycle_event(\r\n                \"stream-sub-halt\",\r\n                &format!(\"stream={} reason={fail:?}\", self.stream_id),\r\n            );\r\n            if fail == SeatFail::Poisoned {\r\n                self.lease.cancel();\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\r\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\r\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\r\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\r\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\r\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\r\n    /// pending yet.\r\n    fn drain(&mut self) -> (Vec<u8>, bool) {\r\n        let mut out = Vec::new();\r\n        while let Some((_, chunk)) = self.ring.pop_front() {\r\n            out.extend_from_slice(&chunk);\r\n        }\r\n        (out, self.finished)\r\n    }\r\n\r\n    /// Mark the read side finished and tell the subscriber (with the final\r\n    /// cursor, so it can verify completeness).\r\n    fn finish(&mut self) {\r\n        self.finished = true;\r\n        if let Some(seat) = &self.subscriber {\r\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\r\n            // Enqueue only — same discipline as append (Amendment fix 2:\r\n            // ALL producer sites halt-and-remove, finish included).\r\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n            if let Err(fail) = seat.enqueue(frame) {\r\n                self.drop_seat(fail);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Install `sub` as the subscriber with the replay snapshot (retained\r\n    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\r\n    /// its seat's WRITER THREAD, which drains it before any live frame — the\r\n    /// replay-then-live seq order is structural (see\r\n    /// [`SubscriberSeat::install`]), while every conn write still happens\r\n    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\r\n    /// under the lock with discarded write errors, the exact serial-poison\r\n    /// engine of the v0.34 field stalls). Returns the replay frame count.\r\n    ///\r\n    /// Refuses while a prior seat writer is still alive (`WouldBlock`):\r\n    /// a replacement subscriber is never installed until the prior one is\r\n    /// fully gone (REQ-DISPATCH-FALLBACK-CIRCUIT no-reinstall rule).\r\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\r\n        if let Some(prior) = &self.subscriber {\r\n            // No-reinstall-until-gone applies to the POISON class only\r\n            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\r\n            // 15s bounded write — installing a replacement now would hand the\r\n            // retry machinery a fresh replay victim. A HEALTHY prior seat is\r\n            // DISPLACED as before (the newer-brain-takes-over contract).\r\n            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\r\n                return Err(io::Error::new(\r\n                    io::ErrorKind::WouldBlock,\r\n                    format!(\r\n                        \"stream {} subscriber busy: prior subscriber still draining\",\r\n                        self.stream_id\r\n                    ),\r\n                ));\r\n            }\r\n            // Healthy displacement, prior fully gone, or same sub re-attach.\r\n            self.subscriber = None;\r\n        }\r\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\r\n        // record — this is one of the two RCA-pinned shared-carrier classes.\r\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\r\n        sub.lifecycle_event(\r\n            \"stream-sub-attach\",\r\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\r\n        );\r\n        let mut replay: Vec<Envelope> = self\r\n            .ring\r\n            .iter()\r\n            .filter(|(s, _)| *s >= from_seq)\r\n            .map(|(seq, bytes)| net_stream_data_envelope(self.stream_id, *seq, bytes))\r\n            .collect();\r\n        if self.finished {\r\n            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\r\n        }\r\n        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\r\n        // on the fix PR): each subscriber generation gets its OWN Arc, and a\r\n        // prior writer — poisoned OR healthily displaced — holds only its own\r\n        // generation's handle. Renewal-on-canceled alone was WRONG: a healthy\r\n        // brain-swap displacement left the displaced writer and the successor\r\n        // SHARING one Arc, so the displaced writer's eventual queued-frame\r\n        // write failure (its conn dying — every daemon refresh) canceled the\r\n        // SUCCESSOR's lease: a spurious serve-kill + breaker trip per\r\n        // displaced stream on exactly the post-refresh path this wave calms.\r\n        // One poison never dead-ends the stream (T5 recovery): the next\r\n        // generation's mint IS the renewal; `send_stream` reads this current\r\n        // Arc, old workers' handles stay theirs (C2 consumes via\r\n        // [`NetHost::stream_lease`]).\r\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n        self.lease = Arc::new(StreamLease::default());\r\n        let replay_len = replay.len();\r\n        self.subscriber = Some(SubscriberSeat::install(\r\n            sub,\r\n            replay,\r\n            Some(Arc::clone(&self.lease)),\r\n        ));\r\n        Ok(replay_len)\r\n    }\r\n\r\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\r","truncated":true,"truncatedBy":"lines","totalLines":2487,"totalBytes":9598,"outputLines":200,"outputBytes":9598,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn new(stream_id: u64, cap_chunks: usize) -> Self {\n        StreamLog {\n…\n            ring: VecDeque::new(),\r\n            cap_chunks,\r\n            subscriber: None,\r\n            lease: Arc::new(StreamLease::default()),\r\n            finished: false,\r\n            retentive_cap: 0,\r\n            opener: OpenerCapture::Buffering(Vec::new()),\r\n        }\r\n    }\r\n\r\n    /// The pinned opener line, if the first `\\n` has arrived (ADR-0038 B).\r\n    /// `None` while still buffering or when capture gave up (`Oversize`).\r\n    fn opener_line(&self) -> Option<Vec<u8>> {\r\n        match &self.opener {\r\n            OpenerCapture::Pinned(line) => Some(line.clone()),\r\n            _ => None,\r\n        }\r\n    }\r\n\r\n    /// The oldest retained ring seq — the safe subscribe cursor after the ring\r\n    /// rolled (a `from_seq` below this floor makes the brain's gap check\r\n    /// reject the resume). `next_seq` when the ring is empty.\r\n    fn floor_seq(&self) -> u64 {\r\n        self.ring.front().map(|(s, _)| *s).unwrap_or(self.next_seq)\r\n    }\r\n\r\n    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.\r\n    fn set_retentive(&mut self, cap: usize) {\r\n        self.retentive_cap = cap;\r\n    }\r\n\r\n    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,\r\n    /// live-send to the current subscriber. Read-pump only. A non-retentive log\r\n    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log\r\n    /// never evicts — bytes leave only via [`drain`](Self::drain).\r\n    fn append(&mut self, bytes: &[u8]) {\r\n        // Opener capture (ADR-0038 B): pin the first NDJSON line as it streams\r\n        // past, before ring bounds can ever evict it. One-shot: `Pinned` and\r\n        // `Oversize` are terminal states.\r\n        // [impl->REQ-STREAM-OPENER-DURABLE]\r\n        if let OpenerCapture::Buffering(buf) = &mut self.opener {\r\n            match bytes.iter().position(|&b| b == b'\\n') {\r\n                Some(nl) => {\r\n                    buf.extend_from_slice(&bytes[..nl]);\r\n                    let line = std::mem::take(buf);\r\n                    self.opener = if line.len() > OPENER_PIN_MAX {\r\n                        OpenerCapture::Oversize\r\n                    } else {\r\n                        OpenerCapture::Pinned(line)\r\n                    };\r\n                }\r\n                None => {\r\n                    buf.extend_from_slice(bytes);\r\n                    if buf.len() > OPENER_PIN_MAX {\r\n                        self.opener = OpenerCapture::Oversize;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        let seq = self.next_seq;\r\n        self.next_seq += 1;\r\n        self.ring.push_back((seq, bytes.to_vec()));\r\n        if self.retentive_cap == 0 {\r\n            while self.ring.len() > self.cap_chunks {\r\n                self.ring.pop_front();\r\n            }\r\n        }\r\n        if let Some(seat) = &self.subscriber {\r\n            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);\r\n            // ENQUEUE only (ADR-0038 Amendment fix 3): the conn I/O runs on\r\n            // the seat's writer thread, outside this mutex — a wedged\r\n            // subscriber can no longer park this producer OR the lock.\r\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n            if let Err(fail) = seat.enqueue(frame) {\r\n                self.drop_seat(fail);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Remove the current seat after an enqueue failure (halt-at-first-\r\n    /// failure, all producer sites): a POISON also cancels the stream's\r\n    /// lease so the paired serve/wire worker terminates (Amendment fix 4);\r\n    /// an OVERFLOW just detaches — the brain resumes from its cursor.\r\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n    fn drop_seat(&mut self, fail: SeatFail) {\r\n        if let Some(seat) = self.subscriber.take() {\r\n            seat.sub.lifecycle_event(\r\n                \"stream-sub-halt\",\r\n                &format!(\"stream={} reason={fail:?}\", self.stream_id),\r\n            );\r\n            if fail == SeatFail::Poisoned {\r\n                self.lease.cancel();\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).\r\n    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the\r\n    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an\r\n    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,\r\n    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so\r\n    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing\r\n    /// pending yet.\r\n    fn drain(&mut self) -> (Vec<u8>, bool) {\r\n        let mut out = Vec::new();\r\n        while let Some((_, chunk)) = self.ring.pop_front() {\r\n            out.extend_from_slice(&chunk);\r\n        }\r\n        (out, self.finished)\r\n    }\r\n\r\n    /// Mark the read side finished and tell the subscriber (with the final\r\n    /// cursor, so it can verify completeness).\r\n    fn finish(&mut self) {\r\n        self.finished = true;\r\n        if let Some(seat) = &self.subscriber {\r\n            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);\r\n            // Enqueue only — same discipline as append (Amendment fix 2:\r\n            // ALL producer sites halt-and-remove, finish included).\r\n            // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n            if let Err(fail) = seat.enqueue(frame) {\r\n                self.drop_seat(fail);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Install `sub` as the subscriber with the replay snapshot (retained\r\n    /// chunks with `seq >= from_seq`, then eof if already finished) handed to\r\n    /// its seat's WRITER THREAD, which drains it before any live frame — the\r\n    /// replay-then-live seq order is structural (see\r\n    /// [`SubscriberSeat::install`]), while every conn write still happens\r\n    /// OUTSIDE this mutex (the pre-Amendment shape replayed the whole ring\r\n    /// under the lock with discarded write errors, the exact serial-poison\r\n    /// engine of the v0.34 field stalls). Returns the replay frame count.\r\n    ///\r\n    /// Refuses while a prior seat writer is still alive (`WouldBlock`):\r\n    /// a replacement subscriber is never installed until the prior one is\r\n    /// fully gone (REQ-DISPATCH-FALLBACK-CIRCUIT no-reinstall rule).\r\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n    fn begin_attach(&mut self, sub: SharedSend, from_seq: u64) -> io::Result<usize> {\r\n        if let Some(prior) = &self.subscriber {\r\n            // No-reinstall-until-gone applies to the POISON class only\r\n            // (REQ-DISPATCH-FALLBACK-CIRCUIT): a wedged writer may sit in a\r\n            // 15s bounded write — installing a replacement now would hand the\r\n            // retry machinery a fresh replay victim. A HEALTHY prior seat is\r\n            // DISPLACED as before (the newer-brain-takes-over contract).\r\n            if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {\r\n                return Err(io::Error::new(\r\n                    io::ErrorKind::WouldBlock,\r\n                    format!(\r\n                        \"stream {} subscriber busy: prior subscriber still draining\",\r\n                        self.stream_id\r\n                    ),\r\n                ));\r\n            }\r\n            // Healthy displacement, prior fully gone, or same sub re-attach.\r\n            self.subscriber = None;\r\n        }\r\n        // Attribution (F-039 legs b+d): stream-subscriber role fact + attach\r\n        // record — this is one of the two RCA-pinned shared-carrier classes.\r\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        sub.describe(&format!(\"stream-subscriber stream={}\", self.stream_id));\r\n        sub.lifecycle_event(\r\n            \"stream-sub-attach\",\r\n            &format!(\"stream={} from_seq={from_seq}\", self.stream_id),\r\n        );\r\n        let mut replay: Vec<Envelope> = self\r\n            .ring\r\n            .iter()\r\n            .filter(|(s, _)| *s >= from_seq)\r\n            .map(|(seq, bytes)| net_stream_data_envelope(self.stream_id, *seq, bytes))\r\n            .collect();\r\n        if self.finished {\r\n            replay.push(net_stream_eof_envelope(self.stream_id, self.next_seq));\r\n        }\r\n        // Mint a FRESH lease at EVERY install (Amendment fix 4 + gate round 1\r\n        // on the fix PR): each subscriber generation gets its OWN Arc, and a\r\n        // prior writer — poisoned OR healthily displaced — holds only its own\r\n        // generation's handle. Renewal-on-canceled alone was WRONG: a healthy\r\n        // brain-swap displacement left the displaced writer and the successor\r\n        // SHARING one Arc, so the displaced writer's eventual queued-frame\r\n        // write failure (its conn dying — every daemon refresh) canceled the\r\n        // SUCCESSOR's lease: a spurious serve-kill + breaker trip per\r\n        // displaced stream on exactly the post-refresh path this wave calms.\r\n        // One poison never dead-ends the stream (T5 recovery): the next\r\n        // generation's mint IS the renewal; `send_stream` reads this current\r\n        // Arc, old workers' handles stay theirs (C2 consumes via\r\n        // [`NetHost::stream_lease`]).\r\n        // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n        self.lease = Arc::new(StreamLease::default());\r\n        let replay_len = replay.len();\r\n        self.subscriber = Some(SubscriberSeat::install(\r\n            sub,\r\n            replay,\r\n            Some(Arc::clone(&self.lease)),\r\n        ));\r\n        Ok(replay_len)\r\n    }\r\n\r\n    /// Detach `sub` iff it is the current subscriber (a late disconnect must\r","startLine":375,"lineNumbers":[375,376,null,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2865,"totalBytes":9598,"outputLines":200,"outputBytes":9598,"shownRange":{"start":379,"end":578},"nextOffset":579}}},"isError":false,"timestamp":1784271713213}}
{"type":"message","id":"d2a96bcf","parentId":"fe2beb45","timestamp":"2026-07-17T07:01:53.260Z","message":{"role":"toolResult","toolCallId":"call_7RK6hAMTH7alA6FB358eM3eu|fc_0e15f89f5b7b4062016a59d3606e308194a1145ca6a7871178","toolName":"read","content":[{"type":"text","text":"fn brain_write_deadline() -> Duration {\n…\n}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\nstruct WakeClaimGuard<'a> {\n    set: &'a Mutex<HashSet<String>>,\n    endpoint: String,\n}\n\nimpl Drop for WakeClaimGuard<'_> {\n    fn drop(&mut self) {\n        recover(self.set).remove(&self.endpoint);\n    }\n}\n\n/// The pure single-flight wake decision (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP), over\n/// the three observable inputs read together under the broker lock. `AlreadyLive` wins\n/// outright — the endpoint already has a live session, so a duplicate wake dedups to it\n/// (no spawn). Else a claim held by a racing spawn WITHIN the wait window is `Racing`\n/// (stand down + retry, waiting for the racer's session to register). Free — or a claim\n/// that OVERRAN the window (a leaked claim from a spawn presumed dead) — is `Claim`\n/// (proceed; availability over a vanishingly rare dup). Pure so the truth table is a unit.\n#[derive(Debug, PartialEq, Eq)]\nenum WakeGate {\n    AlreadyLive,\n    Racing,\n    Claim,\n}\n\n// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\nfn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n    if live {\n        WakeGate::AlreadyLive\n    } else if claimed && !past_deadline {\n        WakeGate::Racing\n    } else {\n        WakeGate::Claim\n    }\n}\n\n/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n/// I/O-under-log-lock) or DEFERS the stamp to the off-lock convergence (the\n/// multi-session reap closure that forbids I/O under the shared sessions lock).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n#[derive(Clone, Copy, PartialEq, Eq)]\nenum StampMode {\n    Inline,\n    Deferred,\n}\n\n/// Broker-global tally of brain-subscriber stall-evicts (W2 observability rider,\n/// doyle ruling Q3): the count since broker boot + the wall-ms of the most recent.\n/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n/// line, not a silent absence.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nstatic STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\nstatic STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n\n/// Record one stall-evict in the broker-global tally (best-effort observability).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn record_stall_evict() {\n    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n}\n\n/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n/// `last_evict_wall_ms` is 0 when none have happened. Read by the `daemon status`\n/// surface (the observability rider).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\npub(crate) fn stall_evict_stats() -> (u64, u64) {\n    (\n…\n    )\n}\n\n[Showing lines 189-273 of 6298. Use :274 to continue]"}],"details":{"truncation":{"content":"}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\nstruct WakeClaimGuard<'a> {\n    set: &'a Mutex<HashSet<String>>,\n    endpoint: String,\n}\n\nimpl Drop for WakeClaimGuard<'_> {\n    fn drop(&mut self) {\n        recover(self.set).remove(&self.endpoint);\n    }\n}\n\n/// The pure single-flight wake decision (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP), over\n/// the three observable inputs read together under the broker lock. `AlreadyLive` wins\n/// outright — the endpoint already has a live session, so a duplicate wake dedups to it\n/// (no spawn). Else a claim held by a racing spawn WITHIN the wait window is `Racing`\n/// (stand down + retry, waiting for the racer's session to register). Free — or a claim\n/// that OVERRAN the window (a leaked claim from a spawn presumed dead) — is `Claim`\n/// (proceed; availability over a vanishingly rare dup). Pure so the truth table is a unit.\n#[derive(Debug, PartialEq, Eq)]\nenum WakeGate {\n    AlreadyLive,\n    Racing,\n    Claim,\n}\n\n// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\nfn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n    if live {\n        WakeGate::AlreadyLive\n    } else if claimed && !past_deadline {\n        WakeGate::Racing\n    } else {\n        WakeGate::Claim\n    }\n}\n\n/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n/// I/O-under-log-lock) or DEFERS the stamp to the off-lock convergence (the\n/// multi-session reap closure that forbids I/O under the shared sessions lock).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n#[derive(Clone, Copy, PartialEq, Eq)]\nenum StampMode {\n    Inline,\n    Deferred,\n}\n\n/// Broker-global tally of brain-subscriber stall-evicts (W2 observability rider,\n/// doyle ruling Q3): the count since broker boot + the wall-ms of the most recent.\n/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n/// line, not a silent absence.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nstatic STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\nstatic STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n\n/// Record one stall-evict in the broker-global tally (best-effort observability).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn record_stall_evict() {\n    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n}\n\n/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n/// `last_evict_wall_ms` is 0 when none have happened. Read by the `daemon status`\n/// surface (the observability rider).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\npub(crate) fn stall_evict_stats() -> (u64, u64) {\n    (","truncated":true,"truncatedBy":"lines","totalLines":6110,"totalBytes":3790,"outputLines":85,"outputBytes":3790,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn brain_write_deadline() -> Duration {\n…\n}\n\n/// Bounded wait for a concurrent wake's session to register before a second wake for\n/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n/// over — availability over a vanishingly rare dup.\nconst WAKE_DEDUP_WAIT: Duration = Duration::from_secs(2);\n/// Poll slice while waiting on a racing wake's session to appear.\nconst WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n\n/// RAII release of a single-flight wake claim (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP):\n/// the broker holds an endpoint id in [`Broker::wake_inflight`] for the duration of a\n/// spawn so a concurrent duplicate wake stands down; the guard releases it on EVERY\n/// exit (success, an early `?` spawn/drain error, or a panic), so a failed spawn never\n/// strands the endpoint permanently claimed.\nstruct WakeClaimGuard<'a> {\n    set: &'a Mutex<HashSet<String>>,\n    endpoint: String,\n}\n\nimpl Drop for WakeClaimGuard<'_> {\n    fn drop(&mut self) {\n        recover(self.set).remove(&self.endpoint);\n    }\n}\n\n/// The pure single-flight wake decision (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP), over\n/// the three observable inputs read together under the broker lock. `AlreadyLive` wins\n/// outright — the endpoint already has a live session, so a duplicate wake dedups to it\n/// (no spawn). Else a claim held by a racing spawn WITHIN the wait window is `Racing`\n/// (stand down + retry, waiting for the racer's session to register). Free — or a claim\n/// that OVERRAN the window (a leaked claim from a spawn presumed dead) — is `Claim`\n/// (proceed; availability over a vanishingly rare dup). Pure so the truth table is a unit.\n#[derive(Debug, PartialEq, Eq)]\nenum WakeGate {\n    AlreadyLive,\n    Racing,\n    Claim,\n}\n\n// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\nfn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n    if live {\n        WakeGate::AlreadyLive\n    } else if claimed && !past_deadline {\n        WakeGate::Racing\n    } else {\n        WakeGate::Claim\n    }\n}\n\n/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n/// I/O-under-log-lock) or DEFERS the stamp to the off-lock convergence (the\n/// multi-session reap closure that forbids I/O under the shared sessions lock).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n#[derive(Clone, Copy, PartialEq, Eq)]\nenum StampMode {\n    Inline,\n    Deferred,\n}\n\n/// Broker-global tally of brain-subscriber stall-evicts (W2 observability rider,\n/// doyle ruling Q3): the count since broker boot + the wall-ms of the most recent.\n/// A brain that never returns is then a DIAGNOSABLE fact — surfaced on `daemon\n/// status` (via [`Broker::stall_evict_stats`]) alongside the per-evict broker log\n/// line, not a silent absence.\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nstatic STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\nstatic STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n\n/// Record one stall-evict in the broker-global tally (best-effort observability).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn record_stall_evict() {\n    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n}\n\n/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n/// `last_evict_wall_ms` is 0 when none have happened. Read by the `daemon status`\n/// surface (the observability rider).\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\npub(crate) fn stall_evict_stats() -> (u64, u64) {\n    (\n…\n    )\n}","startLine":181,"lineNumbers":[181,null,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,null,276,277]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6298,"totalBytes":3790,"outputLines":85,"outputBytes":3790,"shownRange":{"start":189,"end":273},"nextOffset":274}}},"isError":false,"timestamp":1784271713260}}
{"type":"message","id":"190ce050","parentId":"d2a96bcf","timestamp":"2026-07-17T07:02:03.105Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing SubscriberSeat install section**"},{"type":"toolCall","id":"call_187USHDVFI2B3T73q9VCBRF2|fc_0e15f89f5b7b4062016a59d36a6dac8194bf2a970d84bfcf17","name":"read","arguments":{"i":"Reading subscriber install loop","path":"crates/spt-daemon/src/nethost.rs","selector":"120-225"}},{"type":"toolCall","id":"call_jhIr1zSxo55s5A6fZsmMPSeE|fc_0e15f89f5b7b4062016a59d36a6dc0819485fcb3f0b6078994","name":"grep","arguments":{"i":"Finding shared send write","pattern":"struct SharedSend|type SharedSend|impl SharedSend|fn write\\(|set_write_timeout","path":"crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_z0IKPb0CCOT1e2Brfd2rmJuM|fc_0e15f89f5b7b4062016a59d36a6dcc8194b785d218daa14ede","name":"grep","arguments":{"i":"Finding pulse history triggers","pattern":"self.history\\(|history\\(session_id|pulse_tick|ingest_drops|fire_echo|sweep_stale","path":"crates/spt-daemon/src/lifecycle.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_zI0MBedMDL5mVlmq4lXOaNCR|fc_0e15f89f5b7b4062016a59d36a6dd481948c000f56450b12ae","name":"read","arguments":{"i":"Reading pulse tick work","path":"crates/spt-daemon/src/lifecycle.rs","selector":"550-690"}},{"type":"toolCall","id":"call_hvc6tJ6si3fQcGxkcdETb6CQ|fc_0e15f89f5b7b4062016a59d36a6dd88194989c73163dc0559f","name":"grep","arguments":{"i":"Finding invalidation directory polling","pattern":"pub fn pending_at|pub fn drain_at|read_dir","path":"crates/spt-store/src/projinval.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Ix4CKdFtztvSZVYeaSFj2pB0|fc_0e15f89f5b7b4062016a59d36a6de08194892be164eac0185f","name":"read","arguments":{"i":"Reading invalidation polling","path":"crates/spt-store/src/projinval.rs","selector":"80-180"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":15458,"output":345,"cacheRead":86528,"cacheWrite":0,"totalTokens":102331,"reasoningTokens":13,"cost":{"input":0.07729000000000001,"output":0.01035,"cacheRead":0.043264,"cacheWrite":0,"total":0.13090400000000002}},"stopReason":"toolUse","timestamp":1784271713301,"responseId":"resp_0e15f89f5b7b4062016a59d36192d481949e346a19f8154cf0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d36406d48194aaf52c09ddf2c2d2","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdNqbTyrM1RUFZWN8GN7xoFTuuoN2-Mx9fWVZhEv7wZdLfjvVg4kEC1scj3guhxFvugH4zc7Cxp-bO7lm3ZJtCJfR67lKLHord1_HHpo451TfkPYDag5za0r5e8RQCynwSMiESprb7ezoCXZaGIUGVKt7HRljw5CuK2wa0wxA_bWysT5tBAC_RWvNh19UWWPtuU_W43Kv7pLfbcFsW4GTQcaPcd-8U5S28KDeBG4USdM7jE2UaILmo5ujND74RNivOZ9ZngkULdO1WRciE8RZpBX46zJ8ftJ1LnUPbV3iaeaCZWPYhSON3DXYf2OBmic8lfOpV3qcszYIZK5NN2mL5txBVFYa_r5mMN_5N5CrQDvc9438nAx04hmKmONPXCrQ4Ygkr2gj-tUuCNZ_KDdzN16-d7zcPVppYQ9L-wAXDFu16tnfzKWNtVri0IBfbT0tL7ZGFdvFsyqjx1RwYAPNjuUlsJwWbk86cL7wvHsA4mB45Sa8TJoqZDxwvlADgBuqZx7GPFWJ4YOBIS-Dpsq-iGuym9C5bMtrYzyddVcchw7nPrEusBMk012SHsCF5uNGc7QYS7V7DBQb5SUfa_YtB6nvJu4W1yrZ_K9PwypUJOC6aXZTDHDdxxI_t4oUB5ql7r6aIHrkkdUjMHf5WNSLSn5j3mPR36GYAYSgSR5xFVzcMP1Ag4L5ZR_URRCQ0chHyz7kiEE1PvOJzlYAN-5j36rJ48AU4DGMBESrWGSBozCnLmcDf1-8jeZ-3PvgJ02v7N8ykpVZa_bVoyRQV5ZIbuTyul0hhDFx9jDAtsFukhGJJyLTLP0_9tU3uyNdv-_OhG-3bcD5z1q5HXwsCnyoDy7BxS98ceko8zJALy_suOxcHBqQAhFZ8N8lkKtU9CQpyDi-BG5U1fH0Ry535Z-DlSUbmR0hdmdYmukrEyqTGJ9tYwz3n7F8NE2lmpapPeHCmOLLV3lVZQZZbO5DEMz4O91TCpwO3GkfTcNMNR5ODIRxbo=","summary":[{"type":"summary_text","text":"**Reviewing SubscriberSeat install section**"}]},{"id":"fc_0e15f89f5b7b4062016a59d36a6dac8194bf2a970d84bfcf17","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading subscriber install loop\",\"path\":\"crates/spt-daemon/src/nethost.rs\",\"selector\":\"120-225\"}","call_id":"call_187USHDVFI2B3T73q9VCBRF2","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d36a6dc0819485fcb3f0b6078994","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding shared send write\",\"pattern\":\"struct SharedSend|type SharedSend|impl SharedSend|fn write\\\\(|set_write_timeout\",\"path\":\"crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_jhIr1zSxo55s5A6fZsmMPSeE","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d36a6dcc8194b785d218daa14ede","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding pulse history triggers\",\"pattern\":\"self.history\\\\(|history\\\\(session_id|pulse_tick|ingest_drops|fire_echo|sweep_stale\",\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_z0IKPb0CCOT1e2Brfd2rmJuM","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d36a6dd481948c000f56450b12ae","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading pulse tick work\",\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"selector\":\"550-690\"}","call_id":"call_zI0MBedMDL5mVlmq4lXOaNCR","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d36a6dd88194989c73163dc0559f","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding invalidation directory polling\",\"pattern\":\"pub fn pending_at|pub fn drain_at|read_dir\",\"path\":\"crates/spt-store/src/projinval.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_hvc6tJ6si3fQcGxkcdETb6CQ","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d36a6de08194892be164eac0185f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading invalidation polling\",\"path\":\"crates/spt-store/src/projinval.rs\",\"selector\":\"80-180\"}","call_id":"call_Ix4CKdFtztvSZVYeaSFj2pB0","name":"read"}]},"duration":9803.871099993587,"ttft":2744.068599998951,"contextSnapshot":{"promptTokens":101986,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_187USHDVFI2B3T73q9VCBRF2|fc_0e15f89f5b7b4062016a59d36a6dac8194bf2a970d84bfcf17","toolName":"read","startedAt":"2026-07-17T07:02:03.106Z","args":{"path":"crates/spt-daemon/src/nethost.rs"},"intent":"Reading subscriber install loop"},"id":"193645ee","parentId":"190ce050","timestamp":"2026-07-17T07:02:03.106Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jhIr1zSxo55s5A6fZsmMPSeE|fc_0e15f89f5b7b4062016a59d36a6dc0819485fcb3f0b6078994","toolName":"grep","startedAt":"2026-07-17T07:02:03.106Z","args":{"path":"crates/spt-daemon/src"},"intent":"Finding shared send write"},"id":"bc6da3e3","parentId":"193645ee","timestamp":"2026-07-17T07:02:03.106Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_z0IKPb0CCOT1e2Brfd2rmJuM|fc_0e15f89f5b7b4062016a59d36a6dcc8194b785d218daa14ede","toolName":"grep","startedAt":"2026-07-17T07:02:03.106Z","args":{"path":"crates/spt-daemon/src/lifecycle.rs"},"intent":"Finding pulse history triggers"},"id":"230486c9","parentId":"bc6da3e3","timestamp":"2026-07-17T07:02:03.106Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zI0MBedMDL5mVlmq4lXOaNCR|fc_0e15f89f5b7b4062016a59d36a6dd481948c000f56450b12ae","toolName":"read","startedAt":"2026-07-17T07:02:03.106Z","args":{"path":"crates/spt-daemon/src/lifecycle.rs"},"intent":"Reading pulse tick work"},"id":"0b36fe60","parentId":"230486c9","timestamp":"2026-07-17T07:02:03.106Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hvc6tJ6si3fQcGxkcdETb6CQ|fc_0e15f89f5b7b4062016a59d36a6dd88194989c73163dc0559f","toolName":"grep","startedAt":"2026-07-17T07:02:03.106Z","args":{"path":"crates/spt-store/src/projinval.rs"},"intent":"Finding invalidation directory polling"},"id":"5d212925","parentId":"0b36fe60","timestamp":"2026-07-17T07:02:03.106Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Ix4CKdFtztvSZVYeaSFj2pB0|fc_0e15f89f5b7b4062016a59d36a6de08194892be164eac0185f","toolName":"read","startedAt":"2026-07-17T07:02:03.106Z","args":{"path":"crates/spt-store/src/projinval.rs"},"intent":"Reading invalidation polling"},"id":"f8a51be3","parentId":"5d212925","timestamp":"2026-07-17T07:02:03.106Z"}
{"type":"message","id":"a02e7610","parentId":"f8a51be3","timestamp":"2026-07-17T07:02:03.116Z","message":{"role":"toolResult","toolCallId":"call_hvc6tJ6si3fQcGxkcdETb6CQ|fc_0e15f89f5b7b4062016a59d36a6dd88194989c73163dc0559f","toolName":"grep","content":[{"type":"text","text":" 10|//! `$SPT_HOME/index/invalidations/`; the writer polls the directory cheaply\n*11|//! (one `read_dir` — pure file I/O, no git), DRAINS all pending events, and\n 12|//! coalesces them into one refresh (multiple invalidations → one reconcile).\n 13|//! Daemon-offline nudges simply accumulate and are consumed at the next brain\n 14|//! start — the same recover-by-reading-state stance as the rest of the store.\n...\n 90|/// Whether any event is pending in `dir` — the writer's cheap poll (one\n*91|/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\n*92|pub fn pending_at(dir: &Path) -> bool {\n*93|    std::fs::read_dir(dir)\n 94|        .map(|mut d| d.next().is_some())\n 95|        .unwrap_or(false)\n 96|}\n...\n 103|// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*104|pub fn drain_at(dir: &Path) -> Vec<InvalScope> {\n*105|    let Ok(entries) = std::fs::read_dir(dir) else {\n 106|        return Vec::new();\n 107|    };\n 108|    let mut out = Vec::new();"}],"details":{"scopePath":"crates/spt-store/src/projinval.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\projinval.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":6,"fileCount":1,"files":["crates/spt-store/src/projinval.rs"],"fileMatches":[{"path":"crates/spt-store/src/projinval.rs","count":6}],"truncated":false,"displayContent":"  10│//! `$SPT_HOME/index/invalidations/`; the writer polls the directory cheaply\n *11│//! (one `read_dir` — pure file I/O, no git), DRAINS all pending events, and\n  12│//! coalesces them into one refresh (multiple invalidations → one reconcile).\n  13│//! Daemon-offline nudges simply accumulate and are consumed at the next brain\n  14│//! start — the same recover-by-reading-state stance as the rest of the store.\n    │...\n  90│/// Whether any event is pending in `dir` — the writer's cheap poll (one\n *91│/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\n *92│pub fn pending_at(dir: &Path) -> bool {\n *93│    std::fs::read_dir(dir)\n  94│        .map(|mut d| d.next().is_some())\n  95│        .unwrap_or(false)\n  96│}\n    │...\n 103│// [impl->REQ-PROJECT-INDEX-INVALIDATION]\n*104│pub fn drain_at(dir: &Path) -> Vec<InvalScope> {\n*105│    let Ok(entries) = std::fs::read_dir(dir) else {\n 106│        return Vec::new();\n 107│    };\n 108│    let mut out = Vec::new();"},"isError":false,"timestamp":1784271723116}}
{"type":"message","id":"2a1be54a","parentId":"a02e7610","timestamp":"2026-07-17T07:02:03.117Z","message":{"role":"toolResult","toolCallId":"call_z0IKPb0CCOT1e2Brfd2rmJuM|fc_0e15f89f5b7b4062016a59d36a6dcc8194b785d218daa14ede","toolName":"grep","content":[{"type":"text","text":" 11|//! a re-host, not a rewrite: the heavy lifting stays in the already-pure\n*12|//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],\n*13|//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is\n 14|//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 — the\n 15|//! resident `spawn_psyche` seam retired); the ordering invariants\n 16|//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live\n...\n 26|use spt_live::{\n*27|    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n*28|    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n 29|};\n 30|use spt_runtime::{Manifest, ManifestRuntime};\n 31|use spt_store::epoch::EpochSource;\n...\n 433|        // relative `.claude` that parent is \"\" → a garbage project_id, and drop_dirs read\n*434|        // relative to the daemon's cwd → the store never filled. Both moved to pulse_tick.\n 435|        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);\n 436|        // [impl->REQ-INSTALL-11] the cell pins the adapter install dir so the\n 437|        // psyche role's bare program token resolves there before PATH (a\n...\n 556|        if let Some(dir) = &self.signoff_dir {\n*557|            let _ = sweep_stale_signoff(dir, &self.id);\n 558|        }\n 559|    }\n 560|\n...\n 584|    /// echo-commune (T3).\n*585|    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n 586|        let now = now_ms();\n 587|        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n 588|        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n...\n 628|                    let mut got =\n*629|                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\n 630|                            .map_err(|e| e.to_string())?;\n 631|                    ingested.append(&mut got);\n 632|                }\n...\n 637|        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\n*638|        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\n 639|        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\n 640|        // below can never void an already-relayed turn. The commune-sync is NON-\n 641|        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\n...\n 649|                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\n*650|                let commune_res = self.fire_echo(session_id);\n 651|                if let Err(e) = &commune_res {\n 652|                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\n 653|                }\n...\n 677|    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n*678|    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n 679|    /// restart instant on every restart. Now the loop rehydrates a\n 680|    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n 681|    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n...\n 733|            self.reload_manifest_if_changed(&mut manifest_hash);\n*734|            match self.pulse_tick(session_id) {\n 735|                Ok(report) => {\n 736|                    // F-030 W1: feed a fired turn's outcome to the failure budget.\n 737|                    if let Some(outcome) = &report.turn_outcome {\n...\n 775|    /// - **active → (dormant|suspended)**: the transition echo commune\n*776|    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n 777|    ///   outgoing instance's final context delta, BEFORE the flip persists\n 778|    ///   (KH 3.3) — it syncs to whichever instance activates next.\n 779|    /// - **wake (→ active)**: re-activation in place catches up immediately —\n...\n 799|            now_ms(),\n*800|            || self.fire_echo(session_id),\n 801|            || self.fire_wake_effects(),\n 802|        )?;\n 803|        // The shell leg of the edge (M5-D4a): suspend closes online shells,\n...\n 842|    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n*843|    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n 844|        let Some(raw) = &self.commune_dir else {\n 845|            return Ok(());\n 846|        };\n...\n 858|        };\n*859|        let history = self.history(session_id);\n 860|        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n 861|        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n 862|        // dir so the summarizer child runs where the endpoint lives, never in the\n...\n 1128|        let owlery = perch::owlery_dir();\n*1129|        // Project routing mirrors `pulse_tick`: an owlery-internal (psyche-host) cwd\n 1130|        // routes NO project; a real cwd routes its project tier; no cwd → no project.\n 1131|        let project_id = match endpoint_cwd.as_deref() {\n 1132|            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\n...\n 1261|            || {\n*1262|                if let Err(e) = self.fire_echo(session_id) {\n 1263|                    echo_err = Some(e);\n 1264|                }\n 1265|                Ok(())\n...\n 1394|    // A live manifest that ALSO declares an echo-commune summarizer, so\n*1395|    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.\n 1396|    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {\n 1397|        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n 1398|        let toml = format!(\n...\n 1540|    #[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.\n*1541|    fn pulse_tick_ingests_drop() {\n 1542|        with_home(|_| {\n 1543|            let drops = tempfile::tempdir().unwrap();\n 1544|            let manifest = live_manifest(drops.path());\n...\n 1548|            std::fs::write(drops.path().join(\"doyle-commune.md\"), \"brief\").unwrap();\n*1549|            let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 1550|            assert_eq!(report.ingested.len(), 1, \"the drop was folded in\");\n 1551|            assert!(!report.echo_fired, \"no echo gate armed\");\n 1552|            assert!(\n...\n 2022|\n*2023|            let report = host.pulse_tick(Some(\"sid-si1\")).expect(\"tick\");\n 2024|            assert_eq!(\n 2025|                report.ingested.len(),\n 2026|                1,\n...\n 2101|\n*2102|            host.fire_echo(None).expect(\"echo fires clean\");\n 2103|\n 2104|            // The drop landed under the ENDPOINT cwd, not the daemon's.\n 2105|            let drop_path = project.path().join(\".claude\").join(\"doyle-commune.md\");\n...\n 2118|    // [unit->REQ-ECHO-DROP-DIR-RESOLVE] a RELATIVE commune_dir with NO recorded\n*2119|    // endpoint cwd = LOUD SKIP: fire_echo returns Ok (no strike), the summarizer is\n 2120|    // NEVER spawned, and nothing is written into whatever cwd the daemon happens to\n 2121|    // hold. RED pre-fix: the echo ran and wrote (or os-error-5'd) against the\n 2122|    // daemon's own process cwd.\n...\n 2164|\n*2165|            let out = host.fire_echo(None);\n 2166|            assert!(out.is_ok(), \"a no-cwd skip is Ok (no strike), got {out:?}\");\n 2167|            assert!(\n 2168|                !marker.exists(),\n...\n 2190|            // Active → Dormant: the checkpoint/suspend boundary — fires the\n*2191|            // transition echo (KH 3.3) through fire_echo.\n 2192|            let report = host\n 2193|                .rest_event(None, crate::resting::RestEvent::Detach)\n 2194|                .expect(\"rest edge applies\")\n...\n 2241|\n*2242|                let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2243|                let outcome = report.turn_outcome.expect(\"gate was armed — fire ran\");\n 2244|                assert!(\n 2245|                    outcome.is_ok(),\n...\n 2394|            // Recordless (pre-D9-2 parity): the tick stamps recency.\n*2395|            host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2396|            let stamped = read_info(&perch_path)\n 2397|                .unwrap()\n 2398|                .last_active_ms\n...\n 2404|                .unwrap();\n*2405|            host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2406|            let rec = read_info(&perch_path).unwrap();\n 2407|            assert_eq!(\n 2408|                rec.last_active_ms,\n...\n 2418|            // Suspended: the tick neither stamps nor transitions.\n*2419|            host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2420|            let rec = read_info(&perch_path).unwrap();\n 2421|            assert_eq!(rec.last_active_ms, Some(stamped));\n 2422|            assert_eq!(rec.rest_state.as_deref(), Some(\"suspended\"));"}],"details":{"scopePath":"crates/spt-daemon/src/lifecycle.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\lifecycle.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":30,"fileCount":1,"files":["crates/spt-daemon/src/lifecycle.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/lifecycle.rs","count":30}],"truncated":false,"displayContent":"   11│//! a re-host, not a rewrite: the heavy lifting stays in the already-pure\n  *12│//! [`spt_live`] seams ([`ingest_drops`], [`run_echo_commune`],\n  *13│//! [`graceful_signoff`], [`sweep_stale_signoff`]); the per-event psyche turn is\n   14│//! driven in-brain (`BrainLifecycle::run_psyche_event_turn`, F-030 — the\n   15│//! resident `spawn_psyche` seam retired); the ordering invariants\n   16│//! (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-sweep 3.2) live\n     │...\n   26│use spt_live::{\n  *27│    download_psyche_context, fetch_history, graceful_signoff, ingest_drops, run_echo_commune,\n  *28│    sweep_stale_signoff, HistoryRecord, Ingested, SignoffOutcome, TurnError,\n   29│};\n   30│use spt_runtime::{Manifest, ManifestRuntime};\n   31│use spt_store::epoch::EpochSource;\n     │...\n  433│        // relative `.claude` that parent is \"\" → a garbage project_id, and drop_dirs read\n *434│        // relative to the daemon's cwd → the store never filled. Both moved to pulse_tick.\n  435│        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);\n  436│        // [impl->REQ-INSTALL-11] the cell pins the adapter install dir so the\n  437│        // psyche role's bare program token resolves there before PATH (a\n     │...\n  556│        if let Some(dir) = &self.signoff_dir {\n *557│            let _ = sweep_stale_signoff(dir, &self.id);\n  558│        }\n  559│    }\n  560│\n     │...\n  584│    /// echo-commune (T3).\n *585│    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n  586│        let now = now_ms();\n  587│        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n  588│        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n     │...\n  628│                    let mut got =\n *629│                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\n  630│                            .map_err(|e| e.to_string())?;\n  631│                    ingested.append(&mut got);\n  632│                }\n     │...\n  637│        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\n *638│        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\n  639│        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\n  640│        // below can never void an already-relayed turn. The commune-sync is NON-\n  641│        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\n     │...\n  649│                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\n *650│                let commune_res = self.fire_echo(session_id);\n  651│                if let Err(e) = &commune_res {\n  652│                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\n  653│                }\n     │...\n  677│    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n *678│    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n  679│    /// restart instant on every restart. Now the loop rehydrates a\n  680│    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n  681│    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n     │...\n  733│            self.reload_manifest_if_changed(&mut manifest_hash);\n *734│            match self.pulse_tick(session_id) {\n  735│                Ok(report) => {\n  736│                    // F-030 W1: feed a fired turn's outcome to the failure budget.\n  737│                    if let Some(outcome) = &report.turn_outcome {\n     │...\n  775│    /// - **active → (dormant|suspended)**: the transition echo commune\n *776│    ///   ([`fire_echo`](Self::fire_echo) → [`run_echo_commune`]) capturing the\n  777│    ///   outgoing instance's final context delta, BEFORE the flip persists\n  778│    ///   (KH 3.3) — it syncs to whichever instance activates next.\n  779│    /// - **wake (→ active)**: re-activation in place catches up immediately —\n     │...\n  799│            now_ms(),\n *800│            || self.fire_echo(session_id),\n  801│            || self.fire_wake_effects(),\n  802│        )?;\n  803│        // The shell leg of the edge (M5-D4a): suspend closes online shells,\n     │...\n  842│    // [impl->REQ-ECHO-DROP-DIR-RESOLVE]\n *843│    fn fire_echo(&self, session_id: Option<&str>) -> Result<(), String> {\n  844│        let Some(raw) = &self.commune_dir else {\n  845│            return Ok(());\n  846│        };\n     │...\n  858│        };\n *859│        let history = self.history(session_id);\n  860│        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded\n  861│        // echo-commune spawn. The endpoint cwd rides as the spawn's DEFAULT working\n  862│        // dir so the summarizer child runs where the endpoint lives, never in the\n     │...\n 1128│        let owlery = perch::owlery_dir();\n*1129│        // Project routing mirrors `pulse_tick`: an owlery-internal (psyche-host) cwd\n 1130│        // routes NO project; a real cwd routes its project tier; no cwd → no project.\n 1131│        let project_id = match endpoint_cwd.as_deref() {\n 1132│            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\n     │...\n 1261│            || {\n*1262│                if let Err(e) = self.fire_echo(session_id) {\n 1263│                    echo_err = Some(e);\n 1264│                }\n 1265│                Ok(())\n     │...\n 1394│    // A live manifest that ALSO declares an echo-commune summarizer, so\n*1395│    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.\n 1396│    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {\n 1397│        let dir = commune_dir.to_string_lossy().replace('\\\\', \"/\");\n 1398│        let toml = format!(\n     │...\n 1540│    #[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.\n*1541│    fn pulse_tick_ingests_drop() {\n 1542│        with_home(|_| {\n 1543│            let drops = tempfile::tempdir().unwrap();\n 1544│            let manifest = live_manifest(drops.path());\n     │...\n 1548│            std::fs::write(drops.path().join(\"doyle-commune.md\"), \"brief\").unwrap();\n*1549│            let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 1550│            assert_eq!(report.ingested.len(), 1, \"the drop was folded in\");\n 1551│            assert!(!report.echo_fired, \"no echo gate armed\");\n 1552│            assert!(\n     │...\n 2022│\n*2023│            let report = host.pulse_tick(Some(\"sid-si1\")).expect(\"tick\");\n 2024│            assert_eq!(\n 2025│                report.ingested.len(),\n 2026│                1,\n     │...\n 2101│\n*2102│            host.fire_echo(None).expect(\"echo fires clean\");\n 2103│\n 2104│            // The drop landed under the ENDPOINT cwd, not the daemon's.\n 2105│            let drop_path = project.path().join(\".claude\").join(\"doyle-commune.md\");\n     │...\n 2118│    // [unit->REQ-ECHO-DROP-DIR-RESOLVE] a RELATIVE commune_dir with NO recorded\n*2119│    // endpoint cwd = LOUD SKIP: fire_echo returns Ok (no strike), the summarizer is\n 2120│    // NEVER spawned, and nothing is written into whatever cwd the daemon happens to\n 2121│    // hold. RED pre-fix: the echo ran and wrote (or os-error-5'd) against the\n 2122│    // daemon's own process cwd.\n     │...\n 2164│\n*2165│            let out = host.fire_echo(None);\n 2166│            assert!(out.is_ok(), \"a no-cwd skip is Ok (no strike), got {out:?}\");\n 2167│            assert!(\n 2168│                !marker.exists(),\n     │...\n 2190│            // Active → Dormant: the checkpoint/suspend boundary — fires the\n*2191│            // transition echo (KH 3.3) through fire_echo.\n 2192│            let report = host\n 2193│                .rest_event(None, crate::resting::RestEvent::Detach)\n 2194│                .expect(\"rest edge applies\")\n     │...\n 2241│\n*2242│                let report = host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2243│                let outcome = report.turn_outcome.expect(\"gate was armed — fire ran\");\n 2244│                assert!(\n 2245│                    outcome.is_ok(),\n     │...\n 2394│            // Recordless (pre-D9-2 parity): the tick stamps recency.\n*2395│            host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2396│            let stamped = read_info(&perch_path)\n 2397│                .unwrap()\n 2398│                .last_active_ms\n     │...\n 2404│                .unwrap();\n*2405│            host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2406│            let rec = read_info(&perch_path).unwrap();\n 2407│            assert_eq!(\n 2408│                rec.last_active_ms,\n     │...\n 2418│            // Suspended: the tick neither stamps nor transitions.\n*2419│            host.pulse_tick(Some(\"sid-1\")).expect(\"tick\");\n 2420│            let rec = read_info(&perch_path).unwrap();\n 2421│            assert_eq!(rec.last_active_ms, Some(stamped));\n 2422│            assert_eq!(rec.rest_state.as_deref(), Some(\"suspended\"));"},"isError":false,"timestamp":1784271723117}}
{"type":"message","id":"e6dcb988","parentId":"2a1be54a","timestamp":"2026-07-17T07:02:03.121Z","message":{"role":"toolResult","toolCallId":"call_Ix4CKdFtztvSZVYeaSFj2pB0|fc_0e15f89f5b7b4062016a59d36a6de08194892be164eac0185f","toolName":"read","content":[{"type":"text","text":"pub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\n…\n    std::fs::write(dir.join(name), body)\n}\n\n/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\n/// the fire-and-forget form every mutation seam calls (an unwritable queue\n/// must never fail the mutation; the periodic reconcile is the backstop).\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn nudge(scope: &InvalScope) {\n    let _ = nudge_at(&invalidations_dir(), scope);\n}\n\n/// Whether any event is pending in `dir` — the writer's cheap poll (one\n/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\npub fn pending_at(dir: &Path) -> bool {\n    std::fs::read_dir(dir)\n        .map(|mut d| d.next().is_some())\n        .unwrap_or(false)\n}\n\n/// Drain EVERY pending event out of `dir`: read + delete each file, tolerant\n/// of unparseable bodies (skipped + removed) and of files vanishing mid-walk\n/// (a concurrent drain — only the daemon writer drains in production, but the\n/// contract must not depend on that). The returned list is the coalescing\n/// input: the caller unions scopes into one refresh.\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn drain_at(dir: &Path) -> Vec<InvalScope> {\n    let Ok(entries) = std::fs::read_dir(dir) else {\n        return Vec::new();\n    };\n    let mut out = Vec::new();\n    for entry in entries.flatten() {\n        let path = entry.path();\n        if let Ok(body) = std::fs::read_to_string(&path) {\n            if let Ok(scope) = serde_json::from_str::<InvalScope>(&body) {\n                out.push(scope);\n            }\n        }\n        let _ = std::fs::remove_file(&path);\n    }\n    out\n}\n\n/// The coalesced form of a drained event batch — what one reconcile consumes\n/// (multiple invalidations → ONE refresh, ADR-0037 §3).\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct Coalesced {\n    /// Any [`InvalScope::Global`] event was present → membership refresh\n    /// (fingerprint-gated).\n    pub global: bool,\n    /// Endpoint ids needing a row refresh (deduped, sorted).\n    pub endpoints: std::collections::BTreeSet<String>,\n    /// Cwds whose shared derivation-cache entry must be re-derived (deduped\n    /// on the ORIGINAL spelling; the writer normalizes).\n    pub cwds: std::collections::BTreeSet<String>,\n}\n\nimpl Coalesced {\n    pub fn is_empty(&self) -> bool {\n        !self.global && self.endpoints.is_empty() && self.cwds.is_empty()\n    }\n}\n\n/// Union a drained batch into one refresh request — the coalescing proof\n/// surface (N events of any mix in, ONE `Coalesced` out).\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\npub fn coalesce(events: Vec<InvalScope>) -> Coalesced {\n    let mut out = Coalesced::default();\n    for e in events {\n        match e {\n            InvalScope::Global => out.global = true,\n            InvalScope::Endpoint { id, cwd } => {\n                out.endpoints.insert(id);\n                if let Some(c) = cwd {\n                    out.cwds.insert(c);\n                }\n            }\n        }\n    }\n    out\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tempfile::tempdir;\n\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] the queue roundtrip: N nudges of\n    // every scope class land as N files, drain returns them ALL and empties the\n    // dir (drained = consumed), and a second drain is empty (no double-fire).\n    #[test]\n    fn nudge_drain_roundtrip_consumes_queue() {\n        let tmp = tempdir().unwrap();\n        let dir = tmp.path().join(\"invalidations\");\n        assert!(!pending_at(&dir), \"missing dir = nothing pending\");\n\n        nudge_at(&dir, &InvalScope::Global).unwrap();\n        nudge_at(\n            &dir,\n            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\n        )\n        .unwrap();\n        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\n        assert!(pending_at(&dir));\n\n        let drained = drain_at(&dir);\n…\n    }\n…\n}\n\n[Showing lines 79-183 of 229. Use :184 to continue]"}],"details":{"truncation":{"content":"    std::fs::write(dir.join(name), body)\r\n}\r\n\r\n/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\r\n/// the fire-and-forget form every mutation seam calls (an unwritable queue\r\n/// must never fail the mutation; the periodic reconcile is the backstop).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn nudge(scope: &InvalScope) {\r\n    let _ = nudge_at(&invalidations_dir(), scope);\r\n}\r\n\r\n/// Whether any event is pending in `dir` — the writer's cheap poll (one\r\n/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\r\npub fn pending_at(dir: &Path) -> bool {\r\n    std::fs::read_dir(dir)\r\n        .map(|mut d| d.next().is_some())\r\n        .unwrap_or(false)\r\n}\r\n\r\n/// Drain EVERY pending event out of `dir`: read + delete each file, tolerant\r\n/// of unparseable bodies (skipped + removed) and of files vanishing mid-walk\r\n/// (a concurrent drain — only the daemon writer drains in production, but the\r\n/// contract must not depend on that). The returned list is the coalescing\r\n/// input: the caller unions scopes into one refresh.\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn drain_at(dir: &Path) -> Vec<InvalScope> {\r\n    let Ok(entries) = std::fs::read_dir(dir) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out = Vec::new();\r\n    for entry in entries.flatten() {\r\n        let path = entry.path();\r\n        if let Ok(body) = std::fs::read_to_string(&path) {\r\n            if let Ok(scope) = serde_json::from_str::<InvalScope>(&body) {\r\n                out.push(scope);\r\n            }\r\n        }\r\n        let _ = std::fs::remove_file(&path);\r\n    }\r\n    out\r\n}\r\n\r\n/// The coalesced form of a drained event batch — what one reconcile consumes\r\n/// (multiple invalidations → ONE refresh, ADR-0037 §3).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\r\npub struct Coalesced {\r\n    /// Any [`InvalScope::Global`] event was present → membership refresh\r\n    /// (fingerprint-gated).\r\n    pub global: bool,\r\n    /// Endpoint ids needing a row refresh (deduped, sorted).\r\n    pub endpoints: std::collections::BTreeSet<String>,\r\n    /// Cwds whose shared derivation-cache entry must be re-derived (deduped\r\n    /// on the ORIGINAL spelling; the writer normalizes).\r\n    pub cwds: std::collections::BTreeSet<String>,\r\n}\r\n\r\nimpl Coalesced {\r\n    pub fn is_empty(&self) -> bool {\r\n        !self.global && self.endpoints.is_empty() && self.cwds.is_empty()\r\n    }\r\n}\r\n\r\n/// Union a drained batch into one refresh request — the coalescing proof\r\n/// surface (N events of any mix in, ONE `Coalesced` out).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn coalesce(events: Vec<InvalScope>) -> Coalesced {\r\n    let mut out = Coalesced::default();\r\n    for e in events {\r\n        match e {\r\n            InvalScope::Global => out.global = true,\r\n            InvalScope::Endpoint { id, cwd } => {\r\n                out.endpoints.insert(id);\r\n                if let Some(c) = cwd {\r\n                    out.cwds.insert(c);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use tempfile::tempdir;\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] the queue roundtrip: N nudges of\r\n    // every scope class land as N files, drain returns them ALL and empties the\r\n    // dir (drained = consumed), and a second drain is empty (no double-fire).\r\n    #[test]\r\n    fn nudge_drain_roundtrip_consumes_queue() {\r\n        let tmp = tempdir().unwrap();\r\n        let dir = tmp.path().join(\"invalidations\");\r\n        assert!(!pending_at(&dir), \"missing dir = nothing pending\");\r\n\r\n        nudge_at(&dir, &InvalScope::Global).unwrap();\r\n        nudge_at(\r\n            &dir,\r\n            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\r\n        )\r\n        .unwrap();\r\n        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\r\n        assert!(pending_at(&dir));\r\n\r\n        let drained = drain_at(&dir);\r","truncated":true,"truncatedBy":"lines","totalLines":151,"totalBytes":3987,"outputLines":105,"outputBytes":3987,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn nudge_at(dir: &Path, scope: &InvalScope) -> std::io::Result<()> {\n…\n    std::fs::write(dir.join(name), body)\r\n}\r\n\r\n/// [`nudge_at`] against the canonical [`invalidations_dir`], errors swallowed —\r\n/// the fire-and-forget form every mutation seam calls (an unwritable queue\r\n/// must never fail the mutation; the periodic reconcile is the backstop).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn nudge(scope: &InvalScope) {\r\n    let _ = nudge_at(&invalidations_dir(), scope);\r\n}\r\n\r\n/// Whether any event is pending in `dir` — the writer's cheap poll (one\r\n/// `read_dir`, first entry short-circuits; a missing dir is \"nothing pending\").\r\npub fn pending_at(dir: &Path) -> bool {\r\n    std::fs::read_dir(dir)\r\n        .map(|mut d| d.next().is_some())\r\n        .unwrap_or(false)\r\n}\r\n\r\n/// Drain EVERY pending event out of `dir`: read + delete each file, tolerant\r\n/// of unparseable bodies (skipped + removed) and of files vanishing mid-walk\r\n/// (a concurrent drain — only the daemon writer drains in production, but the\r\n/// contract must not depend on that). The returned list is the coalescing\r\n/// input: the caller unions scopes into one refresh.\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn drain_at(dir: &Path) -> Vec<InvalScope> {\r\n    let Ok(entries) = std::fs::read_dir(dir) else {\r\n        return Vec::new();\r\n    };\r\n    let mut out = Vec::new();\r\n    for entry in entries.flatten() {\r\n        let path = entry.path();\r\n        if let Ok(body) = std::fs::read_to_string(&path) {\r\n            if let Ok(scope) = serde_json::from_str::<InvalScope>(&body) {\r\n                out.push(scope);\r\n            }\r\n        }\r\n        let _ = std::fs::remove_file(&path);\r\n    }\r\n    out\r\n}\r\n\r\n/// The coalesced form of a drained event batch — what one reconcile consumes\r\n/// (multiple invalidations → ONE refresh, ADR-0037 §3).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\r\npub struct Coalesced {\r\n    /// Any [`InvalScope::Global`] event was present → membership refresh\r\n    /// (fingerprint-gated).\r\n    pub global: bool,\r\n    /// Endpoint ids needing a row refresh (deduped, sorted).\r\n    pub endpoints: std::collections::BTreeSet<String>,\r\n    /// Cwds whose shared derivation-cache entry must be re-derived (deduped\r\n    /// on the ORIGINAL spelling; the writer normalizes).\r\n    pub cwds: std::collections::BTreeSet<String>,\r\n}\r\n\r\nimpl Coalesced {\r\n    pub fn is_empty(&self) -> bool {\r\n        !self.global && self.endpoints.is_empty() && self.cwds.is_empty()\r\n    }\r\n}\r\n\r\n/// Union a drained batch into one refresh request — the coalescing proof\r\n/// surface (N events of any mix in, ONE `Coalesced` out).\r\n// [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\npub fn coalesce(events: Vec<InvalScope>) -> Coalesced {\r\n    let mut out = Coalesced::default();\r\n    for e in events {\r\n        match e {\r\n            InvalScope::Global => out.global = true,\r\n            InvalScope::Endpoint { id, cwd } => {\r\n                out.endpoints.insert(id);\r\n                if let Some(c) = cwd {\r\n                    out.cwds.insert(c);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use tempfile::tempdir;\r\n\r\n    // [unit->REQ-PROJECT-INDEX-INVALIDATION] the queue roundtrip: N nudges of\r\n    // every scope class land as N files, drain returns them ALL and empties the\r\n    // dir (drained = consumed), and a second drain is empty (no double-fire).\r\n    #[test]\r\n    fn nudge_drain_roundtrip_consumes_queue() {\r\n        let tmp = tempdir().unwrap();\r\n        let dir = tmp.path().join(\"invalidations\");\r\n        assert!(!pending_at(&dir), \"missing dir = nothing pending\");\r\n\r\n        nudge_at(&dir, &InvalScope::Global).unwrap();\r\n        nudge_at(\r\n            &dir,\r\n            &InvalScope::Endpoint { id: \"todlando\".into(), cwd: Some(\"C:/p/spt-core\".into()) },\r\n        )\r\n        .unwrap();\r\n        nudge_at(&dir, &InvalScope::Endpoint { id: \"perri\".into(), cwd: None }).unwrap();\r\n        assert!(pending_at(&dir));\r\n\r\n        let drained = drain_at(&dir);\r\n…\n    }\n…\n}","startLine":66,"lineNumbers":[66,null,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,null,187,null,228]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\projinval.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":229,"totalBytes":3987,"outputLines":105,"outputBytes":3987,"shownRange":{"start":79,"end":183},"nextOffset":184}}},"isError":false,"timestamp":1784271723121}}
{"type":"message","id":"d1cc7528","parentId":"e6dcb988","timestamp":"2026-07-17T07:02:03.122Z","message":{"role":"toolResult","toolCallId":"call_jhIr1zSxo55s5A6fZsmMPSeE|fc_0e15f89f5b7b4062016a59d36a6dc0819485fcb3f0b6078994","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 82|/// matching stays `Arc::ptr_eq`.\n*83|pub(crate) type SharedSend = Arc<BrokerConn>;\n 84|\n 85|/// Default bounded output-log depth (chunks). Large enough that a brain restart\n 86|/// window never evicts unreplayed output in practice; the bound is what keeps a\n## conn.rs\n 491|    /// Bounded framed write. See [`Self::write_gated`].\n*492|    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n 493|        self.write_gated(env, || true)\n 494|    }\n 495|\n## crc_swap.rs\n 285|    /// Write `bytes` to `path`, creating parent dirs. Test helper.\n*286|    fn write(path: &Path, bytes: &[u8]) {\n 287|        if let Some(parent) = path.parent() {\n 288|            fs::create_dir_all(parent).unwrap();\n 289|        }\n## drivehub.rs\n 137|    // [impl->REQ-SHELL-3]\n*138|    pub fn write(&self, owner: &str, shell_id: &str, token: &str, frame: &str) {\n 139|        let mut map = self.map.lock().unwrap();\n 140|        map.insert(\n 141|            slot_key(owner, shell_id),"}],"details":{"scopePath":"crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":4,"fileCount":4,"files":["crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/drivehub.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":1},{"path":"crates/spt-daemon/src/conn.rs","count":1},{"path":"crates/spt-daemon/src/crc_swap.rs","count":1},{"path":"crates/spt-daemon/src/drivehub.rs","count":1}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n 82│/// matching stays `Arc::ptr_eq`.\n*83│pub(crate) type SharedSend = Arc<BrokerConn>;\n 84│\n 85│/// Default bounded output-log depth (chunks). Large enough that a brain restart\n 86│/// window never evicts unreplayed output in practice; the bound is what keeps a\n## conn.rs\n 491│    /// Bounded framed write. See [`Self::write_gated`].\n*492│    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n 493│        self.write_gated(env, || true)\n 494│    }\n 495│\n## crc_swap.rs\n 285│    /// Write `bytes` to `path`, creating parent dirs. Test helper.\n*286│    fn write(path: &Path, bytes: &[u8]) {\n 287│        if let Some(parent) = path.parent() {\n 288│            fs::create_dir_all(parent).unwrap();\n 289│        }\n## drivehub.rs\n 137│    // [impl->REQ-SHELL-3]\n*138│    pub fn write(&self, owner: &str, shell_id: &str, token: &str, frame: &str) {\n 139│        let mut map = self.map.lock().unwrap();\n 140│        map.insert(\n 141│            slot_key(owner, shell_id),"},"isError":false,"timestamp":1784271723122}}
{"type":"message","id":"f6ef5358","parentId":"d1cc7528","timestamp":"2026-07-17T07:02:03.147Z","message":{"role":"toolResult","toolCallId":"call_187USHDVFI2B3T73q9VCBRF2|fc_0e15f89f5b7b4062016a59d36a6dac8194bf2a970d84bfcf17","toolName":"read","content":[{"type":"text","text":"/// no newline is not speaking an NDJSON protocol — capture gives up\n/// (`Oversize`) and classification treats it as unclassifiable, bounded.\nconst OPENER_PIN_MAX: usize = 64 * 1024;\n\n/// How many frames a subscriber's writer queue holds before the subscriber is\n/// judged hopelessly behind and DETACHED (ADR-0038 Amendment fix 3): overflow\n/// is a subscriber problem, never a producer stall — the detached brain\n/// resumes from its cursor (the existing gapless-resume contract).\nconst SUBSCRIBER_QUEUE_CAP: usize = 1024;\n\n/// Why a subscriber seat rejected a frame — the caller removes the seat on\n/// either, but only a POISON (real write failure on the conn) cancels the\n/// stream's lease; an overflow is a slow-but-healthy brain being detached.\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum SeatFail {\n    /// The writer thread died on a failed conn write (the 15s bounded-write\n    /// poison class) — halt-at-first-failure already happened writer-side.\n    Poisoned,\n    /// The bounded queue is full — the subscriber cannot keep up.\n    Overflow,\n}\n\n/// One installed subscriber behind a bounded writer queue (ADR-0038 Amendment\n/// fixes 2+3, REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE): frames ENQUEUE under the\n/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\n/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\n/// OUTSIDE every lock. The field stall was exactly the old shape's serial\n/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\n///\n/// Writer discipline: the thread HALTS at the FIRST failed write (no\n/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\n/// sees it and the caller removes the seat. Dropping the seat drops the queue\n/// sender — the writer drains what it holds and exits.\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\nstruct SubscriberSeat {\n    sub: SharedSend,\n    tx: std::sync::mpsc::SyncSender<Envelope>,\n    /// Writer thread exited (failure or clean drain) — the seat is fully gone\n    /// and a replacement may install (the no-reinstall-until-gone rule,\n    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\n    done: Arc<std::sync::atomic::AtomicBool>,\n    /// The writer died on a WRITE FAILURE (poison), not a clean close.\n    poisoned: Arc<std::sync::atomic::AtomicBool>,\n}\n\nimpl SubscriberSeat {\n    /// Install a seat: spawn its writer thread and hand back the handle.\n    /// `replay` is the attach snapshot, moved in UNDER the log mutex (a\n    /// memory move, never conn I/O): the writer drains it COMPLETELY before\n    /// touching the live queue, so replay-then-live seq order holds by\n    /// construction even while producers enqueue concurrently. (Gate follow-\n    /// up: the first fix-3 shape enqueued the replay from the caller OUTSIDE\n    /// the mutex — a concurrent append could slot a live frame into the\n    /// middle of the replay and break the brain's strict gap check: the xfer\n    /// re-serve \"got seq 134 want 6\" CI red.)\n    /// `lease` (StreamLog seats; `None` for presence) is the stream's serve\n    /// lease AT INSTALL: the writer cancels it the moment a write fails, so\n    /// the paired serve/wire worker's next forward refuses PROMPTLY — even on\n    /// a traffic-quiet stream where no producer enqueue would ever run\n    /// `drop_seat` (a mid-replay poison with no follow-on appends previously\n    /// left the lease alive until the next append; the T4 forwarder starved).\n    /// A LATE cancel from a superseded writer — poisoned OR healthily\n    /// displaced — is harmless by construction: every install mints a fresh\n    /// lease Arc, so an old writer cancels only its own generation's Arc.\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\n    fn install(\n        sub: SharedSend,\n        replay: Vec<Envelope>,\n        lease: Option<Arc<StreamLease>>,\n    ) -> SubscriberSeat {\n        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\n        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\n        let writer_sub = Arc::clone(&sub);\n        let writer_done = Arc::clone(&done);\n        let writer_poisoned = Arc::clone(&poisoned);\n        std::thread::spawn(move || {\n            let poison = |halt_reason: &str| {\n                writer_poisoned.store(true, Ordering::Release);\n                if let Some(l) = &lease {\n                    l.cancel();\n                }\n                writer_sub.lifecycle_event(\"stream-sub-writer-poison\", halt_reason);\n            };\n            // Replay first — halt at the first failed write (Amendment\n            // fix 2), never iterate past a poisoned conn.\n            for frame in replay {\n                if writer_sub.write(&frame).is_err() {\n                    poison(\"replay write failed\");\n                    writer_done.store(true, Ordering::Release);\n                    return;\n                }\n            }\n            // Then live frames, same halt discipline.\n            while let Ok(frame) = rx.recv() {\n                if writer_sub.write(&frame).is_err() {\n                    poison(\"live write failed\");\n                    break;\n                }\n            }\n            writer_done.store(true, Ordering::Release);\n        });\n        SubscriberSeat {\n            sub,\n            tx,\n            done,\n            poisoned,\n        }\n    }\n…\n}\n\n[Showing lines 119-228 of 2865. Use :229 to continue]"}],"details":{"truncation":{"content":"/// no newline is not speaking an NDJSON protocol — capture gives up\r\n/// (`Oversize`) and classification treats it as unclassifiable, bounded.\r\nconst OPENER_PIN_MAX: usize = 64 * 1024;\r\n\r\n/// How many frames a subscriber's writer queue holds before the subscriber is\r\n/// judged hopelessly behind and DETACHED (ADR-0038 Amendment fix 3): overflow\r\n/// is a subscriber problem, never a producer stall — the detached brain\r\n/// resumes from its cursor (the existing gapless-resume contract).\r\nconst SUBSCRIBER_QUEUE_CAP: usize = 1024;\r\n\r\n/// Why a subscriber seat rejected a frame — the caller removes the seat on\r\n/// either, but only a POISON (real write failure on the conn) cancels the\r\n/// stream's lease; an overflow is a slow-but-healthy brain being detached.\r\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum SeatFail {\r\n    /// The writer thread died on a failed conn write (the 15s bounded-write\r\n    /// poison class) — halt-at-first-failure already happened writer-side.\r\n    Poisoned,\r\n    /// The bounded queue is full — the subscriber cannot keep up.\r\n    Overflow,\r\n}\r\n\r\n/// One installed subscriber behind a bounded writer queue (ADR-0038 Amendment\r\n/// fixes 2+3, REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE): frames ENQUEUE under the\r\n/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\r\n/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\r\n/// OUTSIDE every lock. The field stall was exactly the old shape's serial\r\n/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\r\n///\r\n/// Writer discipline: the thread HALTS at the FIRST failed write (no\r\n/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\r\n/// sees it and the caller removes the seat. Dropping the seat drops the queue\r\n/// sender — the writer drains what it holds and exits.\r\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\nstruct SubscriberSeat {\r\n    sub: SharedSend,\r\n    tx: std::sync::mpsc::SyncSender<Envelope>,\r\n    /// Writer thread exited (failure or clean drain) — the seat is fully gone\r\n    /// and a replacement may install (the no-reinstall-until-gone rule,\r\n    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\r\n    done: Arc<std::sync::atomic::AtomicBool>,\r\n    /// The writer died on a WRITE FAILURE (poison), not a clean close.\r\n    poisoned: Arc<std::sync::atomic::AtomicBool>,\r\n}\r\n\r\nimpl SubscriberSeat {\r\n    /// Install a seat: spawn its writer thread and hand back the handle.\r\n    /// `replay` is the attach snapshot, moved in UNDER the log mutex (a\r\n    /// memory move, never conn I/O): the writer drains it COMPLETELY before\r\n    /// touching the live queue, so replay-then-live seq order holds by\r\n    /// construction even while producers enqueue concurrently. (Gate follow-\r\n    /// up: the first fix-3 shape enqueued the replay from the caller OUTSIDE\r\n    /// the mutex — a concurrent append could slot a live frame into the\r\n    /// middle of the replay and break the brain's strict gap check: the xfer\r\n    /// re-serve \"got seq 134 want 6\" CI red.)\r\n    /// `lease` (StreamLog seats; `None` for presence) is the stream's serve\r\n    /// lease AT INSTALL: the writer cancels it the moment a write fails, so\r\n    /// the paired serve/wire worker's next forward refuses PROMPTLY — even on\r\n    /// a traffic-quiet stream where no producer enqueue would ever run\r\n    /// `drop_seat` (a mid-replay poison with no follow-on appends previously\r\n    /// left the lease alive until the next append; the T4 forwarder starved).\r\n    /// A LATE cancel from a superseded writer — poisoned OR healthily\r\n    /// displaced — is harmless by construction: every install mints a fresh\r\n    /// lease Arc, so an old writer cancels only its own generation's Arc.\r\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n    fn install(\r\n        sub: SharedSend,\r\n        replay: Vec<Envelope>,\r\n        lease: Option<Arc<StreamLease>>,\r\n    ) -> SubscriberSeat {\r\n        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\r\n        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n        let writer_sub = Arc::clone(&sub);\r\n        let writer_done = Arc::clone(&done);\r\n        let writer_poisoned = Arc::clone(&poisoned);\r\n        std::thread::spawn(move || {\r\n            let poison = |halt_reason: &str| {\r\n                writer_poisoned.store(true, Ordering::Release);\r\n                if let Some(l) = &lease {\r\n                    l.cancel();\r\n                }\r\n                writer_sub.lifecycle_event(\"stream-sub-writer-poison\", halt_reason);\r\n            };\r\n            // Replay first — halt at the first failed write (Amendment\r\n            // fix 2), never iterate past a poisoned conn.\r\n            for frame in replay {\r\n                if writer_sub.write(&frame).is_err() {\r\n                    poison(\"replay write failed\");\r\n                    writer_done.store(true, Ordering::Release);\r\n                    return;\r\n                }\r\n            }\r\n            // Then live frames, same halt discipline.\r\n            while let Ok(frame) = rx.recv() {\r\n                if writer_sub.write(&frame).is_err() {\r\n                    poison(\"live write failed\");\r\n                    break;\r\n                }\r\n            }\r\n            writer_done.store(true, Ordering::Release);\r\n        });\r\n        SubscriberSeat {\r\n            sub,\r\n            tx,\r\n            done,\r\n            poisoned,\r\n        }\r\n    }\r","truncated":true,"truncatedBy":"lines","totalLines":2747,"totalBytes":5603,"outputLines":110,"outputBytes":5603,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// no newline is not speaking an NDJSON protocol — capture gives up\r\n/// (`Oversize`) and classification treats it as unclassifiable, bounded.\r\nconst OPENER_PIN_MAX: usize = 64 * 1024;\r\n\r\n/// How many frames a subscriber's writer queue holds before the subscriber is\r\n/// judged hopelessly behind and DETACHED (ADR-0038 Amendment fix 3): overflow\r\n/// is a subscriber problem, never a producer stall — the detached brain\r\n/// resumes from its cursor (the existing gapless-resume contract).\r\nconst SUBSCRIBER_QUEUE_CAP: usize = 1024;\r\n\r\n/// Why a subscriber seat rejected a frame — the caller removes the seat on\r\n/// either, but only a POISON (real write failure on the conn) cancels the\r\n/// stream's lease; an overflow is a slow-but-healthy brain being detached.\r\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\nenum SeatFail {\r\n    /// The writer thread died on a failed conn write (the 15s bounded-write\r\n    /// poison class) — halt-at-first-failure already happened writer-side.\r\n    Poisoned,\r\n    /// The bounded queue is full — the subscriber cannot keep up.\r\n    Overflow,\r\n}\r\n\r\n/// One installed subscriber behind a bounded writer queue (ADR-0038 Amendment\r\n/// fixes 2+3, REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE): frames ENQUEUE under the\r\n/// log mutex; the conn I/O — the deadline-bounded `SharedSend::write` that can\r\n/// take 15s against a wedged conn — runs on this seat's OWN writer thread,\r\n/// OUTSIDE every lock. The field stall was exactly the old shape's serial\r\n/// replay-under-mutex against a poisoned conn (33 windows, all ~15s).\r\n///\r\n/// Writer discipline: the thread HALTS at the FIRST failed write (no\r\n/// discarded-errors iteration) and marks the seat poisoned; the next enqueue\r\n/// sees it and the caller removes the seat. Dropping the seat drops the queue\r\n/// sender — the writer drains what it holds and exits.\r\n// [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\nstruct SubscriberSeat {\r\n    sub: SharedSend,\r\n    tx: std::sync::mpsc::SyncSender<Envelope>,\r\n    /// Writer thread exited (failure or clean drain) — the seat is fully gone\r\n    /// and a replacement may install (the no-reinstall-until-gone rule,\r\n    /// REQ-DISPATCH-FALLBACK-CIRCUIT).\r\n    done: Arc<std::sync::atomic::AtomicBool>,\r\n    /// The writer died on a WRITE FAILURE (poison), not a clean close.\r\n    poisoned: Arc<std::sync::atomic::AtomicBool>,\r\n}\r\n\r\nimpl SubscriberSeat {\r\n    /// Install a seat: spawn its writer thread and hand back the handle.\r\n    /// `replay` is the attach snapshot, moved in UNDER the log mutex (a\r\n    /// memory move, never conn I/O): the writer drains it COMPLETELY before\r\n    /// touching the live queue, so replay-then-live seq order holds by\r\n    /// construction even while producers enqueue concurrently. (Gate follow-\r\n    /// up: the first fix-3 shape enqueued the replay from the caller OUTSIDE\r\n    /// the mutex — a concurrent append could slot a live frame into the\r\n    /// middle of the replay and break the brain's strict gap check: the xfer\r\n    /// re-serve \"got seq 134 want 6\" CI red.)\r\n    /// `lease` (StreamLog seats; `None` for presence) is the stream's serve\r\n    /// lease AT INSTALL: the writer cancels it the moment a write fails, so\r\n    /// the paired serve/wire worker's next forward refuses PROMPTLY — even on\r\n    /// a traffic-quiet stream where no producer enqueue would ever run\r\n    /// `drop_seat` (a mid-replay poison with no follow-on appends previously\r\n    /// left the lease alive until the next append; the T4 forwarder starved).\r\n    /// A LATE cancel from a superseded writer — poisoned OR healthily\r\n    /// displaced — is harmless by construction: every install mints a fresh\r\n    /// lease Arc, so an old writer cancels only its own generation's Arc.\r\n    // [impl->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE]\r\n    fn install(\r\n        sub: SharedSend,\r\n        replay: Vec<Envelope>,\r\n        lease: Option<Arc<StreamLease>>,\r\n    ) -> SubscriberSeat {\r\n        let (tx, rx) = std::sync::mpsc::sync_channel::<Envelope>(SUBSCRIBER_QUEUE_CAP);\r\n        let done = Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n        let poisoned = Arc::new(std::sync::atomic::AtomicBool::new(false));\r\n        let writer_sub = Arc::clone(&sub);\r\n        let writer_done = Arc::clone(&done);\r\n        let writer_poisoned = Arc::clone(&poisoned);\r\n        std::thread::spawn(move || {\r\n            let poison = |halt_reason: &str| {\r\n                writer_poisoned.store(true, Ordering::Release);\r\n                if let Some(l) = &lease {\r\n                    l.cancel();\r\n                }\r\n                writer_sub.lifecycle_event(\"stream-sub-writer-poison\", halt_reason);\r\n            };\r\n            // Replay first — halt at the first failed write (Amendment\r\n            // fix 2), never iterate past a poisoned conn.\r\n            for frame in replay {\r\n                if writer_sub.write(&frame).is_err() {\r\n                    poison(\"replay write failed\");\r\n                    writer_done.store(true, Ordering::Release);\r\n                    return;\r\n                }\r\n            }\r\n            // Then live frames, same halt discipline.\r\n            while let Ok(frame) = rx.recv() {\r\n                if writer_sub.write(&frame).is_err() {\r\n                    poison(\"live write failed\");\r\n                    break;\r\n                }\r\n            }\r\n            writer_done.store(true, Ordering::Release);\r\n        });\r\n        SubscriberSeat {\r\n            sub,\r\n            tx,\r\n            done,\r\n            poisoned,\r\n        }\r\n    }\r\n…\n}","startLine":119,"lineNumbers":[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,null,252]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\nethost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2865,"totalBytes":5603,"outputLines":110,"outputBytes":5603,"shownRange":{"start":119,"end":228},"nextOffset":229}}},"isError":false,"timestamp":1784271723147}}
{"type":"message","id":"4120aa55","parentId":"f6ef5358","timestamp":"2026-07-17T07:02:03.170Z","message":{"role":"toolResult","toolCallId":"call_zI0MBedMDL5mVlmq4lXOaNCR|fc_0e15f89f5b7b4062016a59d36a6dd481948c000f56450b12ae","toolName":"read","content":[{"type":"text","text":"    fn drop_dirs(&self) -> Vec<PathBuf> {\n…\n        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\n…\n        {\n            if !dirs.contains(d) {\n…\n            }\n        }\n        dirs\n    }\n\n    /// Sweep a stale signoff sentinel on start (3.2). No-op without a signoff dir.\n    pub fn sweep_on_start(&self) {\n        if let Some(dir) = &self.signoff_dir {\n            let _ = sweep_stale_signoff(dir, &self.id);\n        }\n    }\n\n    /// Mark a daemon-hosted perch `offline` — the teardown counterpart of the\n    /// `online` stamp a daemon-hosted perch carries (a hosted psyche perch, an\n    /// spt-hosted Self). From this point the single liveness resolver reports the\n    /// perch dead regardless of any pid still recorded in its `info.json` (2.5).\n    /// Best-effort: a missing record (perch already gone) is not an error.\n    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n    pub fn mark_offline(perch_path: &std::path::Path) {\n        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);\n    }\n\n    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the\n    /// signal the consent gate resolves \"most-recently-active live session\"\n    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted\n    /// perch that has not been bound yet (no `info.json`) is silently skipped, so\n    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.\n    // [impl->REQ-UPD-4]\n    pub fn touch_active(&self) {\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        let _ = spt_store::info::set_last_active(&perch_path, now_ms());\n    }\n\n    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\n    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\n    /// echo-commune (T3).\n    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\n        let now = now_ms();\n        // Recency follows the resting model (D9-2 — the D8 gap closed): only\n        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\n        // `last_active_ms` — a resting seat ticking its pulse must never win\n        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\n        // the notif first-fire). A dormant seat instead checks the opt-in\n        // auto-suspend threshold (counted from its dormancy-onset anchor);\n        // a suspended seat just keeps ingesting.\n        // [impl->REQ-INST-3]\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\n        match crate::resting::read_rest(&perch_path).map(|r| r.state) {\n            None | Some(crate::resting::RestState::Active) => self.touch_active(),\n            Some(crate::resting::RestState::Dormant) => {\n                let _ = self.rest_event(\n                    session_id,\n                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },\n                );\n            }\n            Some(crate::resting::RestState::Suspended) => {}\n        }\n        // Resolve drop dirs + project routing AT INGEST TIME against the endpoint's\n        // FRESH info.cwd (REQ-STORE-CONTEXT-BRANCH-FILL). A relative manifest drop dir\n        // (claude-spt's `.claude`) resolves against the endpoint's cwd — NEVER the\n        // daemon's process cwd (the pre-fix bug that left the two-tier store pristine).\n        // The project tier routes under the cwd-derived project_id, but an owlery-internal\n        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\n        // the live/agent slice still commits. A relative dir with no recorded cwd is\n        // SKIPPED loud-once, never guessed.\n        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\n        let owlery = perch::owlery_dir();\n        let project_id = match endpoint_cwd.as_deref() {\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\n                spt_store::project::project_id_for_dir(Path::new(cwd))\n            }\n            _ => String::new(),\n        };\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\n        let mut ingested = Vec::new();\n        for raw in self.drop_dirs() {\n            match resolve_endpoint_drop_dir(&raw, cwd_path) {\n                Some(dir) => {\n                    let mut got =\n                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\n                            .map_err(|e| e.to_string())?;\n                    ingested.append(&mut got);\n                }\n                None => warn_no_cwd_once(&self.id, &raw),\n            }\n        }\n        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\n        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\n        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\n        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\n        // below can never void an already-relayed turn. The commune-sync is NON-\n        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\n        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\n        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\n        // the composed mind rides the W2 reseed path, never every pulse.\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\n        let (echo_fired, turn_outcome) =\n            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\n                let turn_res =\n                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\n                let commune_res = self.fire_echo(session_id);\n                if let Err(e) = &commune_res {\n                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\n                }\n                // A strike if EITHER leg failed; the reason names the failing leg.\n                // The turn's relay already happened above regardless of the commune.\n                let outcome = match (turn_res, commune_res) {\n                    (Ok(()), Ok(())) => Ok(()),\n                    (Err(e), _) => Err(format!(\"turn: {e}\")),\n                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\n                };\n                (true, Some(outcome))\n            } else {\n                (false, None)\n            };\n        Ok(TickReport {\n            ingested,\n            echo_fired,\n            turn_outcome,\n        })\n    }\n\n    /// Run the scheduled pulse loop until `stop` is set, calling `on_tick` after\n    /// each tick (so a caller — or a test — observes cadence and can decide to\n    /// stop). The period comes from the durable config, never a constant.\n    ///\n    /// **Restoration D5-2 (ADR-0018 Q4/V4): the cadence is a disk-anchored\n    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\n    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\n    /// restart instant on every restart. Now the loop rehydrates a\n    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\n    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\n    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\n    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\n    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\n    /// deadline is derived functionally — **no per-fire write**.\n    ///\n    /// The loop still holds no work-state beyond the cursor on disk (a fresh loop\n    /// re-reads the drop dirs and gate), so it is freely killable + respawnable;\n    /// D5 makes its *phase* durable too.\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    // [impl->REQ-DAEMON-1]\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\n        &self,\n…\n    ) {\n…\n    }\n\n[Showing lines 549-693 of 2890. Use :694 to continue]"}],"details":{"truncation":{"content":"            }\r\n        }\r\n        dirs\r\n    }\r\n\r\n    /// Sweep a stale signoff sentinel on start (3.2). No-op without a signoff dir.\r\n    pub fn sweep_on_start(&self) {\r\n        if let Some(dir) = &self.signoff_dir {\r\n            let _ = sweep_stale_signoff(dir, &self.id);\r\n        }\r\n    }\r\n\r\n    /// Mark a daemon-hosted perch `offline` — the teardown counterpart of the\r\n    /// `online` stamp a daemon-hosted perch carries (a hosted psyche perch, an\r\n    /// spt-hosted Self). From this point the single liveness resolver reports the\r\n    /// perch dead regardless of any pid still recorded in its `info.json` (2.5).\r\n    /// Best-effort: a missing record (perch already gone) is not an error.\r\n    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n    pub fn mark_offline(perch_path: &std::path::Path) {\r\n        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);\r\n    }\r\n\r\n    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the\r\n    /// signal the consent gate resolves \"most-recently-active live session\"\r\n    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted\r\n    /// perch that has not been bound yet (no `info.json`) is silently skipped, so\r\n    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.\r\n    // [impl->REQ-UPD-4]\r\n    pub fn touch_active(&self) {\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        let _ = spt_store::info::set_last_active(&perch_path, now_ms());\r\n    }\r\n\r\n    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\r\n    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\r\n    /// echo-commune (T3).\r\n    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\r\n        let now = now_ms();\r\n        // Recency follows the resting model (D9-2 — the D8 gap closed): only\r\n        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\r\n        // `last_active_ms` — a resting seat ticking its pulse must never win\r\n        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\r\n        // the notif first-fire). A dormant seat instead checks the opt-in\r\n        // auto-suspend threshold (counted from its dormancy-onset anchor);\r\n        // a suspended seat just keeps ingesting.\r\n        // [impl->REQ-INST-3]\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        match crate::resting::read_rest(&perch_path).map(|r| r.state) {\r\n            None | Some(crate::resting::RestState::Active) => self.touch_active(),\r\n            Some(crate::resting::RestState::Dormant) => {\r\n                let _ = self.rest_event(\r\n                    session_id,\r\n                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },\r\n                );\r\n            }\r\n            Some(crate::resting::RestState::Suspended) => {}\r\n        }\r\n        // Resolve drop dirs + project routing AT INGEST TIME against the endpoint's\r\n        // FRESH info.cwd (REQ-STORE-CONTEXT-BRANCH-FILL). A relative manifest drop dir\r\n        // (claude-spt's `.claude`) resolves against the endpoint's cwd — NEVER the\r\n        // daemon's process cwd (the pre-fix bug that left the two-tier store pristine).\r\n        // The project tier routes under the cwd-derived project_id, but an owlery-internal\r\n        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\r\n        // the live/agent slice still commits. A relative dir with no recorded cwd is\r\n        // SKIPPED loud-once, never guessed.\r\n        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\r\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\r\n        let owlery = perch::owlery_dir();\r\n        let project_id = match endpoint_cwd.as_deref() {\r\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\r\n                spt_store::project::project_id_for_dir(Path::new(cwd))\r\n            }\r\n            _ => String::new(),\r\n        };\r\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\r\n        let mut ingested = Vec::new();\r\n        for raw in self.drop_dirs() {\r\n            match resolve_endpoint_drop_dir(&raw, cwd_path) {\r\n                Some(dir) => {\r\n                    let mut got =\r\n                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\r\n                            .map_err(|e| e.to_string())?;\r\n                    ingested.append(&mut got);\r\n                }\r\n                None => warn_no_cwd_once(&self.id, &raw),\r\n            }\r\n        }\r\n        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\r\n        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\r\n        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\r\n        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\r\n        // below can never void an already-relayed turn. The commune-sync is NON-\r\n        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\r\n        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\r\n        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\r\n        // the composed mind rides the W2 reseed path, never every pulse.\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let (echo_fired, turn_outcome) =\r\n            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\r\n                let turn_res =\r\n                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\r\n                let commune_res = self.fire_echo(session_id);\r\n                if let Err(e) = &commune_res {\r\n                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\r\n                }\r\n                // A strike if EITHER leg failed; the reason names the failing leg.\r\n                // The turn's relay already happened above regardless of the commune.\r\n                let outcome = match (turn_res, commune_res) {\r\n                    (Ok(()), Ok(())) => Ok(()),\r\n                    (Err(e), _) => Err(format!(\"turn: {e}\")),\r\n                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\r\n                };\r\n                (true, Some(outcome))\r\n            } else {\r\n                (false, None)\r\n            };\r\n        Ok(TickReport {\r\n            ingested,\r\n            echo_fired,\r\n            turn_outcome,\r\n        })\r\n    }\r\n\r\n    /// Run the scheduled pulse loop until `stop` is set, calling `on_tick` after\r\n    /// each tick (so a caller — or a test — observes cadence and can decide to\r\n    /// stop). The period comes from the durable config, never a constant.\r\n    ///\r\n    /// **Restoration D5-2 (ADR-0018 Q4/V4): the cadence is a disk-anchored\r\n    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\r\n    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\r\n    /// restart instant on every restart. Now the loop rehydrates a\r\n    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\r\n    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\r\n    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\r\n    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\r\n    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\r\n    /// deadline is derived functionally — **no per-fire write**.\r\n    ///\r\n    /// The loop still holds no work-state beyond the cursor on disk (a fresh loop\r\n    /// re-reads the drop dirs and gate), so it is freely killable + respawnable;\r\n    /// D5 makes its *phase* durable too.\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-1]\r\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\r\n        &self,\r","truncated":true,"truncatedBy":"lines","totalLines":2342,"totalBytes":8004,"outputLines":145,"outputBytes":8004,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn drop_dirs(&self) -> Vec<PathBuf> {\n…\n        for d in [self.commune_dir.as_ref(), self.signoff_dir.as_ref()]\n…\n        {\n            if !dirs.contains(d) {\n…\n            }\r\n        }\r\n        dirs\r\n    }\r\n\r\n    /// Sweep a stale signoff sentinel on start (3.2). No-op without a signoff dir.\r\n    pub fn sweep_on_start(&self) {\r\n        if let Some(dir) = &self.signoff_dir {\r\n            let _ = sweep_stale_signoff(dir, &self.id);\r\n        }\r\n    }\r\n\r\n    /// Mark a daemon-hosted perch `offline` — the teardown counterpart of the\r\n    /// `online` stamp a daemon-hosted perch carries (a hosted psyche perch, an\r\n    /// spt-hosted Self). From this point the single liveness resolver reports the\r\n    /// perch dead regardless of any pid still recorded in its `info.json` (2.5).\r\n    /// Best-effort: a missing record (perch already gone) is not an error.\r\n    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n    pub fn mark_offline(perch_path: &std::path::Path) {\r\n        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);\r\n    }\r\n\r\n    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the\r\n    /// signal the consent gate resolves \"most-recently-active live session\"\r\n    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted\r\n    /// perch that has not been bound yet (no `info.json`) is silently skipped, so\r\n    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.\r\n    // [impl->REQ-UPD-4]\r\n    pub fn touch_active(&self) {\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        let _ = spt_store::info::set_last_active(&perch_path, now_ms());\r\n    }\r\n\r\n    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops\r\n    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one\r\n    /// echo-commune (T3).\r\n    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {\r\n        let now = now_ms();\r\n        // Recency follows the resting model (D9-2 — the D8 gap closed): only\r\n        // an ACTIVE (or pre-D9-2 recordless) instance refreshes\r\n        // `last_active_ms` — a resting seat ticking its pulse must never win\r\n        // a \"most-recently-active\" resolution (REQ-UPD-4's consent target,\r\n        // the notif first-fire). A dormant seat instead checks the opt-in\r\n        // auto-suspend threshold (counted from its dormancy-onset anchor);\r\n        // a suspended seat just keeps ingesting.\r\n        // [impl->REQ-INST-3]\r\n        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);\r\n        match crate::resting::read_rest(&perch_path).map(|r| r.state) {\r\n            None | Some(crate::resting::RestState::Active) => self.touch_active(),\r\n            Some(crate::resting::RestState::Dormant) => {\r\n                let _ = self.rest_event(\r\n                    session_id,\r\n                    crate::resting::RestEvent::AutoSuspendTick { now_ms: now },\r\n                );\r\n            }\r\n            Some(crate::resting::RestState::Suspended) => {}\r\n        }\r\n        // Resolve drop dirs + project routing AT INGEST TIME against the endpoint's\r\n        // FRESH info.cwd (REQ-STORE-CONTEXT-BRANCH-FILL). A relative manifest drop dir\r\n        // (claude-spt's `.claude`) resolves against the endpoint's cwd — NEVER the\r\n        // daemon's process cwd (the pre-fix bug that left the two-tier store pristine).\r\n        // The project tier routes under the cwd-derived project_id, but an owlery-internal\r\n        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\r\n        // the live/agent slice still commits. A relative dir with no recorded cwd is\r\n        // SKIPPED loud-once, never guessed.\r\n        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\r\n        let endpoint_cwd = spt_store::info::read_info(&perch_path).and_then(|i| i.cwd);\r\n        let owlery = perch::owlery_dir();\r\n        let project_id = match endpoint_cwd.as_deref() {\r\n            Some(cwd) if !is_owlery_internal(Path::new(cwd), &owlery) => {\r\n                spt_store::project::project_id_for_dir(Path::new(cwd))\r\n            }\r\n            _ => String::new(),\r\n        };\r\n        let cwd_path = endpoint_cwd.as_deref().map(Path::new);\r\n        let mut ingested = Vec::new();\r\n        for raw in self.drop_dirs() {\r\n            match resolve_endpoint_drop_dir(&raw, cwd_path) {\r\n                Some(dir) => {\r\n                    let mut got =\r\n                        ingest_drops(&dir, &self.id, &project_id, now, self.cfg.protection_window_ms)\r\n                            .map_err(|e| e.to_string())?;\r\n                    ingested.append(&mut got);\r\n                }\r\n                None => warn_no_cwd_once(&self.id, &raw),\r\n            }\r\n        }\r\n        // The echo-gate fire (F-030 W1): a fire now runs ONE bounded per-event\r\n        // psyche turn (`psyche_turn_and_relay` — the D7.5a driver) AND the commune-\r\n        // sync side-effect (`fire_echo`). The turn is the PRIMARY deliverable (parse\r\n        // + relay the psyche's outbound intents); it runs FIRST so the commune-sync\r\n        // below can never void an already-relayed turn. The commune-sync is NON-\r\n        // ATOMIC + LOUD: a failure is stderr-marked and counts a strike (via\r\n        // `turn_outcome`), but never undoes the turn's relay. A pulse fire has no\r\n        // inbound sender (`reply_to = None`) and feeds the minimal standing prompt —\r\n        // the composed mind rides the W2 reseed path, never every pulse.\r\n        // [impl->REQ-PSYCHE-EPHEMERAL-DRIVER]\r\n        let (echo_fired, turn_outcome) =\r\n            if spt_live::pulse::take_echo_gate(&self.id, ParentHint::Infer) {\r\n                let turn_res =\r\n                    self.run_psyche_event_turn(session_id, PSYCHE_PULSE_STANDING_PROMPT, None);\r\n                let commune_res = self.fire_echo(session_id);\r\n                if let Err(e) = &commune_res {\r\n                    eprintln!(\"PSYCHE_COMMUNE_SYNC_FAIL:{}: {e}\", self.id);\r\n                }\r\n                // A strike if EITHER leg failed; the reason names the failing leg.\r\n                // The turn's relay already happened above regardless of the commune.\r\n                let outcome = match (turn_res, commune_res) {\r\n                    (Ok(()), Ok(())) => Ok(()),\r\n                    (Err(e), _) => Err(format!(\"turn: {e}\")),\r\n                    (Ok(()), Err(e)) => Err(format!(\"commune-sync: {e}\")),\r\n                };\r\n                (true, Some(outcome))\r\n            } else {\r\n                (false, None)\r\n            };\r\n        Ok(TickReport {\r\n            ingested,\r\n            echo_fired,\r\n            turn_outcome,\r\n        })\r\n    }\r\n\r\n    /// Run the scheduled pulse loop until `stop` is set, calling `on_tick` after\r\n    /// each tick (so a caller — or a test — observes cadence and can decide to\r\n    /// stop). The period comes from the durable config, never a constant.\r\n    ///\r\n    /// **Restoration D5-2 (ADR-0018 Q4/V4): the cadence is a disk-anchored\r\n    /// absolute-deadline grid, not a phase-relative flat sleep.** The old loop\r\n    /// `pulse_tick`-then-`sleep(pulse_period)` silently re-phased the grid to the\r\n    /// restart instant on every restart. Now the loop rehydrates a\r\n    /// [`DeadlineAnchor`](crate::deadline::DeadlineAnchor) keyed by the session\r\n    /// (the daemon pulse uses `\"pulse\"`; the live-agent adapter's per-agent\r\n    /// drivers pass each agent's id — amd 6, no cross-clobber) and applies the\r\n    /// [`StartReason`] rule: an `Update` restart **preserves the grid phase**\r\n    /// (lands mid-grid), a `Crash`/`Cold` start re-bases it to now. The fire\r\n    /// deadline is derived functionally — **no per-fire write**.\r\n    ///\r\n    /// The loop still holds no work-state beyond the cursor on disk (a fresh loop\r\n    /// re-reads the drop dirs and gate), so it is freely killable + respawnable;\r\n    /// D5 makes its *phase* durable too.\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    // [impl->REQ-DAEMON-1]\r\n    pub fn run_pulse_loop<F: FnMut(&TickReport)>(\r\n        &self,\r\n…\n    ) {\n…\n    }","startLine":541,"lineNumbers":[541,null,543,null,546,547,null,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,null,698,null,751]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\lifecycle.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2890,"totalBytes":8004,"outputLines":145,"outputBytes":8004,"shownRange":{"start":549,"end":693},"nextOffset":694}}},"isError":false,"timestamp":1784271723170}}
{"type":"message","id":"fce99e6c","parentId":"4120aa55","timestamp":"2026-07-17T07:02:14.452Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing pulse read and write patterns**\n\n**Confirming loop read and write timing**"},{"type":"toolCall","id":"call_0MOQNrzMJfTEEbyktZEXDuKU|fc_0e15f89f5b7b4062016a59d375a2588194a1b665c92c40fd0d","name":"read","arguments":{"i":"Reading complete brain heartbeat","path":"crates/spt-daemon/src/brainproc.rs","selector":"180-305"}},{"type":"toolCall","id":"call_UIDNOcOOqOeDLzFfx9FyOxA4|fc_0e15f89f5b7b4062016a59d375a278819485fbb723a455a9f9","name":"read","arguments":{"i":"Reading current hash lines","path":"crates/spt-daemon/src/brainproc.rs","selector":"350-390"}},{"type":"toolCall","id":"call_iZ9QvgGpZWqmVp7xRU3iy853|fc_0e15f89f5b7b4062016a59d375a284819487516e05d96d3bc0","name":"read","arguments":{"i":"Reading connection write gate","path":"crates/spt-daemon/src/conn.rs","selector":"410-510"}},{"type":"toolCall","id":"call_IVim6vAD3ClDwJaJK5IPj7Rd|fc_0e15f89f5b7b4062016a59d375a28881949849eec089b8fb70","name":"read","arguments":{"i":"Reading index reconcile scans","path":"crates/spt-daemon/src/projwriter.rs","selector":"280-450"}},{"type":"toolCall","id":"call_hIsMWacLe5fQYEbHGwmjutJR|fc_0e15f89f5b7b4062016a59d375a290819488367640819891e1","name":"read","arguments":{"i":"Reading session ledger bounds","path":"crates/spt-store/src/sessions.rs","selector":"1-135"}},{"type":"toolCall","id":"call_W0jWK4wbb21id0VnIKAZbZdt|fc_0e15f89f5b7b4062016a59d375a2988194858262c6bc2ae7ac","name":"grep","arguments":{"i":"Finding recent project read","pattern":"fn recent_projects_for|recent_projects_for","path":"crates/spt-daemon/src/registryhost.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11992,"output":475,"cacheRead":97792,"cacheWrite":0,"totalTokens":110259,"reasoningTokens":217,"cost":{"input":0.059960000000000006,"output":0.01425,"cacheRead":0.048895999999999995,"cacheWrite":0,"total":0.12310600000000001}},"stopReason":"toolUse","timestamp":1784271723233,"responseId":"resp_0e15f89f5b7b4062016a59d36ba0cc81949b428650d257d317","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d36ce7a881948d4e912611124025","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdN10cjU2UkJDDDaZP49eqkyDLaH4UHlSe9OcByf-zcZoa66VbkUAyD1llezriS3vvDNseAU5jerCgW0zUYDC9NyZDGEtc5AdaxkFuqW144k_Mg7cDmsS0LXhBx_b7pYNXGYAymjBCxQkgoEmcNaxRCQMvA1BZfK-R1DAu-5zrxz5zYaijzmZJNMIMjqMFj7mwyqucUAq48OJz_yvXZO9-LxA9FN5ZQKGJPoJvKXQybdv6t4c26pggVVBADYwdfaiD3uMumWPjRrM2wa7fAxMkBXHQtDpzW5C-KFuflPlUhNXFHemngT59z1IDYBfGKkEs0wuRDJwRd6vYrVlEWGW2UmnfOh4FM20G0RpT1N8Bmf4HtIX6wQI7_pP_qqMGRg8-G_KLna4Aga3mW8dP1A2UNehDXELJ_f29cXR0VRba-rKvoUF61B6I0f1H2pNqKG5gnZZxgwuBg2dnJjbj6ilKDEghiBb7BxVOgI4up_EyLvhQnDDk8rw4N2FuVZguLkdPrzH1Q6b-sMIPHg501SBDJXuDpY7y15-U8uqpP3CERtY9buCvlzcCAwXCCjEIhkr_qMp84Xu29EugPr_IfaFtSinnOsHstzaQux3SECEbnOGkxoKjFvZ6Lb4rTeXkKzE2MG6_raOLkKHFH3gLfeo5gmQ6RH7lvehhnPvsvvPz1tYsjYlznvqdit2Tz89z3wkVj_4zUkUxJsJaGe92aoefh272pk07SzWl7wZ0E6dACIbR8cnyQvVCIvgO2P26KtPEDHP05VuuHnnoF4o36iirmwfPwhHLrzU_v1bknhqrdq61GTUTr0LOLQZOezW8CXIb4MZlskpNLfroFy5RYXjUWyuOT3AI9CFrD39dssKZHlw4j-zurVIluD14gpnHpT2judDGsHv10Tqv2XC67e14HBfTAYIDIftsydtRNzGsnfyfo4mqGH116klfzcni7AXzeBmBlSUqX6pAewkf9aTHYzgv0vTNZibsaV4_SBVHjVuwoASOg2g6bBlaIbehN9f0VCWQRbEdmSJnIBkfmdJmqSI0ov0VvKBmHmcxPUsjqvRJZDG0xldq7ZaxKMzR7_H_3IrE5g801Tubxzh0MSzvlMcI_OmgXYtRTgwLMA980yAqUonvcVPZ9KGNNzmFdhWZpp5xquhj5bnE4AirhXG_mTEzmgxTk2YDn7aK8F6CJxPZ0rimtV6GU5nSj2rmN9kft_KLVdJERuedwupjb-iC7NO-rq6t2DVEWrpDqMJs_WAhWN_NrRg1fFAhTBwfGxwwFN1Odi-_xI_BKH6SWs3JAG9ohghlpMBk6O9ieU8zQeT-PLgzgjvvKJETKLXwzM61X9u3lNBBqtPZJNJBIbv0rlCeITCWAMrizMecZUsIsx_1iFw57_ghznWUZwJAMPc07pSms_Uh_qxzrdfaGH5nyKG5I7sYPbRVooNkQDnoWkBxhEhApPmWvUDiX51tMcVa19Had9aF3Qa_UCOJmVJNrx0F7Cea8L94RkC4NxUWHqOauAQmUg3V7hYGg1c_aYvhhn248P-yBGwB92kHf5hI93ER5rZzbR6tHLmgYVpgp64dyH1bYNqLz6pCf93lCz9msdGtcMykCmYH7lzOD1_yPTWL0Kbx2saj7cwf7bKYR_fULPEoP6nAA25XZ07VJbB_-21ZgIrh1-EcStbgbDTUdAJFiRi9H9ZhOwYc3RHk3Df2dMhS3r2Xyh322Qk6vzDNi_uT-_Db1qmkmt6HeyYm9qxByTyCXeA_Ul9FOjlx9HiFoJypHkFoS-mqOHdhcEkH3-EDYbSXAVSPBFt17CaJy2NdLFQ9V2WRcXb0maYikhpt9IVYSSZ0qcl1bMiZNCEhr0i_k9Es5144mMTZNJO_6JFsz-b42YHOXi5KQxaTNEkYiRcIeyh42cVk_OkcxM6RBgnpXc3OxRsEuVqQC9UAWIypQ7lUvZR-cd9UctyxpHzL5dTrNwPC-_NhpYPEaSi495ov_xiX3pQ6miUgYnU8zh7FocDieq7O7tiuCyEHO3Fk6YSoanLn1Df0YV4QIDGr5zRnk0QcO9d2Mo4TX7B2bAIS-xKOI-9rCpMNDCDpkT7B8=","summary":[{"type":"summary_text","text":"**Analyzing pulse read and write patterns**"},{"type":"summary_text","text":"**Confirming loop read and write timing**"}]},{"id":"fc_0e15f89f5b7b4062016a59d375a2588194a1b665c92c40fd0d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading complete brain heartbeat\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"180-305\"}","call_id":"call_0MOQNrzMJfTEEbyktZEXDuKU","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d375a278819485fbb723a455a9f9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading current hash lines\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"350-390\"}","call_id":"call_UIDNOcOOqOeDLzFfx9FyOxA4","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d375a284819487516e05d96d3bc0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection write gate\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"410-510\"}","call_id":"call_iZ9QvgGpZWqmVp7xRU3iy853","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d375a28881949849eec089b8fb70","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading index reconcile scans\",\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"selector\":\"280-450\"}","call_id":"call_IVim6vAD3ClDwJaJK5IPj7Rd","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d375a290819488367640819891e1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading session ledger bounds\",\"path\":\"crates/spt-store/src/sessions.rs\",\"selector\":\"1-135\"}","call_id":"call_hIsMWacLe5fQYEbHGwmjutJR","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d375a2988194858262c6bc2ae7ac","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding recent project read\",\"pattern\":\"fn recent_projects_for|recent_projects_for\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_W0jWK4wbb21id0VnIKAZbZdt","name":"grep"}]},"duration":11218.79850000143,"ttft":1685.8227999955416,"contextSnapshot":{"promptTokens":109784,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0MOQNrzMJfTEEbyktZEXDuKU|fc_0e15f89f5b7b4062016a59d375a2588194a1b665c92c40fd0d","toolName":"read","startedAt":"2026-07-17T07:02:14.453Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading complete brain heartbeat"},"id":"791edc98","parentId":"fce99e6c","timestamp":"2026-07-17T07:02:14.453Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UIDNOcOOqOeDLzFfx9FyOxA4|fc_0e15f89f5b7b4062016a59d375a278819485fbb723a455a9f9","toolName":"read","startedAt":"2026-07-17T07:02:14.453Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading current hash lines"},"id":"8be6ced0","parentId":"791edc98","timestamp":"2026-07-17T07:02:14.453Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_iZ9QvgGpZWqmVp7xRU3iy853|fc_0e15f89f5b7b4062016a59d375a284819487516e05d96d3bc0","toolName":"read","startedAt":"2026-07-17T07:02:14.453Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading connection write gate"},"id":"f30ce7ce","parentId":"8be6ced0","timestamp":"2026-07-17T07:02:14.453Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IVim6vAD3ClDwJaJK5IPj7Rd|fc_0e15f89f5b7b4062016a59d375a28881949849eec089b8fb70","toolName":"read","startedAt":"2026-07-17T07:02:14.453Z","args":{"path":"crates/spt-daemon/src/projwriter.rs"},"intent":"Reading index reconcile scans"},"id":"d599cc68","parentId":"f30ce7ce","timestamp":"2026-07-17T07:02:14.453Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hIsMWacLe5fQYEbHGwmjutJR|fc_0e15f89f5b7b4062016a59d375a290819488367640819891e1","toolName":"read","startedAt":"2026-07-17T07:02:14.453Z","args":{"path":"crates/spt-store/src/sessions.rs"},"intent":"Reading session ledger bounds"},"id":"3e23a64e","parentId":"d599cc68","timestamp":"2026-07-17T07:02:14.453Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_W0jWK4wbb21id0VnIKAZbZdt|fc_0e15f89f5b7b4062016a59d375a2988194858262c6bc2ae7ac","toolName":"grep","startedAt":"2026-07-17T07:02:14.453Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Finding recent project read"},"id":"82cdeb54","parentId":"3e23a64e","timestamp":"2026-07-17T07:02:14.453Z"}
{"type":"message","id":"298fccd5","parentId":"82cdeb54","timestamp":"2026-07-17T07:02:14.455Z","message":{"role":"toolResult","toolCallId":"call_W0jWK4wbb21id0VnIKAZbZdt|fc_0e15f89f5b7b4062016a59d375a2988194858262c6bc2ae7ac","toolName":"grep","content":[{"type":"text","text":" 490|            let controlled = info.as_ref().is_some_and(|i| i.controlled);\n*491|            let recent_projects = recent_projects_for(&perch_path, owlery);\n 492|            let last_active_ms = info.and_then(|i| i.last_active_ms);\n 493|            for sub in &subnets.subnets {\n 494|                let Ok(ep) = epoch.next_epoch() else { continue };\n...\n 892|// [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n*893|fn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\n 894|    let entries = spt_store::sessions::read_all(perch_path);\n 895|    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\n 896|    let mut out: Vec<String> = Vec::new();\n...\n 1098|\n*1099|    // [unit->REQ-GOSSIP-ADAPTER-PROJECTS] #4: recent_projects_for derives the gossiped\n 1100|    // project IDs from the session ledger newest→oldest, excludes owlery-internal cwds\n 1101|    // (a psyche-host session is not a project), dedups (a project's newest occurrence\n 1102|    // wins its slot), and bounds the count. Project IDs only — no dirs on the wire.\n 1103|    #[test]\n*1104|    fn recent_projects_for_dedups_newest_first_excludes_owlery() {\n 1105|        let tmp = tempfile::tempdir().unwrap();\n 1106|        let root = tmp.path();\n 1107|        let owlery = root.join(\"owlery\");\n...\n 1134|\n*1135|        let got = recent_projects_for(&perch, &owlery);\n 1136|        // newest-first, owlery excluded, alpha deduped to its newest slot.\n 1137|        assert_eq!(got, vec![\"gamma\", \"alpha\", \"beta\"], \"got {got:?}\");\n 1138|    }"}],"details":{"scopePath":"crates/spt-daemon/src/registryhost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":5,"fileCount":1,"files":["crates/spt-daemon/src/registryhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/registryhost.rs","count":5}],"truncated":false,"displayContent":"  490│            let controlled = info.as_ref().is_some_and(|i| i.controlled);\n *491│            let recent_projects = recent_projects_for(&perch_path, owlery);\n  492│            let last_active_ms = info.and_then(|i| i.last_active_ms);\n  493│            for sub in &subnets.subnets {\n  494│                let Ok(ep) = epoch.next_epoch() else { continue };\n     │...\n  892│// [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n *893│fn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\n  894│    let entries = spt_store::sessions::read_all(perch_path);\n  895│    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\n  896│    let mut out: Vec<String> = Vec::new();\n     │...\n 1098│\n*1099│    // [unit->REQ-GOSSIP-ADAPTER-PROJECTS] #4: recent_projects_for derives the gossiped\n 1100│    // project IDs from the session ledger newest→oldest, excludes owlery-internal cwds\n 1101│    // (a psyche-host session is not a project), dedups (a project's newest occurrence\n 1102│    // wins its slot), and bounds the count. Project IDs only — no dirs on the wire.\n 1103│    #[test]\n*1104│    fn recent_projects_for_dedups_newest_first_excludes_owlery() {\n 1105│        let tmp = tempfile::tempdir().unwrap();\n 1106│        let root = tmp.path();\n 1107│        let owlery = root.join(\"owlery\");\n     │...\n 1134│\n*1135│        let got = recent_projects_for(&perch, &owlery);\n 1136│        // newest-first, owlery excluded, alpha deduped to its newest slot.\n 1137│        assert_eq!(got, vec![\"gamma\", \"alpha\", \"beta\"], \"got {got:?}\");\n 1138│    }"},"isError":false,"timestamp":1784271734455}}
{"type":"message","id":"6355e7e8","parentId":"298fccd5","timestamp":"2026-07-17T07:02:14.466Z","message":{"role":"toolResult","toolCallId":"call_iZ9QvgGpZWqmVp7xRU3iy853|fc_0e15f89f5b7b4062016a59d375a284819487516e05d96d3bc0","toolName":"read","content":[{"type":"text","text":"impl Inner {\n…\n    fn watchdog(&self) {\n…\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n…\n            }\n…\n        }\n…\n    }\n}\n\n[Showing lines 409-513 of 815. Use :514 to continue]"}],"details":{"truncation":{"content":"    }\r\n}\r\n\r\n/// The shared, serialized, BOUNDED send side of one physical broker\r\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\r\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\r\npub(crate) struct BrokerConn {\r\n    inner: Arc<Inner>,\r\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\r\n    /// watchdog aborts through strictly outlives every raw use.\r\n    dog: Option<JoinHandle<()>>,\r\n}\r\n\r\nimpl BrokerConn {\r\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\r\n    /// deadline (gate-wait + OS write completion) — the broker passes its\r\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\r\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\r\n        let raw = raw_of(&half);\r\n        let inner = Arc::new(Inner {\r\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\r\n            label: Mutex::new(String::new()),\r\n            first_write_logged: AtomicBool::new(false),\r\n            gate: Mutex::new(Gate { half: Some(half) }),\r\n            gate_cv: Condvar::new(),\r\n            dog: Mutex::new(Dog {\r\n                inflight: None,\r\n                shutdown: false,\r\n            }),\r\n            dog_cv: Condvar::new(),\r\n            poisoned: AtomicBool::new(false),\r\n            timed_out: AtomicBool::new(false),\r\n            severed: AtomicBool::new(false),\r\n            graveyard: Mutex::new(None),\r\n            op_seq: AtomicU64::new(0),\r\n            bound,\r\n            raw,\r\n        });\r\n        let dog_inner = Arc::clone(&inner);\r\n        let dog = thread::Builder::new()\r\n            .name(\"conn-watchdog\".into())\r\n            .spawn(move || dog_inner.watchdog())\r\n            .ok();\r\n        BrokerConn { inner, dog }\r\n    }\r\n\r\n    /// The stable per-physical-conn id — what lifecycle records key on\r\n    /// (`Arc::ptr_eq` identity does not survive a log line).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn id(&self) -> u64 {\r\n        self.inner.id\r\n    }\r\n\r\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\r\n    /// endpoint/session where known — e.g. `role=brain`,\r\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\r\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\r\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn describe(&self, fact: &str) {\r\n        let mut l = recover(&self.inner.label);\r\n        if l.ends_with('…') {\r\n            return;\r\n        }\r\n        if l.len() + fact.len() + 1 > LABEL_CAP {\r\n            l.push('…');\r\n            return;\r\n        }\r\n        if !l.is_empty() {\r\n            l.push(' ');\r\n        }\r\n        l.push_str(fact);\r\n    }\r\n\r\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\r\n    /// attribution format — the broker's sink machinery uses it for\r\n    /// `writer-exit`, attach/replace, and detach events (leg d).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\r\n        self.inner.lifecycle(event, extra);\r\n    }\r\n\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r","truncated":true,"truncatedBy":"lines","totalLines":407,"totalBytes":4299,"outputLines":105,"outputBytes":4299,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl Inner {\n…\n    fn watchdog(&self) {\n…\n    }\r\n}\r\n\r\n/// The shared, serialized, BOUNDED send side of one physical broker\r\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\r\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\r\npub(crate) struct BrokerConn {\r\n    inner: Arc<Inner>,\r\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\r\n    /// watchdog aborts through strictly outlives every raw use.\r\n    dog: Option<JoinHandle<()>>,\r\n}\r\n\r\nimpl BrokerConn {\r\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\r\n    /// deadline (gate-wait + OS write completion) — the broker passes its\r\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\r\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\r\n        let raw = raw_of(&half);\r\n        let inner = Arc::new(Inner {\r\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\r\n            label: Mutex::new(String::new()),\r\n            first_write_logged: AtomicBool::new(false),\r\n            gate: Mutex::new(Gate { half: Some(half) }),\r\n            gate_cv: Condvar::new(),\r\n            dog: Mutex::new(Dog {\r\n                inflight: None,\r\n                shutdown: false,\r\n            }),\r\n            dog_cv: Condvar::new(),\r\n            poisoned: AtomicBool::new(false),\r\n            timed_out: AtomicBool::new(false),\r\n            severed: AtomicBool::new(false),\r\n            graveyard: Mutex::new(None),\r\n            op_seq: AtomicU64::new(0),\r\n            bound,\r\n            raw,\r\n        });\r\n        let dog_inner = Arc::clone(&inner);\r\n        let dog = thread::Builder::new()\r\n            .name(\"conn-watchdog\".into())\r\n            .spawn(move || dog_inner.watchdog())\r\n            .ok();\r\n        BrokerConn { inner, dog }\r\n    }\r\n\r\n    /// The stable per-physical-conn id — what lifecycle records key on\r\n    /// (`Arc::ptr_eq` identity does not survive a log line).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn id(&self) -> u64 {\r\n        self.inner.id\r\n    }\r\n\r\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\r\n    /// endpoint/session where known — e.g. `role=brain`,\r\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\r\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\r\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn describe(&self, fact: &str) {\r\n        let mut l = recover(&self.inner.label);\r\n        if l.ends_with('…') {\r\n            return;\r\n        }\r\n        if l.len() + fact.len() + 1 > LABEL_CAP {\r\n            l.push('…');\r\n            return;\r\n        }\r\n        if !l.is_empty() {\r\n            l.push(' ');\r\n        }\r\n        l.push_str(fact);\r\n    }\r\n\r\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\r\n    /// attribution format — the broker's sink machinery uses it for\r\n    /// `writer-exit`, attach/replace, and detach events (leg d).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\r\n        self.inner.lifecycle(event, extra);\r\n    }\r\n\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n…\n            }\n…\n        }\n…\n    }\n}","startLine":219,"lineNumbers":[219,null,343,null,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,null,515,null,536,null,601,602]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":815,"totalBytes":4299,"outputLines":105,"outputBytes":4299,"shownRange":{"start":409,"end":513},"nextOffset":514}}},"isError":false,"timestamp":1784271734466}}
{"type":"message","id":"aae89250","parentId":"6355e7e8","timestamp":"2026-07-17T07:02:14.466Z","message":{"role":"toolResult","toolCallId":"call_hIsMWacLe5fQYEbHGwmjutJR|fc_0e15f89f5b7b4062016a59d375a290819488367640819891e1","toolName":"read","content":[{"type":"text","text":"//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /\n//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/\n//! sessions.log`.\n//!\n//! A live agent that `/clear`s or `/compact`s keeps its perch identity but\n//! rotates its harness `session_id` (a new transcript file). The single\n//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*\n//! the thread but cannot *span* it. This ledger records each rotation so the\n//! digest can enumerate the **last K sessions** and bridge a boundary\n//! (ADR-0019 §thread-spanning).\n//!\n//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is\n//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew\n//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates\n//! the oldest rows out. A re-bind of the *same* session is not a new boundary and\n//! is deduped.\n// [impl->REQ-TERM-6]\n\nuse std::io;\nuse std::path::Path;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::atomic::atomic_write_string;\n\n/// The ledger filename under a perch.\npub const LEDGER_FILE: &str = \"sessions.log\";\n\n/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on\n/// append so the file can never grow without bound (risk #2).\npub const MAX_LEDGER: usize = 64;\n\n/// Why a session became active.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum SessionTrigger {\n    /// First bind of the perch (`api listen` / revive).\n    Boot,\n    /// A `/clear` boundary (`api boundary clear`).\n    Clear,\n    /// A `/compact` boundary (`api boundary compact`).\n    Compact,\n}\n\nimpl SessionTrigger {\n    /// The lowercase token (matches the serde wire form) — used as the boundary\n    /// marker `kind` in the spanned digest.\n    pub fn token(self) -> &'static str {\n        match self {\n            SessionTrigger::Boot => \"boot\",\n            SessionTrigger::Clear => \"clear\",\n            SessionTrigger::Compact => \"compact\",\n        }\n    }\n}\n\n/// One ledger row: a `session_id` became active at `ts` via `trigger`.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct SessionEntry {\n    /// RFC3339-UTC instant (see [`crate::timefmt`]).\n    pub ts: String,\n    /// The harness session id that became active.\n    pub session_id: String,\n    /// What caused the rotation.\n    pub trigger: SessionTrigger,\n    /// The project working directory the session was active in (the W3\n    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a\n    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves\n    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history\n    /// (cross-project rows) restores the right dir from here.\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub cwd: Option<String>,\n    /// A **persisted, monotonic** ledger ordinal — the stable per-row sequence the\n    /// digest cursor anchors to (REQ-DIGEST-CURSOR). The ledger is bounded\n    /// ([`MAX_LEDGER`]): a `read_all` index renumbers survivors when the oldest row\n    /// prunes, which would re-number a consumer's cursor under it. A persisted\n    /// ordinal is stamped once at [`append`] and never moves, so a pruned survivor\n    /// keeps its number. **Additive serde-default**: a pre-migration row missing\n    /// `ordinal` deserializes as `None` and is backfilled to its concrete index on\n    /// the next append (so a migrated ledger is fully ordinal-stamped).\n    // [impl->REQ-DIGEST-CURSOR]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub ordinal: Option<u64>,\n    /// The adapter[:profile] the session ran under (D-2, REQ-SESSION-ADAPTER-RECORDED).\n    /// A resume-from-history restores THIS harness — the one that authored the\n    /// transcript — not merely the endpoint's *current* stamp (which a B-2\n    /// ChangeAdapter or a fork may have moved since). **Additive serde-default**: a\n    /// pre-migration row missing `adapter` deserializes as `None`; `None` omits the\n    /// key on serialize (byte-identical to old rows). `None` is a benign degrade —\n    /// resume falls back to the endpoint's current adapter.\n    // [impl->REQ-SESSION-ADAPTER-RECORDED]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub adapter: Option<String>,\n}\n\n/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable\n/// line is skipped, the rest still load (the degrade-don't-crash store stance).\npub fn read_all(perch: &Path) -> Vec<SessionEntry> {\n    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\n        return Vec::new();\n    };\n    s.lines()\n        .filter(|l| !l.trim().is_empty())\n        .filter_map(|l| serde_json::from_str(l).ok())\n        .collect()\n}\n\n/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\npub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\n    if k == 0 {\n        return Vec::new();\n    }\n    let all = read_all(perch);\n    let start = all.len().saturating_sub(k);\n    all[start..].to_vec()\n}\n\n/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\n/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\n/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\n/// `read_all` index (not the slice index) so it stays comparable across sessions.\n/// The absolute index of returned entry `i` is `(all.len() - returned.len()) + i`.\n// [impl->REQ-DIGEST-CURSOR]\npub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\n    if k == 0 {\n        return Vec::new();\n    }\n    let all = read_all(perch);\n    let start = all.len().saturating_sub(k);\n    all[start..]\n        .iter()\n        .enumerate()\n        .map(|(i, e)| {\n            let mut e = e.clone();\n            // Some from disk; else the absolute read_all index as the fallback.\n            e.ordinal = Some(e.ordinal.unwrap_or((start + i) as u64));\n            e\n        })\n        .collect()\n}\n\n[Showing lines 1-138 of 478. Use :139 to continue]"}],"details":{"truncation":{"content":"//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /\r\n//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/\r\n//! sessions.log`.\r\n//!\r\n//! A live agent that `/clear`s or `/compact`s keeps its perch identity but\r\n//! rotates its harness `session_id` (a new transcript file). The single\r\n//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*\r\n//! the thread but cannot *span* it. This ledger records each rotation so the\r\n//! digest can enumerate the **last K sessions** and bridge a boundary\r\n//! (ADR-0019 §thread-spanning).\r\n//!\r\n//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is\r\n//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew\r\n//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates\r\n//! the oldest rows out. A re-bind of the *same* session is not a new boundary and\r\n//! is deduped.\r\n// [impl->REQ-TERM-6]\r\n\r\nuse std::io;\r\nuse std::path::Path;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::atomic::atomic_write_string;\r\n\r\n/// The ledger filename under a perch.\r\npub const LEDGER_FILE: &str = \"sessions.log\";\r\n\r\n/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on\r\n/// append so the file can never grow without bound (risk #2).\r\npub const MAX_LEDGER: usize = 64;\r\n\r\n/// Why a session became active.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(rename_all = \"lowercase\")]\r\npub enum SessionTrigger {\r\n    /// First bind of the perch (`api listen` / revive).\r\n    Boot,\r\n    /// A `/clear` boundary (`api boundary clear`).\r\n    Clear,\r\n    /// A `/compact` boundary (`api boundary compact`).\r\n    Compact,\r\n}\r\n\r\nimpl SessionTrigger {\r\n    /// The lowercase token (matches the serde wire form) — used as the boundary\r\n    /// marker `kind` in the spanned digest.\r\n    pub fn token(self) -> &'static str {\r\n        match self {\r\n            SessionTrigger::Boot => \"boot\",\r\n            SessionTrigger::Clear => \"clear\",\r\n            SessionTrigger::Compact => \"compact\",\r\n        }\r\n    }\r\n}\r\n\r\n/// One ledger row: a `session_id` became active at `ts` via `trigger`.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct SessionEntry {\r\n    /// RFC3339-UTC instant (see [`crate::timefmt`]).\r\n    pub ts: String,\r\n    /// The harness session id that became active.\r\n    pub session_id: String,\r\n    /// What caused the rotation.\r\n    pub trigger: SessionTrigger,\r\n    /// The project working directory the session was active in (the W3\r\n    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a\r\n    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves\r\n    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history\r\n    /// (cross-project rows) restores the right dir from here.\r\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n    /// A **persisted, monotonic** ledger ordinal — the stable per-row sequence the\r\n    /// digest cursor anchors to (REQ-DIGEST-CURSOR). The ledger is bounded\r\n    /// ([`MAX_LEDGER`]): a `read_all` index renumbers survivors when the oldest row\r\n    /// prunes, which would re-number a consumer's cursor under it. A persisted\r\n    /// ordinal is stamped once at [`append`] and never moves, so a pruned survivor\r\n    /// keeps its number. **Additive serde-default**: a pre-migration row missing\r\n    /// `ordinal` deserializes as `None` and is backfilled to its concrete index on\r\n    /// the next append (so a migrated ledger is fully ordinal-stamped).\r\n    // [impl->REQ-DIGEST-CURSOR]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub ordinal: Option<u64>,\r\n    /// The adapter[:profile] the session ran under (D-2, REQ-SESSION-ADAPTER-RECORDED).\r\n    /// A resume-from-history restores THIS harness — the one that authored the\r\n    /// transcript — not merely the endpoint's *current* stamp (which a B-2\r\n    /// ChangeAdapter or a fork may have moved since). **Additive serde-default**: a\r\n    /// pre-migration row missing `adapter` deserializes as `None`; `None` omits the\r\n    /// key on serialize (byte-identical to old rows). `None` is a benign degrade —\r\n    /// resume falls back to the endpoint's current adapter.\r\n    // [impl->REQ-SESSION-ADAPTER-RECORDED]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub adapter: Option<String>,\r\n}\r\n\r\n/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable\r\n/// line is skipped, the rest still load (the degrade-don't-crash store stance).\r\npub fn read_all(perch: &Path) -> Vec<SessionEntry> {\r\n    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\r\n        return Vec::new();\r\n    };\r\n    s.lines()\r\n        .filter(|l| !l.trim().is_empty())\r\n        .filter_map(|l| serde_json::from_str(l).ok())\r\n        .collect()\r\n}\r\n\r\n/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\r\npub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..].to_vec()\r\n}\r\n\r\n/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\r\n/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\r\n/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\r\n/// `read_all` index (not the slice index) so it stays comparable across sessions.\r\n/// The absolute index of returned entry `i` is `(all.len() - returned.len()) + i`.\r\n// [impl->REQ-DIGEST-CURSOR]\r\npub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..]\r\n        .iter()\r\n        .enumerate()\r\n        .map(|(i, e)| {\r\n            let mut e = e.clone();\r\n            // Some from disk; else the absolute read_all index as the fallback.\r\n            e.ordinal = Some(e.ordinal.unwrap_or((start + i) as u64));\r\n            e\r","truncated":true,"truncatedBy":"lines","totalLines":478,"totalBytes":6261,"outputLines":138,"outputBytes":6261,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Per-endpoint **session ledger** — `<perch>/sessions.log` (ADR-0019 /\r\n//! REQ-TERM-6), heir to the sister project's `psyches/tracked/agents/<id>/\r\n//! sessions.log`.\r\n//!\r\n//! A live agent that `/clear`s or `/compact`s keeps its perch identity but\r\n//! rotates its harness `session_id` (a new transcript file). The single\r\n//! `{session_id}` a `[history]`/`[digest]` source resolves therefore *follows*\r\n//! the thread but cannot *span* it. This ledger records each rotation so the\r\n//! digest can enumerate the **last K sessions** and bridge a boundary\r\n//! (ADR-0019 §thread-spanning).\r\n//!\r\n//! One JSONL row per rotation: `{ts, session_id, trigger}`. The ledger is\r\n//! **bounded** ([`MAX_LEDGER`]) — the sister project's seal-on-boot failure grew\r\n//! its equivalent unboundedly (DIGEST-MILESTONE-PLAN risk #2); appending rotates\r\n//! the oldest rows out. A re-bind of the *same* session is not a new boundary and\r\n//! is deduped.\r\n// [impl->REQ-TERM-6]\r\n\r\nuse std::io;\r\nuse std::path::Path;\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::atomic::atomic_write_string;\r\n\r\n/// The ledger filename under a perch.\r\npub const LEDGER_FILE: &str = \"sessions.log\";\r\n\r\n/// On-disk row cap. The ledger is a rolling tail — older rows rotate out on\r\n/// append so the file can never grow without bound (risk #2).\r\npub const MAX_LEDGER: usize = 64;\r\n\r\n/// Why a session became active.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(rename_all = \"lowercase\")]\r\npub enum SessionTrigger {\r\n    /// First bind of the perch (`api listen` / revive).\r\n    Boot,\r\n    /// A `/clear` boundary (`api boundary clear`).\r\n    Clear,\r\n    /// A `/compact` boundary (`api boundary compact`).\r\n    Compact,\r\n}\r\n\r\nimpl SessionTrigger {\r\n    /// The lowercase token (matches the serde wire form) — used as the boundary\r\n    /// marker `kind` in the spanned digest.\r\n    pub fn token(self) -> &'static str {\r\n        match self {\r\n            SessionTrigger::Boot => \"boot\",\r\n            SessionTrigger::Clear => \"clear\",\r\n            SessionTrigger::Compact => \"compact\",\r\n        }\r\n    }\r\n}\r\n\r\n/// One ledger row: a `session_id` became active at `ts` via `trigger`.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct SessionEntry {\r\n    /// RFC3339-UTC instant (see [`crate::timefmt`]).\r\n    pub ts: String,\r\n    /// The harness session id that became active.\r\n    pub session_id: String,\r\n    /// What caused the rotation.\r\n    pub trigger: SessionTrigger,\r\n    /// The project working directory the session was active in (the W3\r\n    /// `info.cwd` set at bind, or the spawn cwd). **Additive serde-default**: a\r\n    /// pre-migration row missing `cwd` deserializes as `None`. A harness resolves\r\n    /// a transcript by `session_id` + `cwd`, so picker Resume-from-history\r\n    /// (cross-project rows) restores the right dir from here.\r\n    // [impl->REQ-SESSION-RESUME-TEMPLATE]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n    /// A **persisted, monotonic** ledger ordinal — the stable per-row sequence the\r\n    /// digest cursor anchors to (REQ-DIGEST-CURSOR). The ledger is bounded\r\n    /// ([`MAX_LEDGER`]): a `read_all` index renumbers survivors when the oldest row\r\n    /// prunes, which would re-number a consumer's cursor under it. A persisted\r\n    /// ordinal is stamped once at [`append`] and never moves, so a pruned survivor\r\n    /// keeps its number. **Additive serde-default**: a pre-migration row missing\r\n    /// `ordinal` deserializes as `None` and is backfilled to its concrete index on\r\n    /// the next append (so a migrated ledger is fully ordinal-stamped).\r\n    // [impl->REQ-DIGEST-CURSOR]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub ordinal: Option<u64>,\r\n    /// The adapter[:profile] the session ran under (D-2, REQ-SESSION-ADAPTER-RECORDED).\r\n    /// A resume-from-history restores THIS harness — the one that authored the\r\n    /// transcript — not merely the endpoint's *current* stamp (which a B-2\r\n    /// ChangeAdapter or a fork may have moved since). **Additive serde-default**: a\r\n    /// pre-migration row missing `adapter` deserializes as `None`; `None` omits the\r\n    /// key on serialize (byte-identical to old rows). `None` is a benign degrade —\r\n    /// resume falls back to the endpoint's current adapter.\r\n    // [impl->REQ-SESSION-ADAPTER-RECORDED]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub adapter: Option<String>,\r\n}\r\n\r\n/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable\r\n/// line is skipped, the rest still load (the degrade-don't-crash store stance).\r\npub fn read_all(perch: &Path) -> Vec<SessionEntry> {\r\n    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {\r\n        return Vec::new();\r\n    };\r\n    s.lines()\r\n        .filter(|l| !l.trim().is_empty())\r\n        .filter_map(|l| serde_json::from_str(l).ok())\r\n        .collect()\r\n}\r\n\r\n/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.\r\npub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..].to_vec()\r\n}\r\n\r\n/// The last `k` rows with every returned entry carrying a usable [`SessionEntry::ordinal`]\r\n/// for digest-cursor seq encoding (REQ-DIGEST-CURSOR). A row stamped on disk keeps\r\n/// its persisted ordinal; a pre-migration `None` row falls back to its **absolute**\r\n/// `read_all` index (not the slice index) so it stays comparable across sessions.\r\n/// The absolute index of returned entry `i` is `(all.len() - returned.len()) + i`.\r\n// [impl->REQ-DIGEST-CURSOR]\r\npub fn last_k_with_ordinals(perch: &Path, k: usize) -> Vec<SessionEntry> {\r\n    if k == 0 {\r\n        return Vec::new();\r\n    }\r\n    let all = read_all(perch);\r\n    let start = all.len().saturating_sub(k);\r\n    all[start..]\r\n        .iter()\r\n        .enumerate()\r\n        .map(|(i, e)| {\r\n            let mut e = e.clone();\r\n            // Some from disk; else the absolute read_all index as the fallback.\r\n            e.ordinal = Some(e.ordinal.unwrap_or((start + i) as u64));\r\n            e\r\n        })\n        .collect()\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\sessions.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":478,"totalBytes":6261,"outputLines":138,"outputBytes":6261,"shownRange":{"start":1,"end":138},"nextOffset":139}}},"isError":false,"timestamp":1784271734466}}
{"type":"message","id":"5206fd52","parentId":"aae89250","timestamp":"2026-07-17T07:02:14.483Z","message":{"role":"toolResult","toolCallId":"call_IVim6vAD3ClDwJaJK5IPj7Rd|fc_0e15f89f5b7b4062016a59d375a28881949849eec089b8fb70","toolName":"read","content":[{"type":"text","text":"        // have moved): drop those cache entries up front so the compose pass\n        // re-derives them exactly once.\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n        for cwd in &req.cwds {\n            self.cwd_cache.remove(&projderive::normalize_path(Path::new(cwd)));\n        }\n\n        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\n        let store = if self.paths.seed_git.exists() {\n            Some(\n                BranchStore::open_or_init(&self.paths.seed_git)\n                    .map_err(|e| format!(\"branch store open: {e}\"))?,\n            )\n        } else {\n            None // no context store yet — sessions/origin still index\n        };\n        let tips: Vec<(String, String)> = match &store {\n            Some(store) => {\n                counters.branch_enumerations += 1;\n                store\n                    .branch_tips_by_recency()\n                    .map_err(|e| format!(\"branch enumeration: {e}\"))?\n            }\n            None => Vec::new(),\n        };\n        let fingerprint = projindex::fingerprint_tips(&tips);\n        let fingerprint_changed = fingerprint != self.last_fingerprint;\n\n        // ── 2. The no-work exit: unchanged generation performs NO scan ───\n        if !fingerprint_changed\n            && !force_full\n            && req.endpoints.is_empty()\n            && req.cwds.is_empty()\n            && self.current.is_some()\n        {\n            let report = CycleReport { counters, published: false, skipped_unchanged: true };\n            self.finish_cycle(&report, now, started.elapsed());\n            return Ok(report);\n        }\n\n        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\n        self.branch_order = tips\n            .iter()\n            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\n            .collect();\n        if let (false, Some(store)) = (self.branch_order.is_empty(), &store) {\n            let live: BTreeSet<&str> =\n                self.branch_order.iter().map(|(_, b)| b.as_str()).collect();\n            self.branch_cache.retain(|b, _| live.contains(b.as_str()));\n            for (branch, tip) in tips.iter().filter(|(b, _)| b.starts_with(\"p-\")) {\n                let cached = self.branch_cache.get(branch);\n                if cached.map(|c| &c.tip) == Some(tip) {\n                    continue; // unchanged tip → cached membership, no scan\n                }\n                let files = store\n                    .ls_files_at(tip)\n                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\n                counters.tree_scans += 1;\n                let members = files\n                    .iter()\n                    .filter_map(|f| {\n                        let (id, rest) = f.split_once('/')?;\n                        (rest == PROJECT_CONTEXT_FILE).then(|| id.to_string())\n                    })\n                    .collect();\n                self.branch_cache\n                    .insert(branch.clone(), BranchScan { tip: tip.clone(), members });\n            }\n        } else {\n            self.branch_cache.clear();\n        }\n\n        // ── 4. Which endpoints recompose ──────────────────────────────────\n        // A membership change can touch ANY endpoint, so a changed fingerprint\n        // (or the backstop / a missing snapshot) recomposes the full roster;\n        // otherwise only the scoped rows are patched.\n        let full = force_full || fingerprint_changed || self.current.is_none();\n        let roster = enumerate_perches(&self.paths.owlery);\n        let targets: Vec<(String, PathBuf, Option<String>)> = if full {\n            roster\n        } else {\n            let by_id: HashMap<String, (PathBuf, Option<String>)> = roster\n                .into_iter()\n                .map(|(id, path, cwd)| (id, (path, cwd)))\n                .collect();\n            req.endpoints\n                .iter()\n                .filter_map(|id| {\n                    by_id.get(id).map(|(p, c)| (id.clone(), p.clone(), c.clone()))\n                })\n                .collect()\n        };\n\n        // ── 5. Compose rows through the SHARED precedence kernel ─────────\n        // (parity by construction — ADR-0037 §4). The derive closure is the\n        // cwd cache: one real derivation per distinct normalized cwd.\n        let cache = std::cell::RefCell::new(&mut self.cwd_cache);\n        let hits = std::cell::Cell::new(0u64);\n        let misses = std::cell::Cell::new(0u64);\n        let derivations = std::cell::Cell::new(0u64);\n        let derive = |dir: &Path| -> (String, String) {\n            let key = projderive::normalize_path(dir);\n            let marker = discover_git_marker(dir);\n            let stamp = marker.as_deref().and_then(stamp_of);\n            let mut cache = cache.borrow_mut();\n            if let Some(hit) = cache.get(&key) {\n                if hit.marker == marker && hit.stamp == stamp {\n                    hits.set(hits.get() + 1);\n                    return (hit.id.clone(), hit.display.clone());\n                }\n            }\n            misses.set(misses.get() + 1);\n            derivations.set(derivations.get() + 1);\n            let (id, display) = spt_store::project::project_id_and_display_for_dir(dir);\n            cache.insert(\n                key,\n                CwdDerivation { id: id.clone(), display: display.clone(), marker, stamp },\n            );\n            (id, display)\n        };\n\n        let mut endpoints: BTreeMap<String, EndpointProject> = if full {\n            BTreeMap::new()\n        } else {\n            self.current.as_ref().map(|i| i.endpoints.clone()).unwrap_or_default()\n        };\n        if !full {\n            // A scoped id whose perch vanished (purge) drops its row — the\n            // roster is the authority; a stale attribution never survives it.\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n            let live: BTreeSet<&String> = targets.iter().map(|(id, _, _)| id).collect();\n            for id in &req.endpoints {\n                if !live.contains(id) {\n                    endpoints.remove(id);\n                }\n            }\n        }\n        for (id, perch_path, origin_cwd) in &targets {\n            let entries = sessions::read_all(perch_path);\n            let store_ids: Vec<String> = self\n                .branch_order\n                .iter()\n                .filter(|(_, branch)| {\n                    self.branch_cache\n                        .get(branch)\n                        .is_some_and(|scan| scan.members.contains(id))\n                })\n                .map(|(project, _)| project.clone())\n                .collect();\n            let refs = projderive::project_refs_from(\n                &entries,\n                origin_cwd.as_deref(),\n                store_ids,\n                &self.paths.owlery,\n                derive, // captures only shared refs → Copy; each call reuses one closure\n            );\n            // The FULL precedence-ordered history is persisted (W3 additive\n            // `refs`, REQ-PICKER-PROJECT-HISTORY-TRUTH: the picker projects\n            // history, not head); the head fields stay beside it for W2-shaped\n            // consumers and the status surface.\n            // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\n            let head = refs.first().cloned();\n            let ref_entries: Vec<projindex::ProjectRefEntry> = refs\n                .into_iter()\n                .map(|r| projindex::ProjectRefEntry {\n                    id: r.id,\n                    dir: r.dir,\n                    display: r.display,\n                    source: Some(r.source.as_str().to_string()),\n                })\n                .collect();\n            endpoints.insert(\n                id.clone(),\n                EndpointProject {\n                    project_id: head.as_ref().map(|h| h.id.clone()),\n…\n                },\n            );\n        }\n\n[Showing lines 279-453 of 1270. Use :454 to continue]"}],"details":{"truncation":{"content":"        // have moved): drop those cache entries up front so the compose pass\r\n        // re-derives them exactly once.\r\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n        for cwd in &req.cwds {\r\n            self.cwd_cache.remove(&projderive::normalize_path(Path::new(cwd)));\r\n        }\r\n\r\n        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\r\n        let store = if self.paths.seed_git.exists() {\r\n            Some(\r\n                BranchStore::open_or_init(&self.paths.seed_git)\r\n                    .map_err(|e| format!(\"branch store open: {e}\"))?,\r\n            )\r\n        } else {\r\n            None // no context store yet — sessions/origin still index\r\n        };\r\n        let tips: Vec<(String, String)> = match &store {\r\n            Some(store) => {\r\n                counters.branch_enumerations += 1;\r\n                store\r\n                    .branch_tips_by_recency()\r\n                    .map_err(|e| format!(\"branch enumeration: {e}\"))?\r\n            }\r\n            None => Vec::new(),\r\n        };\r\n        let fingerprint = projindex::fingerprint_tips(&tips);\r\n        let fingerprint_changed = fingerprint != self.last_fingerprint;\r\n\r\n        // ── 2. The no-work exit: unchanged generation performs NO scan ───\r\n        if !fingerprint_changed\r\n            && !force_full\r\n            && req.endpoints.is_empty()\r\n            && req.cwds.is_empty()\r\n            && self.current.is_some()\r\n        {\r\n            let report = CycleReport { counters, published: false, skipped_unchanged: true };\r\n            self.finish_cycle(&report, now, started.elapsed());\r\n            return Ok(report);\r\n        }\r\n\r\n        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\r\n        self.branch_order = tips\r\n            .iter()\r\n            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\r\n            .collect();\r\n        if let (false, Some(store)) = (self.branch_order.is_empty(), &store) {\r\n            let live: BTreeSet<&str> =\r\n                self.branch_order.iter().map(|(_, b)| b.as_str()).collect();\r\n            self.branch_cache.retain(|b, _| live.contains(b.as_str()));\r\n            for (branch, tip) in tips.iter().filter(|(b, _)| b.starts_with(\"p-\")) {\r\n                let cached = self.branch_cache.get(branch);\r\n                if cached.map(|c| &c.tip) == Some(tip) {\r\n                    continue; // unchanged tip → cached membership, no scan\r\n                }\r\n                let files = store\r\n                    .ls_files_at(tip)\r\n                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\r\n                counters.tree_scans += 1;\r\n                let members = files\r\n                    .iter()\r\n                    .filter_map(|f| {\r\n                        let (id, rest) = f.split_once('/')?;\r\n                        (rest == PROJECT_CONTEXT_FILE).then(|| id.to_string())\r\n                    })\r\n                    .collect();\r\n                self.branch_cache\r\n                    .insert(branch.clone(), BranchScan { tip: tip.clone(), members });\r\n            }\r\n        } else {\r\n            self.branch_cache.clear();\r\n        }\r\n\r\n        // ── 4. Which endpoints recompose ──────────────────────────────────\r\n        // A membership change can touch ANY endpoint, so a changed fingerprint\r\n        // (or the backstop / a missing snapshot) recomposes the full roster;\r\n        // otherwise only the scoped rows are patched.\r\n        let full = force_full || fingerprint_changed || self.current.is_none();\r\n        let roster = enumerate_perches(&self.paths.owlery);\r\n        let targets: Vec<(String, PathBuf, Option<String>)> = if full {\r\n            roster\r\n        } else {\r\n            let by_id: HashMap<String, (PathBuf, Option<String>)> = roster\r\n                .into_iter()\r\n                .map(|(id, path, cwd)| (id, (path, cwd)))\r\n                .collect();\r\n            req.endpoints\r\n                .iter()\r\n                .filter_map(|id| {\r\n                    by_id.get(id).map(|(p, c)| (id.clone(), p.clone(), c.clone()))\r\n                })\r\n                .collect()\r\n        };\r\n\r\n        // ── 5. Compose rows through the SHARED precedence kernel ─────────\r\n        // (parity by construction — ADR-0037 §4). The derive closure is the\r\n        // cwd cache: one real derivation per distinct normalized cwd.\r\n        let cache = std::cell::RefCell::new(&mut self.cwd_cache);\r\n        let hits = std::cell::Cell::new(0u64);\r\n        let misses = std::cell::Cell::new(0u64);\r\n        let derivations = std::cell::Cell::new(0u64);\r\n        let derive = |dir: &Path| -> (String, String) {\r\n            let key = projderive::normalize_path(dir);\r\n            let marker = discover_git_marker(dir);\r\n            let stamp = marker.as_deref().and_then(stamp_of);\r\n            let mut cache = cache.borrow_mut();\r\n            if let Some(hit) = cache.get(&key) {\r\n                if hit.marker == marker && hit.stamp == stamp {\r\n                    hits.set(hits.get() + 1);\r\n                    return (hit.id.clone(), hit.display.clone());\r\n                }\r\n            }\r\n            misses.set(misses.get() + 1);\r\n            derivations.set(derivations.get() + 1);\r\n            let (id, display) = spt_store::project::project_id_and_display_for_dir(dir);\r\n            cache.insert(\r\n                key,\r\n                CwdDerivation { id: id.clone(), display: display.clone(), marker, stamp },\r\n            );\r\n            (id, display)\r\n        };\r\n\r\n        let mut endpoints: BTreeMap<String, EndpointProject> = if full {\r\n            BTreeMap::new()\r\n        } else {\r\n            self.current.as_ref().map(|i| i.endpoints.clone()).unwrap_or_default()\r\n        };\r\n        if !full {\r\n            // A scoped id whose perch vanished (purge) drops its row — the\r\n            // roster is the authority; a stale attribution never survives it.\r\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n            let live: BTreeSet<&String> = targets.iter().map(|(id, _, _)| id).collect();\r\n            for id in &req.endpoints {\r\n                if !live.contains(id) {\r\n                    endpoints.remove(id);\r\n                }\r\n            }\r\n        }\r\n        for (id, perch_path, origin_cwd) in &targets {\r\n            let entries = sessions::read_all(perch_path);\r\n            let store_ids: Vec<String> = self\r\n                .branch_order\r\n                .iter()\r\n                .filter(|(_, branch)| {\r\n                    self.branch_cache\r\n                        .get(branch)\r\n                        .is_some_and(|scan| scan.members.contains(id))\r\n                })\r\n                .map(|(project, _)| project.clone())\r\n                .collect();\r\n            let refs = projderive::project_refs_from(\r\n                &entries,\r\n                origin_cwd.as_deref(),\r\n                store_ids,\r\n                &self.paths.owlery,\r\n                derive, // captures only shared refs → Copy; each call reuses one closure\r\n            );\r\n            // The FULL precedence-ordered history is persisted (W3 additive\r\n            // `refs`, REQ-PICKER-PROJECT-HISTORY-TRUTH: the picker projects\r\n            // history, not head); the head fields stay beside it for W2-shaped\r\n            // consumers and the status surface.\r\n            // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\r\n            let head = refs.first().cloned();\r\n            let ref_entries: Vec<projindex::ProjectRefEntry> = refs\r\n                .into_iter()\r\n                .map(|r| projindex::ProjectRefEntry {\r\n                    id: r.id,\r\n                    dir: r.dir,\r\n                    display: r.display,\r\n                    source: Some(r.source.as_str().to_string()),\r\n                })\r\n                .collect();\r\n            endpoints.insert(\r\n                id.clone(),\r\n                EndpointProject {\r\n                    project_id: head.as_ref().map(|h| h.id.clone()),\r","truncated":true,"truncatedBy":"lines","totalLines":992,"totalBytes":8191,"outputLines":175,"outputBytes":8191,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"        // have moved): drop those cache entries up front so the compose pass\r\n        // re-derives them exactly once.\r\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n        for cwd in &req.cwds {\r\n            self.cwd_cache.remove(&projderive::normalize_path(Path::new(cwd)));\r\n        }\r\n\r\n        // ── 1. ONE branch enumeration → fingerprint ──────────────────────\r\n        let store = if self.paths.seed_git.exists() {\r\n            Some(\r\n                BranchStore::open_or_init(&self.paths.seed_git)\r\n                    .map_err(|e| format!(\"branch store open: {e}\"))?,\r\n            )\r\n        } else {\r\n            None // no context store yet — sessions/origin still index\r\n        };\r\n        let tips: Vec<(String, String)> = match &store {\r\n            Some(store) => {\r\n                counters.branch_enumerations += 1;\r\n                store\r\n                    .branch_tips_by_recency()\r\n                    .map_err(|e| format!(\"branch enumeration: {e}\"))?\r\n            }\r\n            None => Vec::new(),\r\n        };\r\n        let fingerprint = projindex::fingerprint_tips(&tips);\r\n        let fingerprint_changed = fingerprint != self.last_fingerprint;\r\n\r\n        // ── 2. The no-work exit: unchanged generation performs NO scan ───\r\n        if !fingerprint_changed\r\n            && !force_full\r\n            && req.endpoints.is_empty()\r\n            && req.cwds.is_empty()\r\n            && self.current.is_some()\r\n        {\r\n            let report = CycleReport { counters, published: false, skipped_unchanged: true };\r\n            self.finish_cycle(&report, now, started.elapsed());\r\n            return Ok(report);\r\n        }\r\n\r\n        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\r\n        self.branch_order = tips\r\n            .iter()\r\n            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\r\n            .collect();\r\n        if let (false, Some(store)) = (self.branch_order.is_empty(), &store) {\r\n            let live: BTreeSet<&str> =\r\n                self.branch_order.iter().map(|(_, b)| b.as_str()).collect();\r\n            self.branch_cache.retain(|b, _| live.contains(b.as_str()));\r\n            for (branch, tip) in tips.iter().filter(|(b, _)| b.starts_with(\"p-\")) {\r\n                let cached = self.branch_cache.get(branch);\r\n                if cached.map(|c| &c.tip) == Some(tip) {\r\n                    continue; // unchanged tip → cached membership, no scan\r\n                }\r\n                let files = store\r\n                    .ls_files_at(tip)\r\n                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\r\n                counters.tree_scans += 1;\r\n                let members = files\r\n                    .iter()\r\n                    .filter_map(|f| {\r\n                        let (id, rest) = f.split_once('/')?;\r\n                        (rest == PROJECT_CONTEXT_FILE).then(|| id.to_string())\r\n                    })\r\n                    .collect();\r\n                self.branch_cache\r\n                    .insert(branch.clone(), BranchScan { tip: tip.clone(), members });\r\n            }\r\n        } else {\r\n            self.branch_cache.clear();\r\n        }\r\n\r\n        // ── 4. Which endpoints recompose ──────────────────────────────────\r\n        // A membership change can touch ANY endpoint, so a changed fingerprint\r\n        // (or the backstop / a missing snapshot) recomposes the full roster;\r\n        // otherwise only the scoped rows are patched.\r\n        let full = force_full || fingerprint_changed || self.current.is_none();\r\n        let roster = enumerate_perches(&self.paths.owlery);\r\n        let targets: Vec<(String, PathBuf, Option<String>)> = if full {\r\n            roster\r\n        } else {\r\n            let by_id: HashMap<String, (PathBuf, Option<String>)> = roster\r\n                .into_iter()\r\n                .map(|(id, path, cwd)| (id, (path, cwd)))\r\n                .collect();\r\n            req.endpoints\r\n                .iter()\r\n                .filter_map(|id| {\r\n                    by_id.get(id).map(|(p, c)| (id.clone(), p.clone(), c.clone()))\r\n                })\r\n                .collect()\r\n        };\r\n\r\n        // ── 5. Compose rows through the SHARED precedence kernel ─────────\r\n        // (parity by construction — ADR-0037 §4). The derive closure is the\r\n        // cwd cache: one real derivation per distinct normalized cwd.\r\n        let cache = std::cell::RefCell::new(&mut self.cwd_cache);\r\n        let hits = std::cell::Cell::new(0u64);\r\n        let misses = std::cell::Cell::new(0u64);\r\n        let derivations = std::cell::Cell::new(0u64);\r\n        let derive = |dir: &Path| -> (String, String) {\r\n            let key = projderive::normalize_path(dir);\r\n            let marker = discover_git_marker(dir);\r\n            let stamp = marker.as_deref().and_then(stamp_of);\r\n            let mut cache = cache.borrow_mut();\r\n            if let Some(hit) = cache.get(&key) {\r\n                if hit.marker == marker && hit.stamp == stamp {\r\n                    hits.set(hits.get() + 1);\r\n                    return (hit.id.clone(), hit.display.clone());\r\n                }\r\n            }\r\n            misses.set(misses.get() + 1);\r\n            derivations.set(derivations.get() + 1);\r\n            let (id, display) = spt_store::project::project_id_and_display_for_dir(dir);\r\n            cache.insert(\r\n                key,\r\n                CwdDerivation { id: id.clone(), display: display.clone(), marker, stamp },\r\n            );\r\n            (id, display)\r\n        };\r\n\r\n        let mut endpoints: BTreeMap<String, EndpointProject> = if full {\r\n            BTreeMap::new()\r\n        } else {\r\n            self.current.as_ref().map(|i| i.endpoints.clone()).unwrap_or_default()\r\n        };\r\n        if !full {\r\n            // A scoped id whose perch vanished (purge) drops its row — the\r\n            // roster is the authority; a stale attribution never survives it.\r\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n            let live: BTreeSet<&String> = targets.iter().map(|(id, _, _)| id).collect();\r\n            for id in &req.endpoints {\r\n                if !live.contains(id) {\r\n                    endpoints.remove(id);\r\n                }\r\n            }\r\n        }\r\n        for (id, perch_path, origin_cwd) in &targets {\r\n            let entries = sessions::read_all(perch_path);\r\n            let store_ids: Vec<String> = self\r\n                .branch_order\r\n                .iter()\r\n                .filter(|(_, branch)| {\r\n                    self.branch_cache\r\n                        .get(branch)\r\n                        .is_some_and(|scan| scan.members.contains(id))\r\n                })\r\n                .map(|(project, _)| project.clone())\r\n                .collect();\r\n            let refs = projderive::project_refs_from(\r\n                &entries,\r\n                origin_cwd.as_deref(),\r\n                store_ids,\r\n                &self.paths.owlery,\r\n                derive, // captures only shared refs → Copy; each call reuses one closure\r\n            );\r\n            // The FULL precedence-ordered history is persisted (W3 additive\r\n            // `refs`, REQ-PICKER-PROJECT-HISTORY-TRUTH: the picker projects\r\n            // history, not head); the head fields stay beside it for W2-shaped\r\n            // consumers and the status surface.\r\n            // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\r\n            let head = refs.first().cloned();\r\n            let ref_entries: Vec<projindex::ProjectRefEntry> = refs\r\n                .into_iter()\r\n                .map(|r| projindex::ProjectRefEntry {\r\n                    id: r.id,\r\n                    dir: r.dir,\r\n                    display: r.display,\r\n                    source: Some(r.source.as_str().to_string()),\r\n                })\r\n                .collect();\r\n            endpoints.insert(\r\n                id.clone(),\r\n                EndpointProject {\r\n                    project_id: head.as_ref().map(|h| h.id.clone()),\r\n…\n                },\n            );\n        }","startLine":279,"lineNumbers":[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,null,458,459,460]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\projwriter.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1270,"totalBytes":8191,"outputLines":175,"outputBytes":8191,"shownRange":{"start":279,"end":453},"nextOffset":454}}},"isError":false,"timestamp":1784271734483}}
{"type":"message","id":"bd56ae40","parentId":"5206fd52","timestamp":"2026-07-17T07:02:14.503Z","message":{"role":"toolResult","toolCallId":"call_UIDNOcOOqOeDLzFfx9FyOxA4|fc_0e15f89f5b7b4062016a59d375a278819485fbb723a455a9f9","toolName":"read","content":[{"type":"text","text":"\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\n/// flips across a swap; the D7-4 field run asserts it matches the released\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn write_ready(generation: u64) {\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\n    if let Some(hash) = current_exe_hash() {\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\n    }\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\n}\n\n/// Lowercase-hex SHA-256 of the running process's own executable file\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\n/// read + hash per brain start — negligible, and it pays rent far beyond the\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\nfn current_exe_hash() -> Option<String> {\n    let exe = std::env::current_exe().ok()?;\n    let bytes = std::fs::read(exe).ok()?;\n    Some(crate::release::sha256_hex(&bytes))\n}\n\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\npub fn parse_ready_generation(body: &str) -> Option<u64> {\n    serde_json::from_str::<serde_json::Value>(body)\n        .ok()?\n        .get(\"generation\")?\n        .as_u64()\n}\n\n[Showing lines 349-393 of 2116. Use :394 to continue]"}],"details":{"truncation":{"content":"\r\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\r\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\r\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\r\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\r\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\r\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\r\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\r\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\r\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\r\n/// flips across a swap; the D7-4 field run asserts it matches the released\r\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\r\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\r\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\r\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn write_ready(generation: u64) {\r\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\r\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\r\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\r\n    if let Some(hash) = current_exe_hash() {\r\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\r\n    }\r\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\r\n}\r\n\r\n/// Lowercase-hex SHA-256 of the running process's own executable file\r\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\r\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\r\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\r\n/// read + hash per brain start — negligible, and it pays rent far beyond the\r\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\r\nfn current_exe_hash() -> Option<String> {\r\n    let exe = std::env::current_exe().ok()?;\r\n    let bytes = std::fs::read(exe).ok()?;\r\n    Some(crate::release::sha256_hex(&bytes))\r\n}\r\n\r\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\r\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\r\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\r\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn parse_ready_generation(body: &str) -> Option<u64> {\r\n    serde_json::from_str::<serde_json::Value>(body)\r","truncated":true,"truncatedBy":"lines","totalLines":1768,"totalBytes":2827,"outputLines":45,"outputBytes":2827,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n/// Write the readiness breadcrumb (best-effort), restoration D6-1b + D7-1: a\r\n/// self-describing JSON `{pid, generation, exe_hash}`. A supervisor/test reads\r\n/// the `pid` to confirm the brain is up and (on a change) that it respawned; the\r\n/// `generation` is the **readiness gate's** key (ADR-0018 Q7, D6-2) — the\r\n/// supervisor that spawned generation N promotes only on a ready stamp **== N**,\r\n/// so a stale stamp from a prior generation cannot falsely promote a brain that\r\n/// never booted. **`exe_hash` (D7-1)** is the lowercase-hex SHA-256 of *this\r\n/// brain's own executable* — the \"which bytes are resident\" diagnostic: it proves\r\n/// a respawn landed on the **swapped** binary (the D7-1 survival E2E asserts it\r\n/// flips across a swap; the D7-4 field run asserts it matches the released\r\n/// artifact's hash — the fact the v0.3.2/enlyzeam incident lacked). It is an\r\n/// **additive / N-1-readable** field (an N-1 brain ignores it; absence parses to\r\n/// `None`) registered in the D6-3 [`crate::rollback_compat::PRE_READY_DURABLE_FILES`]\r\n/// tripwire. Liveness of the *daemon* stays the seed-control ping, never this file.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn write_ready(generation: u64) {\r\n    let mut obj = serde_json::json!({ \"pid\": std::process::id(), \"generation\": generation });\r\n    // Best-effort: omit `exe_hash` if the self-read fails (additive — a reader\r\n    // tolerates its absence). Never fail the ready write over a diagnostic field.\r\n    if let Some(hash) = current_exe_hash() {\r\n        obj[\"exe_hash\"] = serde_json::Value::String(hash);\r\n    }\r\n    let _ = spt_store::atomic::atomic_write_string(&brain_ready_path(), &obj.to_string());\r\n}\r\n\r\n/// Lowercase-hex SHA-256 of the running process's own executable file\r\n/// (best-effort; `None` if `current_exe` or the file read fails — degrades safe,\r\n/// the breadcrumb simply omits the field). Restoration D7-1: the `exe_hash`\r\n/// breadcrumb that proves *which bytes* a brain generation is running. One exe\r\n/// read + hash per brain start — negligible, and it pays rent far beyond the\r\n/// D7-1 test (the resident-bytes diagnostic the enlyzeam regression lacked).\r\nfn current_exe_hash() -> Option<String> {\r\n    let exe = std::env::current_exe().ok()?;\r\n    let bytes = std::fs::read(exe).ok()?;\r\n    Some(crate::release::sha256_hex(&bytes))\r\n}\r\n\r\n/// Parse the `generation` stamp out of a `brain.ready` body — pure, so the\r\n/// staleness gate (D6-2) is unit-tested with explicit values, no wall-clock or\r\n/// real process. `None` for an absent/garbled/legacy-bare-pid body (the gate\r\n/// reads that as not-ready — fail-safe). Restoration D6-1b.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\npub fn parse_ready_generation(body: &str) -> Option<u64> {\r\n    serde_json::from_str::<serde_json::Value>(body)\r\n        .ok()?\n        .get(\"generation\")?\n        .as_u64()\n}","startLine":349,"lineNumbers":[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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":2827,"outputLines":45,"outputBytes":2827,"shownRange":{"start":349,"end":393},"nextOffset":394}}},"isError":false,"timestamp":1784271734503}}
{"type":"message","id":"138dbdff","parentId":"bd56ae40","timestamp":"2026-07-17T07:02:14.524Z","message":{"role":"toolResult","toolCallId":"call_0MOQNrzMJfTEEbyktZEXDuKU|fc_0e15f89f5b7b4062016a59d375a2588194a1b665c92c40fd0d","toolName":"read","content":[{"type":"text","text":"pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n…\n    eprintln!(\n…\n    );\n\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\n    // from its delivered cursor, the production replacement for the retired\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    match brain.resume_session_cursors() {\n        Ok(ids) if !ids.is_empty() => {\n            eprintln!(\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\n                ids.len()\n            )\n        }\n        Ok(_) => {}\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\n    }\n\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\n    // breadcrumb would accept a brain that connected but never rebuilt\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\n    // this is forward-correct: the gate is defined on the real readiness\n    // semantic the live-agent adapter inherits.\n    write_ready(generation);\n\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\n    // online/offline exclusivity — a reconcile loop that runs each offline\n    // instance's `wake_command` as a supervised watcher child. Relocated from\n    // the broker process to the brain child: shellwake is brain-owned logic\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n    // posture. The stop flag is held for symmetry, never raised (the watchers\n    // die with the brain process and the next brain re-sweeps them).\n    // [impl->REQ-SHELL-2]\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\n\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n    // shell waker). Online-only is staged behind the interim `api listen`\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n    // brain the first-host. The stop flag is held for symmetry; the threads die\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n    // [impl->REQ-DAEMON-1]\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\n\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\n    // reconcile in the background (ready-without-warm is preserved because\n    // write_ready already fired above), then drain/debounce/coalesce the\n    // invalidation queue. Same posture as the hosts above: the stop flag is\n    // held for symmetry, the thread dies with the brain, and the next brain\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\n    // [impl->REQ-PROJECT-INDEX-WRITER]\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\n\n    let mut consumers_started = false;\n    loop {\n        thread::sleep(BRAIN_HEARTBEAT);\n        // `net-status` is both the net-capability probe and the broker-liveness\n        // probe: an `Err` is the broker gone (or a future planned\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\n        let status = match brain.net_status() {\n            Ok(s) => s,\n            Err(e) => {\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\n                return Err(e);\n            }\n        };\n        if consumer_gate(status.enabled, consumers_started) {\n            let hex = status.node_id_hex.clone().unwrap_or_default();\n            spawn_net_consumers(&hex);\n            consumers_started = true;\n            eprintln!(\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\n            );\n        }\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\n        // window, so without this the wedge never drains: the promotion DRAINED gate\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\n        // best-effort — a transient sessions error must never fell the heartbeat (the\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\n        // its own trial against a live older broker — no coordinated broker restart.\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n        let _ = brain.sessions();\n        write_ready(generation);\n    }\n}\n\n/// Whether to spawn the net consumers this heartbeat: only once the broker\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\n/// semantics that keep a single dispatcher/peer-pump set running.\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\n    net_enabled && !already_started\n}\n\n[Showing lines 179-308 of 2116. Use :309 to continue]"}],"details":{"truncation":{"content":"    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r","truncated":true,"truncatedBy":"lines","totalLines":1938,"totalBytes":8103,"outputLines":130,"outputBytes":8103,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n…\n    eprintln!(\n…\n    );\r\n\r\n    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild\r\n    // continuity from the broker — re-attach EVERY hosted session in resume mode\r\n    // from its delivered cursor, the production replacement for the retired\r\n    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\r\n    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\r\n    // so this is a no-op now and forward-correct when daemon-hosted sessions land\r\n    // (the live-agent adapter). A failure here is non-fatal — the brain still\r\n    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\r\n    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\r\n    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\r\n    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\r\n    // CURSOR-ONLY resume (UPDATE-WEDGE round 3, REQ-BRAIN-RESUME-NO-CONN-DEADLOCK,\r\n    // doyle Option A-split): the supervised daemon brain is a NON-DRAINING consumer of\r\n    // this conn (the heartbeat below reads it only in net_status()/sessions()\r\n    // drain-and-discard loops), so it MUST NOT subscribe — an undrained subscription on\r\n    // this request/reply conn is the SharedSend self-deadlock that re-wedged v0.30.4\r\n    // (brain.rs resume_sessions deadlock warning). `resume_session_cursors` re-establishes\r\n    // the per-session dedup cursors + returns the ids WITHOUT subscribing; the subscribing\r\n    // `resume_sessions` stays for DRAINING callers (the resume int tests / the forward\r\n    // live-agent-adapter drained carrier). Empty today (the daemon hosts no PTY sessions).\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    match brain.resume_session_cursors() {\r\n        Ok(ids) if !ids.is_empty() => {\r\n            eprintln!(\r\n                \"BRAIN_RESUMED: re-established {} session cursor(s): {ids:?}\",\r\n                ids.len()\r\n            )\r\n        }\r\n        Ok(_) => {}\r\n        Err(e) => eprintln!(\"BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing\"),\r\n    }\r\n\r\n    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018\r\n    // Q7): \"ready\" is the design's \"re-attached all sessions + resumed loops\",\r\n    // not merely \"socket connected\". The supervisor's readiness gate (D6-2)\r\n    // promotes the trial binary on this stamp; promoting on a bare post-connect\r\n    // breadcrumb would accept a brain that connected but never rebuilt\r\n    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so\r\n    // this is forward-correct: the gate is defined on the real readiness\r\n    // semantic the live-agent adapter inherits.\r\n    write_ready(generation);\r\n\r\n    // Shell wake-watchers (restoration D2-2): the offline half of the shell\r\n    // online/offline exclusivity — a reconcile loop that runs each offline\r\n    // instance's `wake_command` as a supervised watcher child. Relocated from\r\n    // the broker process to the brain child: shellwake is brain-owned logic\r\n    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the\r\n    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\r\n    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\r\n    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\r\n    // posture. The stop flag is held for symmetry, never raised (the watchers\r\n    // die with the brain process and the next brain re-sweeps them).\r\n    // [impl->REQ-SHELL-2]\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));\r\n\r\n    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller\r\n    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at\r\n    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting\r\n    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\r\n    // shell waker). Online-only is staged behind the interim `api listen`\r\n    // (status-absent perches are skipped — no double-host) until W0.2 makes the\r\n    // brain the first-host. The stop flag is held for symmetry; the threads die\r\n    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\r\n    // [impl->REQ-DAEMON-1]\r\n    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);\r\n\r\n    // Materialized project-index writer (ADR-0037 §2, PROJECT-INDEX W2): the\r\n    // brain hosts the SOLE single-flight writer — load persisted index, boot\r\n    // reconcile in the background (ready-without-warm is preserved because\r\n    // write_ready already fired above), then drain/debounce/coalesce the\r\n    // invalidation queue. Same posture as the hosts above: the stop flag is\r\n    // held for symmetry, the thread dies with the brain, and the next brain\r\n    // rehydrates from the persisted index (warm start, fingerprint-gated).\r\n    // [impl->REQ-PROJECT-INDEX-WRITER]\r\n    let _ = crate::projwriter::spawn_index_writer(Arc::new(AtomicBool::new(false)));\r\n\r\n    let mut consumers_started = false;\r\n    loop {\r\n        thread::sleep(BRAIN_HEARTBEAT);\r\n        // `net-status` is both the net-capability probe and the broker-liveness\r\n        // probe: an `Err` is the broker gone (or a future planned\r\n        // snapshot+self-exit, Q3) — exit for a supervised respawn.\r\n        let status = match brain.net_status() {\r\n            Ok(s) => s,\r\n            Err(e) => {\r\n                eprintln!(\"BRAIN_BROKER_LOST: {e} — exiting for supervised respawn\");\r\n                return Err(e);\r\n            }\r\n        };\r\n        if consumer_gate(status.enabled, consumers_started) {\r\n            let hex = status.node_id_hex.clone().unwrap_or_default();\r\n            spawn_net_consumers(&hex);\r\n            consumers_started = true;\r\n            eprintln!(\r\n                \"BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started\"\r\n            );\r\n        }\r\n        // DRIVE the broker's controller-liveness reap every heartbeat (UPDATE-WEDGE\r\n        // fix, counter-54). A `KIND_SESSIONS` poll is what runs the broker's\r\n        // `reap_dead_controller` sweep (broker.rs), which stall-evicts a black-holed\r\n        // LOCAL controller conn a HARD-KILLED prior brain generation left behind (its\r\n        // hosted PTYs keep writing to a dead pipe → the broker-side writer BLOCKS, never\r\n        // EOFs). Nothing else polls `KIND_SESSIONS` during an isolated brain-trial\r\n        // window, so without this the wedge never drains: the promotion DRAINED gate\r\n        // (`old_gen_drained()` = `!any_local_controller_wedged()`, a PURE READ that never\r\n        // drives the evict) stays false for the full trial → \"alive but never ready\"\r\n        // rollback (the 2026-07-09 field freeze), AND the same wedge black-holes recovery\r\n        // (the late `BRAIN_SUBSCRIBER_STALL_EVICT`). Driven on the FIRST heartbeat and on\r\n        // BOTH the trial candidate AND the rollback/recovery brain (one loop covers both);\r\n        // best-effort — a transient sessions error must never fell the heartbeat (the\r\n        // net_status probe above already owns broker-liveness). Self-applies: it drives\r\n        // the CURRENT (older) broker's already-shipped reap verb, so a new brain un-strands\r\n        // its own trial against a live older broker — no coordinated broker restart.\r\n        // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n        let _ = brain.sessions();\r\n        write_ready(generation);\r\n    }\r\n}\r\n\r\n/// Whether to spawn the net consumers this heartbeat: only once the broker\r\n/// reports a net endpoint up (`net_enabled`) AND we have not already started\r\n/// them. Pure for the unit table — the spawn-exactly-once + net-gating\r\n/// semantics that keep a single dispatcher/peer-pump set running.\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn consumer_gate(net_enabled: bool, already_started: bool) -> bool {\r\n    net_enabled && !already_started\r\n}\r","startLine":159,"lineNumbers":[159,null,175,null,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":8103,"outputLines":130,"outputBytes":8103,"shownRange":{"start":179,"end":308},"nextOffset":309}}},"isError":false,"timestamp":1784271734524}}
{"type":"message","id":"faefec95","parentId":"138dbdff","timestamp":"2026-07-17T07:02:23.703Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing BRAIN_HEARTBEAT lines 57-64**"},{"type":"toolCall","id":"call_fx8uo1OCUA7y7sGMWWbdCpEe|fc_0e15f89f5b7b4062016a59d37f086081949ef01cdc7e5bfef6","name":"read","arguments":{"i":"Reading heartbeat cadence","path":"crates/spt-daemon/src/brainproc.rs","selector":"54-70"}},{"type":"toolCall","id":"call_0M6U1CNpj7EllEqRxpGChsG6|fc_0e15f89f5b7b4062016a59d37f09d08194801d140796ca65c2","name":"read","arguments":{"i":"Reading recent project scanner","path":"crates/spt-daemon/src/registryhost.rs","selector":"875-930"}},{"type":"toolCall","id":"call_Q4cQ8cFQb1YIc4wlJNxSB25V|fc_0e15f89f5b7b4062016a59d37f09dc8194ab30bbb0d25aeeb4","name":"grep","arguments":{"i":"Finding heartbeat write path","pattern":"pub fn atomic_write_string|fn atomic_write_string|atomic_write_string_durable|atomic_write\\(","path":"crates/spt-store/src/atomic.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_g3jWU2T4vXCUyS7KRc6RspRf|fc_0e15f89f5b7b4062016a59d37f09e881949909abeeba29af9f","name":"read","arguments":{"i":"Reading atomic ready write","path":"crates/spt-store/src/atomic.rs","selector":"1-115"}},{"type":"toolCall","id":"call_aGKdbnZeGyFaQEsoye3zqESs|fc_0e15f89f5b7b4062016a59d37f09f08194b5da00fefe56fef6","name":"grep","arguments":{"i":"Finding registry policy loads","pattern":"struct RegistryGatePolicy|impl RegistryGatePolicy|pub fn load\\(|Subnets|RosterStore","path":"crates/spt-daemon/src/registryhost.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_DvJsn2GdS7acbBdzRfqMU2OL|fc_0e15f89f5b7b4062016a59d37f09f88194a3efd84e1b422a23","name":"read","arguments":{"i":"Reading registry policy load","path":"crates/spt-daemon/src/registryhost.rs","selector":"40-110"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":12036,"output":320,"cacheRead":105984,"cacheWrite":0,"totalTokens":118340,"reasoningTokens":17,"cost":{"input":0.060180000000000004,"output":0.009600000000000001,"cacheRead":0.052992,"cacheWrite":0,"total":0.122772}},"stopReason":"toolUse","timestamp":1784271734570,"responseId":"resp_0e15f89f5b7b4062016a59d3770e488194b6f1a94cbec0bf4a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d3791c688194aa9064a8f0942d17","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdN_ZZjcn0WQ9-DSP_4j-w9ThPhQ42ksFQLQUL7Oh0e5ohwikLMEmHv-BGtWElUCitvsC_iPD-vQcICHecKK7Vvj3Mbo02EZEFZM1hssKRDVT1xq0VT4dRbS4Jk4CvZXIuJCEiEov-yIbsBw-X8xwusr6WBZlvSkWF36sP4cdr2DV8p0az0lnokX8HnHpkZxpSlI_OsmYS4kiwdH4iJAfjUhvOi6TJ8lkERl2p3-nTgtTbQOGSRcMEKlBs7EzP5RCEiU1FEw3lEE322xQwEcweU3jh_rQTdTSaqVSa9JvzfYGN0AcNmDQVf60q4U9-9IUtVSKNGIc3A5IuzdQWcYXcdzaFPEL3NYXqv-i1F0UOZr_1FhatciW5yyMlOps_9M_SdympWwwH0NdxJ-jZFBn9pkXHUstJWH30xuX4886xdE7FJ18Cx3joi3sz7qUuOxnRCxbGv5KrM_k3l4ZWMw9e-K670oKTxvCq-UfNFDqMtkzNALBsnD4tOHglXw6dKcCE1TVOC5yZocOvl1m1v2KAp39wapGSAYpBTPt6t1GdmcY6ImHl9lq66spwfbvqHWVA21nW7j1wjVx2gAy46avI-gi745utDVAgMx9dBJ3gm0nCt2oOpsW3tNuRtuYcXrp8Pv6Xoq9PQ6RvMXp_pGJ6ROMZOTUg1uoRH-TZBwjrNQWpeFv01c-jP3fGyF8gXmvjKGVAsyAxQlQ0aiy7roUqHn-mN0G5h61qKTuOt9hQ3fxds7gFLcPmKhso4F9AijBFJevj8BKKjbGYPBOaax3byunQkIlkmNh2nh4zz6bkFc2dQrvwrJeofCE5Xn__Ci_OriJQtk2FIqacq2GxKkElmPofJnb1JI8KfiS9ztIcFEy9XDBpYbzKt7O6g1isll9gPmCYxO1q7infzrVR2PeMjEBhMX4WeiVawo2e1vGm7OiXvUjpmszvuadKbBeJu0GxFnqhdFKcX9OYVaB0jy-Ngm-g==","summary":[{"type":"summary_text","text":"**Reviewing BRAIN_HEARTBEAT lines 57-64**"}]},{"id":"fc_0e15f89f5b7b4062016a59d37f086081949ef01cdc7e5bfef6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading heartbeat cadence\",\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"selector\":\"54-70\"}","call_id":"call_fx8uo1OCUA7y7sGMWWbdCpEe","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d37f09d08194801d140796ca65c2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading recent project scanner\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"875-930\"}","call_id":"call_0M6U1CNpj7EllEqRxpGChsG6","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d37f09dc8194ab30bbb0d25aeeb4","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding heartbeat write path\",\"pattern\":\"pub fn atomic_write_string|fn atomic_write_string|atomic_write_string_durable|atomic_write\\\\(\",\"path\":\"crates/spt-store/src/atomic.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Q4cQ8cFQb1YIc4wlJNxSB25V","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d37f09e881949909abeeba29af9f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading atomic ready write\",\"path\":\"crates/spt-store/src/atomic.rs\",\"selector\":\"1-115\"}","call_id":"call_g3jWU2T4vXCUyS7KRc6RspRf","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d37f09f08194b5da00fefe56fef6","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding registry policy loads\",\"pattern\":\"struct RegistryGatePolicy|impl RegistryGatePolicy|pub fn load\\\\(|Subnets|RosterStore\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_aGKdbnZeGyFaQEsoye3zqESs","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d37f09f88194a3efd84e1b422a23","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry policy load\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"40-110\"}","call_id":"call_DvJsn2GdS7acbBdzRfqMU2OL","name":"read"}]},"duration":9131.751000002027,"ttft":2553.5750000029802,"contextSnapshot":{"promptTokens":118020,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_fx8uo1OCUA7y7sGMWWbdCpEe|fc_0e15f89f5b7b4062016a59d37f086081949ef01cdc7e5bfef6","toolName":"read","startedAt":"2026-07-17T07:02:23.703Z","args":{"path":"crates/spt-daemon/src/brainproc.rs"},"intent":"Reading heartbeat cadence"},"id":"c3af47b0","parentId":"faefec95","timestamp":"2026-07-17T07:02:23.703Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0M6U1CNpj7EllEqRxpGChsG6|fc_0e15f89f5b7b4062016a59d37f09d08194801d140796ca65c2","toolName":"read","startedAt":"2026-07-17T07:02:23.703Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading recent project scanner"},"id":"138ae88a","parentId":"c3af47b0","timestamp":"2026-07-17T07:02:23.703Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Q4cQ8cFQb1YIc4wlJNxSB25V|fc_0e15f89f5b7b4062016a59d37f09dc8194ab30bbb0d25aeeb4","toolName":"grep","startedAt":"2026-07-17T07:02:23.703Z","args":{"path":"crates/spt-store/src/atomic.rs"},"intent":"Finding heartbeat write path"},"id":"873a7e31","parentId":"138ae88a","timestamp":"2026-07-17T07:02:23.703Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_g3jWU2T4vXCUyS7KRc6RspRf|fc_0e15f89f5b7b4062016a59d37f09e881949909abeeba29af9f","toolName":"read","startedAt":"2026-07-17T07:02:23.703Z","args":{"path":"crates/spt-store/src/atomic.rs"},"intent":"Reading atomic ready write"},"id":"ff8425d6","parentId":"873a7e31","timestamp":"2026-07-17T07:02:23.703Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_aGKdbnZeGyFaQEsoye3zqESs|fc_0e15f89f5b7b4062016a59d37f09f08194b5da00fefe56fef6","toolName":"grep","startedAt":"2026-07-17T07:02:23.704Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Finding registry policy loads"},"id":"5389fb2f","parentId":"ff8425d6","timestamp":"2026-07-17T07:02:23.704Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DvJsn2GdS7acbBdzRfqMU2OL|fc_0e15f89f5b7b4062016a59d37f09f88194a3efd84e1b422a23","toolName":"read","startedAt":"2026-07-17T07:02:23.704Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading registry policy load"},"id":"10e06435","parentId":"5389fb2f","timestamp":"2026-07-17T07:02:23.704Z"}
{"type":"message","id":"e32ee632","parentId":"10e06435","timestamp":"2026-07-17T07:02:23.705Z","message":{"role":"toolResult","toolCallId":"call_Q4cQ8cFQb1YIc4wlJNxSB25V|fc_0e15f89f5b7b4062016a59d37f09dc8194ab30bbb0d25aeeb4","toolName":"grep","content":[{"type":"text","text":" 57|// [impl->REQ-HAZARD-EBUSY-RENAME]\n*58|pub fn atomic_write_string(path: &Path, body: &str) -> io::Result<()> {\n 59|    atomic_write_bytes(path, body.as_bytes())\n 60|}\n 61|\n...\n 99|// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n*100|pub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\n 101|    atomic_write_bytes_durable(path, body.as_bytes())\n 102|}\n 103|\n...\n 270|        let s_target = dir.path().join(\"rec.json\");\n*271|        atomic_write_string_durable(&s_target, \"{\\\"id\\\":\\\"hall-a\\\"}\").unwrap();\n 272|        assert_eq!(fs::read_to_string(&s_target).unwrap(), \"{\\\"id\\\":\\\"hall-a\\\"}\");\n 273|        no_tmp_sibling_remains(dir.path());\n 274|    }"}],"details":{"scopePath":"crates/spt-store/src/atomic.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\atomic.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":3,"fileCount":1,"files":["crates/spt-store/src/atomic.rs"],"fileMatches":[{"path":"crates/spt-store/src/atomic.rs","count":3}],"truncated":false,"displayContent":"  57│// [impl->REQ-HAZARD-EBUSY-RENAME]\n *58│pub fn atomic_write_string(path: &Path, body: &str) -> io::Result<()> {\n  59│    atomic_write_bytes(path, body.as_bytes())\n  60│}\n  61│\n    │...\n  99│// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n*100│pub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\n 101│    atomic_write_bytes_durable(path, body.as_bytes())\n 102│}\n 103│\n    │...\n 270│        let s_target = dir.path().join(\"rec.json\");\n*271│        atomic_write_string_durable(&s_target, \"{\\\"id\\\":\\\"hall-a\\\"}\").unwrap();\n 272│        assert_eq!(fs::read_to_string(&s_target).unwrap(), \"{\\\"id\\\":\\\"hall-a\\\"}\");\n 273│        no_tmp_sibling_remains(dir.path());\n 274│    }"},"isError":false,"timestamp":1784271743705}}
{"type":"message","id":"e3a55158","parentId":"e32ee632","timestamp":"2026-07-17T07:02:23.706Z","message":{"role":"toolResult","toolCallId":"call_aGKdbnZeGyFaQEsoye3zqESs|fc_0e15f89f5b7b4062016a59d37f09f08194b5da00fefe56fef6","toolName":"grep","content":[{"type":"text","text":" 40|use spt_store::perch::{self, ParentHint};\n*41|use spt_store::roster::RosterStore;\n 42|use spt_store::subnet::SubnetStore;\n 43|use spt_store::visibility::VisibilityStore;\n 44|\n...\n 58|/// without a restart; the access-gate stance shared with sync/notif).\n*59|pub struct RegistryGatePolicy {\n 60|    /// Names of every subnet this node is a member of.\n 61|    pub subnets: Vec<String>,\n 62|    /// The membership directory (Mesh-D5, REQ-MESH-5): authorization is now\n...\n 65|    /// so this only ever runs against a peer that already proved seed knowledge.\n*66|    pub roster: RosterStore,\n 67|}\n 68|\n*69|impl RegistryGatePolicy {\n 70|    /// Load from the node's canonical stores.\n*71|    pub fn load() -> RegistryGatePolicy {\n 72|        let subnets = SubnetStore::load()\n 73|            .subnets\n 74|            .iter()\n...\n 78|            subnets,\n*79|            roster: RosterStore::load(),\n 80|        }\n 81|    }\n 82|\n...\n 969|/// The roster tombstone is the load-bearing eviction since Mesh-D5a: the\n*970|/// inbound gates authorize on [`RosterStore::is_member`], so a re-keyed identity\n 971|/// that kept its roster entry would stay a member. Tombstone (not a bare remove)\n 972|/// because the roster is a grow-set: a plain delete re-merges from any peer\n 973|/// that still lists the dead key. The D3 tombstone primitive dominates +\n...\n 979|pub fn repair_evict_superseded(\n*980|    roster: &mut spt_store::roster::RosterStore,\n 981|    subnet: &str,\n 982|    joiner_hex: &str,\n 983|    intro: &spt_net::net::pairing::wire::NodeIntro,\n...\n 1204|    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {\n*1205|        let mut roster = RosterStore::default();\n 1206|        roster.merge_entry(member(subnet, trusted));\n 1207|        RegistryGatePolicy {\n 1208|            subnets: vec![subnet.to_string()],\n...\n 1330|        // Roster membership in both subnets — the gate-relevant state (D5a).\n*1331|        let mut roster = RosterStore::default();\n 1332|        roster.merge_entry(member(\"home\", \"old-key\"));\n 1333|        roster.merge_entry(member(\"worknet\", \"old-key\"));\n 1334|        assert!(roster.is_member(\"home\", \"old-key\"), \"member before re-pair\");\n...\n 1382|        // No-match intro: nothing evicts, no marker, no notice.\n*1383|        let mut r2 = RosterStore::default();\n 1384|        r2.merge_entry(member(\"home\", \"unrelated\"));\n 1385|        let other = spt_net::net::pairing::wire::NodeIntro {\n 1386|            label: \"ELSEWHERE\".into(),\n...\n 1412|        );\n*1413|        let mut r3 = RosterStore::default();\n 1414|        r3.merge_entry(member(\"home\", \"old2\"));\n 1415|        let no_id = spt_net::net::pairing::wire::NodeIntro {\n 1416|            label: \"BOX\".into(),"}],"details":{"scopePath":"crates/spt-daemon/src/registryhost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":12,"fileCount":1,"files":["crates/spt-daemon/src/registryhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/registryhost.rs","count":12}],"truncated":false,"displayContent":"   40│use spt_store::perch::{self, ParentHint};\n  *41│use spt_store::roster::RosterStore;\n   42│use spt_store::subnet::SubnetStore;\n   43│use spt_store::visibility::VisibilityStore;\n   44│\n     │...\n   58│/// without a restart; the access-gate stance shared with sync/notif).\n  *59│pub struct RegistryGatePolicy {\n   60│    /// Names of every subnet this node is a member of.\n   61│    pub subnets: Vec<String>,\n   62│    /// The membership directory (Mesh-D5, REQ-MESH-5): authorization is now\n     │...\n   65│    /// so this only ever runs against a peer that already proved seed knowledge.\n  *66│    pub roster: RosterStore,\n   67│}\n   68│\n  *69│impl RegistryGatePolicy {\n   70│    /// Load from the node's canonical stores.\n  *71│    pub fn load() -> RegistryGatePolicy {\n   72│        let subnets = SubnetStore::load()\n   73│            .subnets\n   74│            .iter()\n     │...\n   78│            subnets,\n  *79│            roster: RosterStore::load(),\n   80│        }\n   81│    }\n   82│\n     │...\n  969│/// The roster tombstone is the load-bearing eviction since Mesh-D5a: the\n *970│/// inbound gates authorize on [`RosterStore::is_member`], so a re-keyed identity\n  971│/// that kept its roster entry would stay a member. Tombstone (not a bare remove)\n  972│/// because the roster is a grow-set: a plain delete re-merges from any peer\n  973│/// that still lists the dead key. The D3 tombstone primitive dominates +\n     │...\n  979│pub fn repair_evict_superseded(\n *980│    roster: &mut spt_store::roster::RosterStore,\n  981│    subnet: &str,\n  982│    joiner_hex: &str,\n  983│    intro: &spt_net::net::pairing::wire::NodeIntro,\n     │...\n 1204│    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {\n*1205│        let mut roster = RosterStore::default();\n 1206│        roster.merge_entry(member(subnet, trusted));\n 1207│        RegistryGatePolicy {\n 1208│            subnets: vec![subnet.to_string()],\n     │...\n 1330│        // Roster membership in both subnets — the gate-relevant state (D5a).\n*1331│        let mut roster = RosterStore::default();\n 1332│        roster.merge_entry(member(\"home\", \"old-key\"));\n 1333│        roster.merge_entry(member(\"worknet\", \"old-key\"));\n 1334│        assert!(roster.is_member(\"home\", \"old-key\"), \"member before re-pair\");\n     │...\n 1382│        // No-match intro: nothing evicts, no marker, no notice.\n*1383│        let mut r2 = RosterStore::default();\n 1384│        r2.merge_entry(member(\"home\", \"unrelated\"));\n 1385│        let other = spt_net::net::pairing::wire::NodeIntro {\n 1386│            label: \"ELSEWHERE\".into(),\n     │...\n 1412│        );\n*1413│        let mut r3 = RosterStore::default();\n 1414│        r3.merge_entry(member(\"home\", \"old2\"));\n 1415│        let no_id = spt_net::net::pairing::wire::NodeIntro {\n 1416│            label: \"BOX\".into(),"},"isError":false,"timestamp":1784271743706}}
{"type":"message","id":"365c2b1c","parentId":"e3a55158","timestamp":"2026-07-17T07:02:23.710Z","message":{"role":"toolResult","toolCallId":"call_g3jWU2T4vXCUyS7KRc6RspRf|fc_0e15f89f5b7b4062016a59d37f09e881949909abeeba29af9f","toolName":"read","content":[{"type":"text","text":"//! Atomic file writes + serialized-path normalization.\n//!\n//! Two KNOWN-HAZARDS invariants live here:\n//!\n//! - **5.2 Windows EBUSY on atomic rename** (REQ-HAZARD-EBUSY-RENAME): a\n//!   `fs::rename` can fail transiently while another process (AV scanner,\n//!   indexer, a just-closed handle) still holds the target. The sister's\n//!   `atomic_write_string` was a bare tmp-write + rename; spt-core adds\n//!   retry-with-backoff that tolerates transient sharing/access errors and\n//!   only surfaces a persistent failure.\n//! - **5.4 Windows UNC prefix in serialized paths** (REQ-HAZARD-UNC-PATH-STRIP):\n//!   a canonicalized `\\\\?\\C:\\…` path serializes to `//?/C:/…` and then fails\n//!   `read_to_string` on the consumer side. [`to_forward_slash`] strips the\n//!   `\\\\?\\` prefix after the backslash→slash conversion so serialized path\n//!   attributes are directly consumable. Copied verbatim from sister\n//!   `owlery.rs:377-384`.\n//!\n//! The write pattern itself (sibling `{name}.tmp` + rename) is verbatim; same-\n//! volume rename is atomic on both NTFS and POSIX, so a concurrent reader never\n//! observes a half-written file.\n\nuse std::fs;\nuse std::io;\nuse std::io::Write;\nuse std::path::Path;\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::time::Duration;\n\n/// Maximum rename attempts before surfacing the error (KNOWN-HAZARDS 5.2).\nconst MAX_RENAME_ATTEMPTS: u32 = 10;\n\n/// Process-local counter making each write's tmp sibling unique (KNOWN-HAZARDS\n/// 5.15). NO clock / NO rand — a monotonic per-process seq plus the pid is enough\n/// to distinguish every concurrent writer to the same target, and stays\n/// resume/replay-safe.\nstatic TMP_SEQ: AtomicU64 = AtomicU64::new(0);\n\n/// Convert a path to a forward-slash string for JSON / cross-platform output,\n/// stripping the Windows UNC `\\\\?\\` prefix (which becomes `//?/` after the\n/// slash conversion). Serialized path attributes must be directly consumable\n/// by a `read_to_string` on the other side. Copied verbatim (sister\n/// `owlery::to_forward_slash`).\n// [impl->REQ-HAZARD-UNC-PATH-STRIP]\npub fn to_forward_slash(path: &Path) -> String {\n    let mut s = path.to_string_lossy().replace('\\\\', \"/\");\n    // `\\\\?\\C:\\...` -> `//?/C:/...` -> `C:/...`\n    if let Some(stripped) = s.strip_prefix(\"//?/\") {\n        s = stripped.to_string();\n    }\n    s\n}\n\n/// Atomically write `body` to `path` via a sibling `{name}.tmp` + rename, with\n/// EBUSY retry/backoff (KNOWN-HAZARDS 5.2). A concurrent reader sees either the\n/// old file or the complete new file — never a torn write. **Non-durable** — see\n/// [`atomic_write_bytes`] for the durability trade-off.\n// [impl->REQ-HAZARD-EBUSY-RENAME]\npub fn atomic_write_string(path: &Path, body: &str) -> io::Result<()> {\n    atomic_write_bytes(path, body.as_bytes())\n}\n\n/// Byte-oriented sibling of [`atomic_write_string`] — for binary payloads\n/// (blobs, signature material) that are not valid UTF-8.\n///\n/// **Non-durable** (no fsync): a plain `fs::write` of the tmp + rename. Still\n/// reader-atomic (old-or-new, never torn), but a hard reset between the rename\n/// and the data flush can resurrect the file at full length filled with NUL. That\n/// is the correct default for RECONSTRUCTIBLE state — registry snapshots, epoch\n/// counters, peer caches, the release cache — where a post-crash NUL husk simply\n/// parse-fails to *absent* and is regenerated / re-gossiped (and the 5.14\n/// read-side makes a corrupt record harmless at every seam). For AUTHORITATIVE\n/// records whose loss IS an incident, use [`atomic_write_bytes_durable`].\n// [impl->REQ-HAZARD-EBUSY-RENAME]\npub fn atomic_write_bytes(path: &Path, body: &[u8]) -> io::Result<()> {\n    write_then_rename(path, body, false)\n}\n\n/// **Durable** sibling of [`atomic_write_bytes`]: the tmp file is fsync'd\n/// (`sync_all`) BEFORE the rename publishes the name (KNOWN-HAZARDS 5.13), so a\n/// hard reset yields the complete old OR new file — never a full-length NUL\n/// zero-fill (NTFS keeps the rename metadata but loses un-synced data blocks).\n///\n/// **Scoped, NOT a blanket default.** Durability is opt-in for the few records\n/// whose loss is unrecoverable — the authoritative per-perch record\n/// ([`crate::info::write_info`]) and one-shot identity material\n/// ([`crate::nodeid`], `spt_daemon::machineid`). Making *every* atomic write\n/// durable serializes an fsync behind each of ~26 callers — and for `info.json`\n/// the flush lands under the per-perch `.info.lock` (the W1b lock-across-fsync\n/// wedge shape) — which stalls daemon bringup 4–6× and misses the endpoint\n/// ONLINE window (caught by `attach_wedge_e2e`). Scope durability to the record,\n/// not the writer.\n// [impl->REQ-HAZARD-EBUSY-RENAME]\n// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\npub fn atomic_write_bytes_durable(path: &Path, body: &[u8]) -> io::Result<()> {\n    write_then_rename(path, body, true)\n}\n\n/// String sibling of [`atomic_write_bytes_durable`] (durable tmp+rename).\n// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\npub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\n    atomic_write_bytes_durable(path, body.as_bytes())\n}\n\n/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\n/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\n/// reconstructible-state default, [`atomic_write_bytes`]).\n///\n/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\n/// 5.15). A fixed `{name}.tmp` shared by every writer is a latent collision: two\n/// processes writing the same target concurrently (e.g. the daemon's `mutate_info`\n/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\n/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\n/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\n/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\n/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\n// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\nfn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\n    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\n…\n}\n\n[Showing lines 1-118 of 379. Use :119 to continue]"}],"details":{"truncation":{"content":"//! Atomic file writes + serialized-path normalization.\r\n//!\r\n//! Two KNOWN-HAZARDS invariants live here:\r\n//!\r\n//! - **5.2 Windows EBUSY on atomic rename** (REQ-HAZARD-EBUSY-RENAME): a\r\n//!   `fs::rename` can fail transiently while another process (AV scanner,\r\n//!   indexer, a just-closed handle) still holds the target. The sister's\r\n//!   `atomic_write_string` was a bare tmp-write + rename; spt-core adds\r\n//!   retry-with-backoff that tolerates transient sharing/access errors and\r\n//!   only surfaces a persistent failure.\r\n//! - **5.4 Windows UNC prefix in serialized paths** (REQ-HAZARD-UNC-PATH-STRIP):\r\n//!   a canonicalized `\\\\?\\C:\\…` path serializes to `//?/C:/…` and then fails\r\n//!   `read_to_string` on the consumer side. [`to_forward_slash`] strips the\r\n//!   `\\\\?\\` prefix after the backslash→slash conversion so serialized path\r\n//!   attributes are directly consumable. Copied verbatim from sister\r\n//!   `owlery.rs:377-384`.\r\n//!\r\n//! The write pattern itself (sibling `{name}.tmp` + rename) is verbatim; same-\r\n//! volume rename is atomic on both NTFS and POSIX, so a concurrent reader never\r\n//! observes a half-written file.\r\n\r\nuse std::fs;\r\nuse std::io;\r\nuse std::io::Write;\r\nuse std::path::Path;\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\nuse std::time::Duration;\r\n\r\n/// Maximum rename attempts before surfacing the error (KNOWN-HAZARDS 5.2).\r\nconst MAX_RENAME_ATTEMPTS: u32 = 10;\r\n\r\n/// Process-local counter making each write's tmp sibling unique (KNOWN-HAZARDS\r\n/// 5.15). NO clock / NO rand — a monotonic per-process seq plus the pid is enough\r\n/// to distinguish every concurrent writer to the same target, and stays\r\n/// resume/replay-safe.\r\nstatic TMP_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// Convert a path to a forward-slash string for JSON / cross-platform output,\r\n/// stripping the Windows UNC `\\\\?\\` prefix (which becomes `//?/` after the\r\n/// slash conversion). Serialized path attributes must be directly consumable\r\n/// by a `read_to_string` on the other side. Copied verbatim (sister\r\n/// `owlery::to_forward_slash`).\r\n// [impl->REQ-HAZARD-UNC-PATH-STRIP]\r\npub fn to_forward_slash(path: &Path) -> String {\r\n    let mut s = path.to_string_lossy().replace('\\\\', \"/\");\r\n    // `\\\\?\\C:\\...` -> `//?/C:/...` -> `C:/...`\r\n    if let Some(stripped) = s.strip_prefix(\"//?/\") {\r\n        s = stripped.to_string();\r\n    }\r\n    s\r\n}\r\n\r\n/// Atomically write `body` to `path` via a sibling `{name}.tmp` + rename, with\r\n/// EBUSY retry/backoff (KNOWN-HAZARDS 5.2). A concurrent reader sees either the\r\n/// old file or the complete new file — never a torn write. **Non-durable** — see\r\n/// [`atomic_write_bytes`] for the durability trade-off.\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\npub fn atomic_write_string(path: &Path, body: &str) -> io::Result<()> {\r\n    atomic_write_bytes(path, body.as_bytes())\r\n}\r\n\r\n/// Byte-oriented sibling of [`atomic_write_string`] — for binary payloads\r\n/// (blobs, signature material) that are not valid UTF-8.\r\n///\r\n/// **Non-durable** (no fsync): a plain `fs::write` of the tmp + rename. Still\r\n/// reader-atomic (old-or-new, never torn), but a hard reset between the rename\r\n/// and the data flush can resurrect the file at full length filled with NUL. That\r\n/// is the correct default for RECONSTRUCTIBLE state — registry snapshots, epoch\r\n/// counters, peer caches, the release cache — where a post-crash NUL husk simply\r\n/// parse-fails to *absent* and is regenerated / re-gossiped (and the 5.14\r\n/// read-side makes a corrupt record harmless at every seam). For AUTHORITATIVE\r\n/// records whose loss IS an incident, use [`atomic_write_bytes_durable`].\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\npub fn atomic_write_bytes(path: &Path, body: &[u8]) -> io::Result<()> {\r\n    write_then_rename(path, body, false)\r\n}\r\n\r\n/// **Durable** sibling of [`atomic_write_bytes`]: the tmp file is fsync'd\r\n/// (`sync_all`) BEFORE the rename publishes the name (KNOWN-HAZARDS 5.13), so a\r\n/// hard reset yields the complete old OR new file — never a full-length NUL\r\n/// zero-fill (NTFS keeps the rename metadata but loses un-synced data blocks).\r\n///\r\n/// **Scoped, NOT a blanket default.** Durability is opt-in for the few records\r\n/// whose loss is unrecoverable — the authoritative per-perch record\r\n/// ([`crate::info::write_info`]) and one-shot identity material\r\n/// ([`crate::nodeid`], `spt_daemon::machineid`). Making *every* atomic write\r\n/// durable serializes an fsync behind each of ~26 callers — and for `info.json`\r\n/// the flush lands under the per-perch `.info.lock` (the W1b lock-across-fsync\r\n/// wedge shape) — which stalls daemon bringup 4–6× and misses the endpoint\r\n/// ONLINE window (caught by `attach_wedge_e2e`). Scope durability to the record,\r\n/// not the writer.\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\n// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\r\npub fn atomic_write_bytes_durable(path: &Path, body: &[u8]) -> io::Result<()> {\r\n    write_then_rename(path, body, true)\r\n}\r\n\r\n/// String sibling of [`atomic_write_bytes_durable`] (durable tmp+rename).\r\n// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\r\npub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\r\n    atomic_write_bytes_durable(path, body.as_bytes())\r\n}\r\n\r\n/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\r\n/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\r\n/// reconstructible-state default, [`atomic_write_bytes`]).\r\n///\r\n/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\r\n/// 5.15). A fixed `{name}.tmp` shared by every writer is a latent collision: two\r\n/// processes writing the same target concurrently (e.g. the daemon's `mutate_info`\r\n/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\r\n/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\r\n/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\r\n/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\r\n/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\r\n// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\r\nfn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\r\n    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\r","truncated":true,"truncatedBy":"lines","totalLines":379,"totalBytes":6332,"outputLines":118,"outputBytes":6332,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Atomic file writes + serialized-path normalization.\r\n//!\r\n//! Two KNOWN-HAZARDS invariants live here:\r\n//!\r\n//! - **5.2 Windows EBUSY on atomic rename** (REQ-HAZARD-EBUSY-RENAME): a\r\n//!   `fs::rename` can fail transiently while another process (AV scanner,\r\n//!   indexer, a just-closed handle) still holds the target. The sister's\r\n//!   `atomic_write_string` was a bare tmp-write + rename; spt-core adds\r\n//!   retry-with-backoff that tolerates transient sharing/access errors and\r\n//!   only surfaces a persistent failure.\r\n//! - **5.4 Windows UNC prefix in serialized paths** (REQ-HAZARD-UNC-PATH-STRIP):\r\n//!   a canonicalized `\\\\?\\C:\\…` path serializes to `//?/C:/…` and then fails\r\n//!   `read_to_string` on the consumer side. [`to_forward_slash`] strips the\r\n//!   `\\\\?\\` prefix after the backslash→slash conversion so serialized path\r\n//!   attributes are directly consumable. Copied verbatim from sister\r\n//!   `owlery.rs:377-384`.\r\n//!\r\n//! The write pattern itself (sibling `{name}.tmp` + rename) is verbatim; same-\r\n//! volume rename is atomic on both NTFS and POSIX, so a concurrent reader never\r\n//! observes a half-written file.\r\n\r\nuse std::fs;\r\nuse std::io;\r\nuse std::io::Write;\r\nuse std::path::Path;\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\nuse std::time::Duration;\r\n\r\n/// Maximum rename attempts before surfacing the error (KNOWN-HAZARDS 5.2).\r\nconst MAX_RENAME_ATTEMPTS: u32 = 10;\r\n\r\n/// Process-local counter making each write's tmp sibling unique (KNOWN-HAZARDS\r\n/// 5.15). NO clock / NO rand — a monotonic per-process seq plus the pid is enough\r\n/// to distinguish every concurrent writer to the same target, and stays\r\n/// resume/replay-safe.\r\nstatic TMP_SEQ: AtomicU64 = AtomicU64::new(0);\r\n\r\n/// Convert a path to a forward-slash string for JSON / cross-platform output,\r\n/// stripping the Windows UNC `\\\\?\\` prefix (which becomes `//?/` after the\r\n/// slash conversion). Serialized path attributes must be directly consumable\r\n/// by a `read_to_string` on the other side. Copied verbatim (sister\r\n/// `owlery::to_forward_slash`).\r\n// [impl->REQ-HAZARD-UNC-PATH-STRIP]\r\npub fn to_forward_slash(path: &Path) -> String {\r\n    let mut s = path.to_string_lossy().replace('\\\\', \"/\");\r\n    // `\\\\?\\C:\\...` -> `//?/C:/...` -> `C:/...`\r\n    if let Some(stripped) = s.strip_prefix(\"//?/\") {\r\n        s = stripped.to_string();\r\n    }\r\n    s\r\n}\r\n\r\n/// Atomically write `body` to `path` via a sibling `{name}.tmp` + rename, with\r\n/// EBUSY retry/backoff (KNOWN-HAZARDS 5.2). A concurrent reader sees either the\r\n/// old file or the complete new file — never a torn write. **Non-durable** — see\r\n/// [`atomic_write_bytes`] for the durability trade-off.\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\npub fn atomic_write_string(path: &Path, body: &str) -> io::Result<()> {\r\n    atomic_write_bytes(path, body.as_bytes())\r\n}\r\n\r\n/// Byte-oriented sibling of [`atomic_write_string`] — for binary payloads\r\n/// (blobs, signature material) that are not valid UTF-8.\r\n///\r\n/// **Non-durable** (no fsync): a plain `fs::write` of the tmp + rename. Still\r\n/// reader-atomic (old-or-new, never torn), but a hard reset between the rename\r\n/// and the data flush can resurrect the file at full length filled with NUL. That\r\n/// is the correct default for RECONSTRUCTIBLE state — registry snapshots, epoch\r\n/// counters, peer caches, the release cache — where a post-crash NUL husk simply\r\n/// parse-fails to *absent* and is regenerated / re-gossiped (and the 5.14\r\n/// read-side makes a corrupt record harmless at every seam). For AUTHORITATIVE\r\n/// records whose loss IS an incident, use [`atomic_write_bytes_durable`].\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\npub fn atomic_write_bytes(path: &Path, body: &[u8]) -> io::Result<()> {\r\n    write_then_rename(path, body, false)\r\n}\r\n\r\n/// **Durable** sibling of [`atomic_write_bytes`]: the tmp file is fsync'd\r\n/// (`sync_all`) BEFORE the rename publishes the name (KNOWN-HAZARDS 5.13), so a\r\n/// hard reset yields the complete old OR new file — never a full-length NUL\r\n/// zero-fill (NTFS keeps the rename metadata but loses un-synced data blocks).\r\n///\r\n/// **Scoped, NOT a blanket default.** Durability is opt-in for the few records\r\n/// whose loss is unrecoverable — the authoritative per-perch record\r\n/// ([`crate::info::write_info`]) and one-shot identity material\r\n/// ([`crate::nodeid`], `spt_daemon::machineid`). Making *every* atomic write\r\n/// durable serializes an fsync behind each of ~26 callers — and for `info.json`\r\n/// the flush lands under the per-perch `.info.lock` (the W1b lock-across-fsync\r\n/// wedge shape) — which stalls daemon bringup 4–6× and misses the endpoint\r\n/// ONLINE window (caught by `attach_wedge_e2e`). Scope durability to the record,\r\n/// not the writer.\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\n// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\r\npub fn atomic_write_bytes_durable(path: &Path, body: &[u8]) -> io::Result<()> {\r\n    write_then_rename(path, body, true)\r\n}\r\n\r\n/// String sibling of [`atomic_write_bytes_durable`] (durable tmp+rename).\r\n// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\r\npub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\r\n    atomic_write_bytes_durable(path, body.as_bytes())\r\n}\r\n\r\n/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\r\n/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\r\n/// reconstructible-state default, [`atomic_write_bytes`]).\r\n///\r\n/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\r\n/// 5.15). A fixed `{name}.tmp` shared by every writer is a latent collision: two\r\n/// processes writing the same target concurrently (e.g. the daemon's `mutate_info`\r\n/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\r\n/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\r\n/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\r\n/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\r\n/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\r\n// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\r\nfn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\r\n    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\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,null,140]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\atomic.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":379,"totalBytes":6332,"outputLines":118,"outputBytes":6332,"shownRange":{"start":1,"end":118},"nextOffset":119}}},"isError":false,"timestamp":1784271743710}}
{"type":"message","id":"d952484a","parentId":"365c2b1c","timestamp":"2026-07-17T07:02:23.726Z","message":{"role":"toolResult","toolCallId":"call_fx8uo1OCUA7y7sGMWWbdCpEe|fc_0e15f89f5b7b4062016a59d37f086081949ef01cdc7e5bfef6","toolName":"read","content":[{"type":"text","text":"/// capped-backoff cadence; either this budget OR an alive-but-never-ready\n/// window-expiry triggers rollback.\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\nconst TICK: Duration = Duration::from_millis(200);\n\n/// How long the brain child retries connecting to the broker socket before\n/// giving up (the broker may still be binding — the child is freshly spawned).\nconst BRAIN_CONNECT_TRIES: u32 = 300;\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\n\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\n/// reliable than a brain→brain frame, which retires in D4).\n///\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\n/// never a parse failure that would brick the brain before it rehydrates.\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n\n[Showing lines 53-73 of 2116. Use :74 to continue]"}],"details":{"truncation":{"content":"/// capped-backoff cadence; either this budget OR an alive-but-never-ready\r\n/// window-expiry triggers rollback.\r\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\r\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\r\nconst TICK: Duration = Duration::from_millis(200);\r\n\r\n/// How long the brain child retries connecting to the broker socket before\r\n/// giving up (the broker may still be binding — the child is freshly spawned).\r\nconst BRAIN_CONNECT_TRIES: u32 = 300;\r\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\r\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\r\n\r\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\r\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\r\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\r\n/// reliable than a brain→brain frame, which retires in D4).\r\n///\r\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\r\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\r\n/// never a parse failure that would brick the brain before it rehydrates.\r\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r","truncated":true,"truncatedBy":"lines","totalLines":2064,"totalBytes":1226,"outputLines":21,"outputBytes":1226,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// capped-backoff cadence; either this budget OR an alive-but-never-ready\r\n/// window-expiry triggers rollback.\r\nconst TRIAL_PREREADY_BUDGET: u32 = 3;\r\n/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\r\nconst TICK: Duration = Duration::from_millis(200);\r\n\r\n/// How long the brain child retries connecting to the broker socket before\r\n/// giving up (the broker may still be binding — the child is freshly spawned).\r\nconst BRAIN_CONNECT_TRIES: u32 = 300;\r\n/// Cadence the idle brain refreshes its ready breadcrumb + probes the broker.\r\nconst BRAIN_HEARTBEAT: Duration = Duration::from_millis(500);\r\n\r\n/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the\r\n/// broker stamps at spawn time (ADR-0018 V2). One channel, three payloads: the\r\n/// broker *observes* every respawn, so it alone is authoritative (strictly more\r\n/// reliable than a brain→brain frame, which retires in D4).\r\n///\r\n/// Parsing is **lenient** (KH-2.3 forward-compat): an unrecognized value — a\r\n/// reason a *future* broker adds that this brain predates — defaults to `Cold`,\r\n/// never a parse failure that would brick the brain before it rehydrates.\r\n// [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r","startLine":53,"lineNumbers":[53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brainproc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2116,"totalBytes":1226,"outputLines":21,"outputBytes":1226,"shownRange":{"start":53,"end":73},"nextOffset":74}}},"isError":false,"timestamp":1784271743726}}
{"type":"message","id":"07c0c03a","parentId":"d952484a","timestamp":"2026-07-17T07:02:23.757Z","message":{"role":"toolResult","toolCallId":"call_DvJsn2GdS7acbBdzRfqMU2OL|fc_0e15f89f5b7b4062016a59d37f09f88194a3efd84e1b422a23","toolName":"read","content":[{"type":"text","text":"use spt_store::liveness::{is_perch_alive, is_perch_unbound};\nuse spt_store::perch::{self, ParentHint};\nuse spt_store::roster::RosterStore;\nuse spt_store::subnet::SubnetStore;\nuse spt_store::visibility::VisibilityStore;\n\n/// What the host did with one inbound registry record — loud + typed.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum RegistryApplyVerdict {\n    /// Admitted and merged with the stated outcome (the lease may still have\n    /// dropped it as [`MergeOutcome::Stale`] — that IS a merge decision).\n    Merged(MergeOutcome),\n    /// Refused: this node is not a member of the record's subnet.\n    NotMember,\n    /// Refused: the origin is not trusted in the record's subnet.\n    UntrustedOrigin,\n}\n\n/// The inbound gate's config — reloaded per feed (config edits take effect\n/// without a restart; the access-gate stance shared with sync/notif).\npub struct RegistryGatePolicy {\n    /// Names of every subnet this node is a member of.\n    pub subnets: Vec<String>,\n    /// The membership directory (Mesh-D5, REQ-MESH-5): authorization is now\n    /// live roster membership (roster-listed ∧ ¬tombstoned), not a stored\n    /// pairwise pin. A non-member can't even connect (D2 seed-proof drops it),\n    /// so this only ever runs against a peer that already proved seed knowledge.\n    pub roster: RosterStore,\n}\n\nimpl RegistryGatePolicy {\n    /// Load from the node's canonical stores.\n    pub fn load() -> RegistryGatePolicy {\n        let subnets = SubnetStore::load()\n            .subnets\n            .iter()\n            .map(|s| s.name.clone())\n            .collect();\n        RegistryGatePolicy {\n            subnets,\n            roster: RosterStore::load(),\n        }\n    }\n\n    /// member subnet ∧ origin is a roster member of that subnet — fail-closed on\n    /// both.\n    // [impl->REQ-MESH-5]\n    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {\n        self.subnets.iter().any(|s| s == subnet) && self.roster.is_member(subnet, origin_node)\n    }\n}\n\n/// The daemon's per-subnet registry instances + this node's advertisement\n/// epoch source. Shared (`Arc`) between the dispatcher's registry arm and\n/// the peer pump.\npub struct RegistryHost {\n    /// This node's stable name (lowercase-hex pubkey).\n    node_hex: String,\n    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,\n    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every\n    /// advertised row — a hostname change reaches peers as ordinary\n    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the\n    /// identity.\n    node_label: Option<String>,\n    /// This node's hashed stable machine identifier (M8 decision 13/22,\n    /// REQ-SUBNET-7): stamped onto every advertised row beside the label —\n    /// the re-pair overwrite anchor. Already domain-separated-hashed by\n    /// [`crate::machineid`]; never the raw id.\n    machine_id: Option<String>,\n    /// When each peer node was last *heard* (an admitted inbound feed,\n    /// wall-clock ms) — the gossip-recency half of the M7 D2 hybrid\n    /// liveness (REQ-SUBNET-1): a recently-heard node renders online without\n    /// a probe; only stale rows get dialed. Local-only (never on the wire);\n    /// mirrored to `heard.meta` beside the snapshots for the CLI reader.\n    heard: Mutex<BTreeMap<String, u64>>,\n…\n}\n\n[Showing lines 39-113 of 2238. Use :114 to continue]"}],"details":{"truncation":{"content":"use spt_store::liveness::{is_perch_alive, is_perch_unbound};\r\nuse spt_store::perch::{self, ParentHint};\r\nuse spt_store::roster::RosterStore;\r\nuse spt_store::subnet::SubnetStore;\r\nuse spt_store::visibility::VisibilityStore;\r\n\r\n/// What the host did with one inbound registry record — loud + typed.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub enum RegistryApplyVerdict {\r\n    /// Admitted and merged with the stated outcome (the lease may still have\r\n    /// dropped it as [`MergeOutcome::Stale`] — that IS a merge decision).\r\n    Merged(MergeOutcome),\r\n    /// Refused: this node is not a member of the record's subnet.\r\n    NotMember,\r\n    /// Refused: the origin is not trusted in the record's subnet.\r\n    UntrustedOrigin,\r\n}\r\n\r\n/// The inbound gate's config — reloaded per feed (config edits take effect\r\n/// without a restart; the access-gate stance shared with sync/notif).\r\npub struct RegistryGatePolicy {\r\n    /// Names of every subnet this node is a member of.\r\n    pub subnets: Vec<String>,\r\n    /// The membership directory (Mesh-D5, REQ-MESH-5): authorization is now\r\n    /// live roster membership (roster-listed ∧ ¬tombstoned), not a stored\r\n    /// pairwise pin. A non-member can't even connect (D2 seed-proof drops it),\r\n    /// so this only ever runs against a peer that already proved seed knowledge.\r\n    pub roster: RosterStore,\r\n}\r\n\r\nimpl RegistryGatePolicy {\r\n    /// Load from the node's canonical stores.\r\n    pub fn load() -> RegistryGatePolicy {\r\n        let subnets = SubnetStore::load()\r\n            .subnets\r\n            .iter()\r\n            .map(|s| s.name.clone())\r\n            .collect();\r\n        RegistryGatePolicy {\r\n            subnets,\r\n            roster: RosterStore::load(),\r\n        }\r\n    }\r\n\r\n    /// member subnet ∧ origin is a roster member of that subnet — fail-closed on\r\n    /// both.\r\n    // [impl->REQ-MESH-5]\r\n    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {\r\n        self.subnets.iter().any(|s| s == subnet) && self.roster.is_member(subnet, origin_node)\r\n    }\r\n}\r\n\r\n/// The daemon's per-subnet registry instances + this node's advertisement\r\n/// epoch source. Shared (`Arc`) between the dispatcher's registry arm and\r\n/// the peer pump.\r\npub struct RegistryHost {\r\n    /// This node's stable name (lowercase-hex pubkey).\r\n    node_hex: String,\r\n    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,\r\n    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every\r\n    /// advertised row — a hostname change reaches peers as ordinary\r\n    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the\r\n    /// identity.\r\n    node_label: Option<String>,\r\n    /// This node's hashed stable machine identifier (M8 decision 13/22,\r\n    /// REQ-SUBNET-7): stamped onto every advertised row beside the label —\r\n    /// the re-pair overwrite anchor. Already domain-separated-hashed by\r\n    /// [`crate::machineid`]; never the raw id.\r\n    machine_id: Option<String>,\r\n    /// When each peer node was last *heard* (an admitted inbound feed,\r\n    /// wall-clock ms) — the gossip-recency half of the M7 D2 hybrid\r\n    /// liveness (REQ-SUBNET-1): a recently-heard node renders online without\r\n    /// a probe; only stale rows get dialed. Local-only (never on the wire);\r\n    /// mirrored to `heard.meta` beside the snapshots for the CLI reader.\r\n    heard: Mutex<BTreeMap<String, u64>>,\r","truncated":true,"truncatedBy":"lines","totalLines":2200,"totalBytes":3411,"outputLines":75,"outputBytes":3411,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"use spt_store::liveness::{is_perch_alive, is_perch_unbound};\r\nuse spt_store::perch::{self, ParentHint};\r\nuse spt_store::roster::RosterStore;\r\nuse spt_store::subnet::SubnetStore;\r\nuse spt_store::visibility::VisibilityStore;\r\n\r\n/// What the host did with one inbound registry record — loud + typed.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub enum RegistryApplyVerdict {\r\n    /// Admitted and merged with the stated outcome (the lease may still have\r\n    /// dropped it as [`MergeOutcome::Stale`] — that IS a merge decision).\r\n    Merged(MergeOutcome),\r\n    /// Refused: this node is not a member of the record's subnet.\r\n    NotMember,\r\n    /// Refused: the origin is not trusted in the record's subnet.\r\n    UntrustedOrigin,\r\n}\r\n\r\n/// The inbound gate's config — reloaded per feed (config edits take effect\r\n/// without a restart; the access-gate stance shared with sync/notif).\r\npub struct RegistryGatePolicy {\r\n    /// Names of every subnet this node is a member of.\r\n    pub subnets: Vec<String>,\r\n    /// The membership directory (Mesh-D5, REQ-MESH-5): authorization is now\r\n    /// live roster membership (roster-listed ∧ ¬tombstoned), not a stored\r\n    /// pairwise pin. A non-member can't even connect (D2 seed-proof drops it),\r\n    /// so this only ever runs against a peer that already proved seed knowledge.\r\n    pub roster: RosterStore,\r\n}\r\n\r\nimpl RegistryGatePolicy {\r\n    /// Load from the node's canonical stores.\r\n    pub fn load() -> RegistryGatePolicy {\r\n        let subnets = SubnetStore::load()\r\n            .subnets\r\n            .iter()\r\n            .map(|s| s.name.clone())\r\n            .collect();\r\n        RegistryGatePolicy {\r\n            subnets,\r\n            roster: RosterStore::load(),\r\n        }\r\n    }\r\n\r\n    /// member subnet ∧ origin is a roster member of that subnet — fail-closed on\r\n    /// both.\r\n    // [impl->REQ-MESH-5]\r\n    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {\r\n        self.subnets.iter().any(|s| s == subnet) && self.roster.is_member(subnet, origin_node)\r\n    }\r\n}\r\n\r\n/// The daemon's per-subnet registry instances + this node's advertisement\r\n/// epoch source. Shared (`Arc`) between the dispatcher's registry arm and\r\n/// the peer pump.\r\npub struct RegistryHost {\r\n    /// This node's stable name (lowercase-hex pubkey).\r\n    node_hex: String,\r\n    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,\r\n    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every\r\n    /// advertised row — a hostname change reaches peers as ordinary\r\n    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the\r\n    /// identity.\r\n    node_label: Option<String>,\r\n    /// This node's hashed stable machine identifier (M8 decision 13/22,\r\n    /// REQ-SUBNET-7): stamped onto every advertised row beside the label —\r\n    /// the re-pair overwrite anchor. Already domain-separated-hashed by\r\n    /// [`crate::machineid`]; never the raw id.\r\n    machine_id: Option<String>,\r\n    /// When each peer node was last *heard* (an admitted inbound feed,\r\n    /// wall-clock ms) — the gossip-recency half of the M7 D2 hybrid\r\n    /// liveness (REQ-SUBNET-1): a recently-heard node renders online without\r\n    /// a probe; only stale rows get dialed. Local-only (never on the wire);\r\n    /// mirrored to `heard.meta` beside the snapshots for the CLI reader.\r\n    heard: Mutex<BTreeMap<String, u64>>,\r\n…\n}","startLine":39,"lineNumbers":[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,125]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2238,"totalBytes":3411,"outputLines":75,"outputBytes":3411,"shownRange":{"start":39,"end":113},"nextOffset":114}}},"isError":false,"timestamp":1784271743757}}
{"type":"message","id":"0b755cf4","parentId":"07c0c03a","timestamp":"2026-07-17T07:02:23.757Z","message":{"role":"toolResult","toolCallId":"call_0M6U1CNpj7EllEqRxpGChsG6|fc_0e15f89f5b7b4062016a59d37f09d08194801d140796ca65c2","toolName":"read","content":[{"type":"text","text":"fn gossip_controller_node(\n…\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\n/// picker + `REQ-STORE-CONTEXT-BRANCH-FILL` exclusion), deduped newest-first, bounded.\n/// Project IDs ONLY — dirs never cross the wire (they leak usernames/dir structure).\n/// Degrade-to-empty on any IO error (best-effort gossip, never a crash).\n// [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\nfn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\n    let entries = spt_store::sessions::read_all(perch_path);\n    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\n    let mut out: Vec<String> = Vec::new();\n    for cwd in entries.iter().rev().filter_map(|e| e.cwd.as_deref()) {\n        let dir = Path::new(cwd);\n        if crate::lifecycle::is_owlery_internal(dir, owlery) {\n            continue;\n        }\n        let pid = spt_store::project::project_id_for_dir(dir);\n        if pid.is_empty() {\n            continue;\n        }\n        if seen.insert(pid.clone()) {\n            out.push(pid);\n            if out.len() >= MAX_GOSSIPED_PROJECTS {\n                break;\n            }\n        }\n    }\n    out\n}\n\n/// The superseded identities a completed ceremony's intro reveals (pure,\n/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node\n/// label AND machine id under a DIFFERENT node key than the re-paired\n/// joiner. Both must match — a label alone re-pairs nothing (hostnames\n/// collide), a machine id alone is never gossiped without its label.\n// [impl->REQ-SUBNET-7]\npub fn superseded_identities(\n    reg: &SubnetRegistry,\n    intro: &spt_net::net::pairing::wire::NodeIntro,\n    joiner_hex: &str,\n) -> Vec<String> {\n    let mut out = Vec::new();\n    for id in reg.endpoint_ids() {\n        for inst in reg.instances(id) {\n            if inst.node != joiner_hex\n                && inst.node_label.as_deref() == Some(intro.label.as_str())\n                && inst.machine_id.as_deref() == Some(intro.machine_id.as_str())\n                && !out.contains(&inst.node)\n…\n            }\n        }\n    }\n…\n}\n\n[Showing lines 874-933 of 2238. Use :934 to continue]"}],"details":{"truncation":{"content":"    match driven_by {\r\n        Some(remote) => Some(remote.to_string()),\r\n        None if controlled => Some(self_node.to_string()),\r\n        None => None,\r\n    }\r\n}\r\n\r\n/// Bound on the recent-project IDs a single instance gossips — newest-first, so the\r\n/// cap keeps the freshest projects. Keeps the wire compact (a long-lived endpoint can\r\n/// accumulate many cwds); the picker's project pane never needs more.\r\nconst MAX_GOSSIPED_PROJECTS: usize = 8;\r\n\r\n/// The endpoint's recent project IDs newest→oldest for gossip (#4,\r\n/// REQ-GOSSIP-ADAPTER-PROJECTS): the session ledger's cwds mapped to project IDs,\r\n/// owlery-internal cwds excluded (a psyche-host session is not a project — mirrors the\r\n/// picker + `REQ-STORE-CONTEXT-BRANCH-FILL` exclusion), deduped newest-first, bounded.\r\n/// Project IDs ONLY — dirs never cross the wire (they leak usernames/dir structure).\r\n/// Degrade-to-empty on any IO error (best-effort gossip, never a crash).\r\n// [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\nfn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\r\n    let entries = spt_store::sessions::read_all(perch_path);\r\n    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\r\n    let mut out: Vec<String> = Vec::new();\r\n    for cwd in entries.iter().rev().filter_map(|e| e.cwd.as_deref()) {\r\n        let dir = Path::new(cwd);\r\n        if crate::lifecycle::is_owlery_internal(dir, owlery) {\r\n            continue;\r\n        }\r\n        let pid = spt_store::project::project_id_for_dir(dir);\r\n        if pid.is_empty() {\r\n            continue;\r\n        }\r\n        if seen.insert(pid.clone()) {\r\n            out.push(pid);\r\n            if out.len() >= MAX_GOSSIPED_PROJECTS {\r\n                break;\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n/// The superseded identities a completed ceremony's intro reveals (pure,\r\n/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node\r\n/// label AND machine id under a DIFFERENT node key than the re-paired\r\n/// joiner. Both must match — a label alone re-pairs nothing (hostnames\r\n/// collide), a machine id alone is never gossiped without its label.\r\n// [impl->REQ-SUBNET-7]\r\npub fn superseded_identities(\r\n    reg: &SubnetRegistry,\r\n    intro: &spt_net::net::pairing::wire::NodeIntro,\r\n    joiner_hex: &str,\r\n) -> Vec<String> {\r\n    let mut out = Vec::new();\r\n    for id in reg.endpoint_ids() {\r\n        for inst in reg.instances(id) {\r\n            if inst.node != joiner_hex\r\n                && inst.node_label.as_deref() == Some(intro.label.as_str())\r\n                && inst.machine_id.as_deref() == Some(intro.machine_id.as_str())\r\n                && !out.contains(&inst.node)\r","truncated":true,"truncatedBy":"lines","totalLines":1365,"totalBytes":2693,"outputLines":60,"outputBytes":2693,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn gossip_controller_node(\n…\n) -> Option<String> {\n    match driven_by {\r\n        Some(remote) => Some(remote.to_string()),\r\n        None if controlled => Some(self_node.to_string()),\r\n        None => None,\r\n    }\r\n}\r\n\r\n/// Bound on the recent-project IDs a single instance gossips — newest-first, so the\r\n/// cap keeps the freshest projects. Keeps the wire compact (a long-lived endpoint can\r\n/// accumulate many cwds); the picker's project pane never needs more.\r\nconst MAX_GOSSIPED_PROJECTS: usize = 8;\r\n\r\n/// The endpoint's recent project IDs newest→oldest for gossip (#4,\r\n/// REQ-GOSSIP-ADAPTER-PROJECTS): the session ledger's cwds mapped to project IDs,\r\n/// owlery-internal cwds excluded (a psyche-host session is not a project — mirrors the\r\n/// picker + `REQ-STORE-CONTEXT-BRANCH-FILL` exclusion), deduped newest-first, bounded.\r\n/// Project IDs ONLY — dirs never cross the wire (they leak usernames/dir structure).\r\n/// Degrade-to-empty on any IO error (best-effort gossip, never a crash).\r\n// [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\nfn recent_projects_for(perch_path: &Path, owlery: &Path) -> Vec<String> {\r\n    let entries = spt_store::sessions::read_all(perch_path);\r\n    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();\r\n    let mut out: Vec<String> = Vec::new();\r\n    for cwd in entries.iter().rev().filter_map(|e| e.cwd.as_deref()) {\r\n        let dir = Path::new(cwd);\r\n        if crate::lifecycle::is_owlery_internal(dir, owlery) {\r\n            continue;\r\n        }\r\n        let pid = spt_store::project::project_id_for_dir(dir);\r\n        if pid.is_empty() {\r\n            continue;\r\n        }\r\n        if seen.insert(pid.clone()) {\r\n            out.push(pid);\r\n            if out.len() >= MAX_GOSSIPED_PROJECTS {\r\n                break;\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n/// The superseded identities a completed ceremony's intro reveals (pure,\r\n/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node\r\n/// label AND machine id under a DIFFERENT node key than the re-paired\r\n/// joiner. Both must match — a label alone re-pairs nothing (hostnames\r\n/// collide), a machine id alone is never gossiped without its label.\r\n// [impl->REQ-SUBNET-7]\r\npub fn superseded_identities(\r\n    reg: &SubnetRegistry,\r\n    intro: &spt_net::net::pairing::wire::NodeIntro,\r\n    joiner_hex: &str,\r\n) -> Vec<String> {\r\n    let mut out = Vec::new();\r\n    for id in reg.endpoint_ids() {\r\n        for inst in reg.instances(id) {\r\n            if inst.node != joiner_hex\r\n                && inst.node_label.as_deref() == Some(intro.label.as_str())\r\n                && inst.machine_id.as_deref() == Some(intro.machine_id.as_str())\r\n                && !out.contains(&inst.node)\r\n…\n            }\n        }\n    }\n…\n}","startLine":869,"lineNumbers":[869,null,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,null,936,937,938,null,940]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2238,"totalBytes":2693,"outputLines":60,"outputBytes":2693,"shownRange":{"start":874,"end":933},"nextOffset":934}}},"isError":false,"timestamp":1784271743757}}
{"type":"message","id":"68631f7a","parentId":"0b755cf4","timestamp":"2026-07-17T07:03:01.682Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning Windows file I/O probing methods**\n\n**Analyzing registry and I/O operation patterns**\n\n**Exploring HashMap duplicate prevention**"},{"type":"thinking","thinking":"**Analyzing broker-to-brain connection behavior**\n\n**Planning detailed Windows IO and CPU profiling**\n\n**Distinguishing registry stream poison patterns**"},{"type":"thinking","thinking":"**Inspecting connection lifecycle logs**"},{"type":"toolCall","id":"call_w2a2tNIgHzaMLJiaQYa70JaA|fc_0e15f89f5b7b4062016a59d3a4e16c8194993be4b3fb01975b","name":"grep","arguments":{"i":"Locating conn lifecycle format","pattern":"fn lifecycle\\(|CONN_|conn-lifecycle|event=","path":"crates/spt-daemon/src/conn.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_6x1jJS68LG452nxWPy6QHP1H|fc_0e15f89f5b7b4062016a59d3a4e18881949400212128c1a044","name":"read","arguments":{"i":"Reading conn lifecycle logs","path":"crates/spt-daemon/src/conn.rs","selector":"155-235"}},{"type":"toolCall","id":"call_Ls7Kw1rESvf7c7VozIhpMeA6|fc_0e15f89f5b7b4062016a59d3a4e19c8194bee965ab451c01ca","name":"read","arguments":{"i":"Reading registry snapshot writes","path":"crates/spt-daemon/src/registryhost.rs","selector":"730-770"}},{"type":"toolCall","id":"call_VtqOtdLDXkvh6YiXvOkeFk8s|fc_0e15f89f5b7b4062016a59d3a4e1a48194829de2d91ef25fd8","name":"read","arguments":{"i":"Reading dispatch retry semantics","path":"crates/spt-daemon/src/dispatch.rs","selector":"210-330"}},{"type":"toolCall","id":"call_Q759xD5yIyyTS1tCdRWUaZkv|fc_0e15f89f5b7b4062016a59d3a4e1ac8194a4378e81c3611a2a","name":"grep","arguments":{"i":"Finding dispatcher retirement","pattern":"net_stream_retire|retire_stream|KIND_NET_STREAM_RETIRE","path":"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8813,"output":1342,"cacheRead":114176,"cacheWrite":0,"totalTokens":124331,"reasoningTokens":1063,"cost":{"input":0.04406500000000001,"output":0.040260000000000004,"cacheRead":0.057088,"cacheWrite":0,"total":0.141413}},"stopReason":"toolUse","timestamp":1784271743802,"responseId":"resp_0e15f89f5b7b4062016a59d38039a8819497a212594ed70254","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d381f5308194ae0d74ec585164c1","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdOQ1CtMjexv89R_07ne7CtKTZvexuajAZ1d3xvBRG4J3zyrE4vczm_NSEhlX9JVP9E_FvDAh-UegGfZRGN4ej-de6grZbUg82fqSi4g7QjHeJ2u5qtR_uOuUfd-SVdQ3uPgrihJU4vbwL7mJ8nqtOplzQmYsxsBG5KR455_t3tnDEJ6n7mAy2et-LUcPZXQj8Ak1Ajl2jFIHMPvRuLmUKNFqD-J3CuyudRydvxGOpNLfx1uKUFZQoYyHd6plUM0-oKyFu378yG2UlWTptEoruFuitkw5cxBuHpRKDRMJnW6ZjAhrxDh2r7ifsJLW75dprtKNYUo1Dgey62gRcBwBO1m___NfcCpr1D2tq9N2ktrI_RGi23DnHxR86B9p_ArB8FF3ov3C4pKRRbCYxiVuc9xiUkdku1db7VvZ3svs_k0SYe-t-_cXV0m_jgOgTa57lF41vJMEG3JsLpiS1lbcYSDFksTHqT0Obqoh_W33xMx8qymGqjFmaggOda8jlVzsW8t0gy6FKQYU7BnwhXWgHgf6uwdAsZKrUq_iP9zi7WlHOFuFEtu1vihVwJ84mDYqXbcR-LfaNKN5Vihr9e-BkJHjq1gvVX0UwsEeDsHvO2lptRUE527sDHMuPYZQ43o2H8sM-6gsSITkuOgKLCXK4AQ1awhLIwTJHVWrXesS5Ztn_9oKc72dsDJqtTiuKlvf2S4DrqjGBCT3R0ww7LyaxS4ErEcgHl-A4SNm08IwIRDpl26ri3On2MfwWyzPHgPcYQD1ba-KQmNAH17GigdhWKbqzhbKZS9Iw8rEk5_Rzk3gTx6BMdIVAeI6s_fXHe3pyeeMfI67lWK27Sp4PQ3OMSAr_6mRquChtwl4tKgZtWLSbIr73_3SKKWrX_bbStoDHqFECz0yXjPeakOIRtucC50Ph6wnVPwxMGpbIFs8lE0xb8QFw24Cbx0cK1Vw_TaKsUyo7BbuyAT2uVFiQlvFCJh6IsXDaIrnKxQ4ymUJHXZ_zW9L9tql0UCECvc8kzhk6xy206cNGZZkCg5xBstbVO2DtEsMg2xGlwoOx-qkV7TIMEoBvjvJZqb6S2uNdwLRMW37sFyuZJP5WzjawsCOj_4pJThtTjywPvZS9jo0kFvJau4yzNDmJzWxRX07TqzXe8ul3Gl-pwR-0wkyWLhqx0vxf-7N6ry0GWUtJpY2me984Ut2FTr4GSUvjNVJ0pmDzeUEKHlgFruuXajoB-NS4YzrBX7nQp9AuFF-FsN3YtlSszqWQ3Yrib9hAkCeTAf-2EWbHjmj5wQl_8ywYzyaCfbUtowX5ftZeI0w6AOT0_GSpQVbfc1RKgv-lkCdHMsS_cQrjl89GKzrP1um09mbJXoeZFJV9dG2XM80qqg3ClXx0XPU_Jqyz9OQqTKEsdhAkaL3j0SSDU9ioLcollPvWUcCGCu9DN3BNeP0ChG4O2j6KsX4HaBEe9n4zTvNlcsQ_wnsWNzYBjjefsWCpVf9TGaqT3guFVmJ6X-VQdhomv5532Db1VykguhuIFw3uHerh_adxqp29plVTm_ekoZcke_v3Y6L9Og-mBoIH8KsDFIbgeLqhzZn6jhYoIHqfgL6SM7eEhXRAjHwLkoCIOVBxEPwwbyA-1NFo5YpucCjoHTMNtOjJteey1e_MGAFdfMVr7suEnzKXP9KeBo9Z2v_1vcLxhJuLFbVhggBPvQX0VFWZUfKQC4f6_fx9G7oO8-O_aYcHEi9z6OKeafJ4tqZRIDqY1w5l4jwftUKI7yDAm5kz6wWYDN9vK-R9iZvLtt9EciZEkywzUqEZSR4uQ28JZHL_ERZ6Ij2S8nwHu5Z7U7gbMztzMqVLvdBBpiP0fiebbxsPny7i_CmABtKF1-gn_LnX5_eLl9drz6qP6rZyO5VRr00IEDnpJIdkDateYkdtizK-kxgoonR6qMHCs8IZp0WSq43T18qsx3YeUf-iftU6Wu_ssSAS1tlElJUAC4nT4RPFVZrT0_xth4AMDR4faGzdT1EUYp6fTOqGRd74Hb40MDw_0nali-SFW9Gjf73EAM8vqL-RxRuPHFsToDg0c35-ZOYPo4O0Q6s8Ot476lrfPr9LEDp0qcysVk8hkyd8q2sZpBkXPa3aQstPOZiHpScDjyvfWsWj0_nwUCvmJx3TJygDFkjMQdrEL7nAtOFz43EoCnnbOrtlVCjMz63JHjfwklfcPLuY7w1RNYqFQlXsCXohwBRy-_9XeEZNKMXeU9RGgClLgn8fxAydTHgEGk72pIaNIidXDPRUSipXwzLwsZY4n6WrD2oyhI3OkEO4hu3tjiPrg9rHT-RS_znKESxfqKN9QjQdNCBwnf9el9rvURXVdVLPjd68ClPL6Bq39zlOJYYHjiq0mDKoQL8q5FYlgILKUi7X5VJVRYAl6WgxsOPOGnIZa5bljTiPC_SD06-FNQWZkBmjbBbhvVybZobq3ZLRrjJV8VSMH8WnqHLar1cYicJzPK4yu9i_f0tClgSjHaMHnejTzkR8WGy2hD5AzzBOrqmXbxccTJWq7LxJbiI_MEj5tijb5B1PtSN5B4I8b0G7kr623d2S8DRbuGp2NW4Tz0ueeYYuOwL2XUnuJO9uAt4-rAKJmufZnnEC5H931r9ws7mNaIUjUQdBVkzkZIpQwUPGQvFvwZz5FsnXUKZkBVSyifmVV7IGKaEYRxJuwzuh7Bn7rs7gk8gKvok1Cus1opjNBxDs9r66ggxIcn3lECxkGvRXKgDPmollb-Md2PhH_sfsK5YdIGGNZXn91g9IpRaymQ2J0ofUtg-uOZO42TBRXGAsIVP8UdcV3dVsXdwzjSItrN15LA2YYLqBkDT_1zAMowd8elpB51p5t9D68dsiWUD52jL9LATG4wr9tGBu0EuOIRmacmUs6ngR-EwvVa1Nf6KpHmZ7bg02so1V-Ff_RRmkq83kSff9arl7auaUSgDp83RkQKrwv28sgKsLhU20vpA1U2cjbUI19-Y1tmornvpNBGFY1-ilPGvHozqtjGaL8yDZCN0-wOJUH3P-965kGibFzYIJs3pPe7P-7ug6oW8IObi1tkoGlVKsayjj0hG9jLlL8NJOm4JmhZsFIM4RpkTzmDMltfZlnKaJZ-EQxT8swxEkucJ1Qr8XMKrtj81QbRIBv8PcXw_1zdkcaXElEjakAaUONl_-PZo5CEe69mwlrpPUKu5zXk9ZjIsgEvZHVjMhC74BGdO1cNm7OD7vtx4xzAbo6vBm3V7cpAGNCi7XaWcIqa30ZdRYfL_lBzMD3-45Q1psXoat4elX1eheo7DJoYQ68sZ4zkYHfAbKn0tPk4UxcUft4wgpJCQyc1wREHKDaP94SHx1QECcbuZ9GKkGtJKfPQXoU5cSwEeTBzzL74apA8BlamuGgSCb265d_dESbqwlCrdrgk8O9sL7xMCAsPBuX5uazpRapAzmEUyetdQxEzHqber5ooKC3Ist_nGJfepWJNdOmZPXWKCgbkJr_RhTlJnJybWzEXSI1jL74pTLI7NMnCTKoAdFjONPcOL45Wl9_YTuUz90UPZdP9LtDReYhdrl3nns1AB_JLTRpjfgqFaYSqJL3dcATmzy4vGcAOlnwCSd6FHoQ81uZ2goA--65lEpHdE6oDLCP6thwTeSA21HW4cn7yNstuFw8fEfPFv6btxPuKdtUarrXmPFtqdGPkmdjM3c2lzqY36Au0mgaKwsjiVPTXA3MymcR_ypRJ0df9DqeR9dSBSc7OiDV1Fpn7yEQqnNvfrHA24mf728bPYlhJ_Es5604XinbeTk4NgFLlIlVLQZPB548TFDELLcMqy3TPhDPoZe1r0lkdwNr5OEyyguBE4EQDXfuvrmP7D3vkD0bFjf0vQBC_t49PsRY8gb-hyftQE9aJ8IWUI-A9pDvcw0zp2Tq5laEvwEdidRtFUKO0EQavWSJafhEn0EteGGTxPwmftCPZMUehCDG07mmG","summary":[{"type":"summary_text","text":"**Planning Windows file I/O probing methods**"},{"type":"summary_text","text":"**Analyzing registry and I/O operation patterns**"},{"type":"summary_text","text":"**Exploring HashMap duplicate prevention**"}]},{"id":"rs_0e15f89f5b7b4062016a59d39093ec819491ce6a8ae060d0b4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdOhQGlCgVQQNqdBOL1v_g5r_zGjgWTLYuari_lgx6UU3LLiV3_zP3ipPUUaekl4qW9kTMOD4LH0me1ICeDEZyAch90AzsrpsFuioDiNgZOXR-LhCBHeZBRsEn8kZt1_WU9F-_dpSpfBwMdCyuKvZqBP0qX0fibV8cDjYttRMtdztG4Q-iQKPF2DTdS1DKs2cjHvhmO49iK4UDEc414ct9sH717K-EEeWIiG7m_zkGNylLGtuFL_PoOmwtET5b-voWmrRJ0qn09Gs0_Gtdw6y5HpjZ_rwlzVqtp3uFuW8kSgS6IF7IW9XvlhVl-VakOurjZ5UaX75vxnj7gSkWxRvTzxuV3OjH2Ocx0sI0GUod2OFC531j7C_fHztFmA4E2QAqeSgF6yMoSJtSPINbfPhkkDBSbwoKKcVlcAzORybpN7wmgNRLW5ZfWU9hkczBFTrGWi2GwI-YEo2hbgO_1PPPyjwNNhxSw7r-G1-I7ohQro0pVGrx6sGN_RhcFxKIKY1hXNVoBLYjEwpVgjPCEn9VyMAWJtvIQbEIbInXeG3BT7gutBxKglWITwk9sdaPOkHQGe6va9KXPkc5rAU3KfBUFq4KL-SPys2FQ4UjYj2GKjl6wloQ_hkjeNeVpiq7MF2cbvDcfPGYbgcN4Bde0aAFWGJ-dTClJ4LXLeZi-jaR_57WGkIg0sKrRhySt9hAXn2rju7GpvyMmuHlqeBhHA0KGngXttb9zQvxxIjtfdqNhRK-RAPNU-G216PRraH-20wwVMxkOURPO_dtph4jTtf_VUVrJuvDsnh1_AxIVf9e3VvF2MTNXLOxJ7sf-dEumLzsyH0jT3nnIG9rxd_16jtRWxIzEdyhjjOI5z6wQJ1VS6Bs8FUwqTVMjMPF-FFPypiZrryTOwibz5nORMYO0JbEt-lzVfFBgG7WAcN5yizH_wV2T9y8dA9Xb6BMn22lMi0VER42_sJBlil9MwEuk4aONymwwFi0T7pGghhUcurb8XSBm6X7Qc7X5LrjY4lHTDUd3rpOhAWTWZ6Cv9I1Ct7CJnPSukfbZ7xZbCBPlzUEGzKFLHJx1xEZjbwGhJOcFF2tfKRBwbyv6Olz2M-KRtVgDuMSt3qwXrDk3x0_ymLj9s-0sl3h5LNiaOQuhWiDJ1GG-B2Doo-NHN9oIMFInJOjmnpwilNkgGzM_MYZweHpzsy5xu2Baf_1_O3oMUaLGn5j9Wngml965zsW03aNOS6p_TgCaxNPTAFY7MPWBLGWewd1BRu1srGoqtDZeGljP4dPJDCUCzgx_ilC3SaQZgd_PgrnRi38N-QvgUjeU7spKZ1AejDHGlXL-7roUsQt8YZDxsW5sB-yDkC44lj7Tlqfuq60IYELhT8Rn9g_BPdxI89U-5_FFNClsqE7imMgddM86iWiwsZommga_u2caDyw_AHEOYZcLbIUFBOpoBGuB7rV0EhPUG9vkDBwLrJHMp-u0trpKq7JtUX7YMjyn4Hej84XzPptd5JXfwpfXtS4Bd_IpKCGc4sbl3ecwusI-TMJij-MYZuDp5M3DjZJ03TZmE00lfZ6YESJw2wCRMj1RwxpWt2nlkVfqb80_RNCkK9yqMEBvVYfDMRTi_qQLlsrbCjGillVgnvGFPnGEamVHnH5S3twHtJjX_uuwA3Tp8SymT21BVK-GevNWPHJWoCXUY_ykDjl6UReMSaB67JxSbIT-9OV2SNYTz1zo7dNBOd9SIqlqCMaIuSYo5NPjwtTZkItS1ZzPkMyLymxJUZpdTdNlf6b-OX6sFXPG5lK3VxKH3tT3yULUwgmGkXDX_5yD5iQh3iim4ffpck70NaLlgzqyNinfqMlrCbvlOrASlv-ich_4l0aoMsT-OGBgI_xKBqU-cnuiW7ihrfNMDvVA9GaUp9mdOSTYQMimOXEUmW35iFF5EtSfb6trbKbNAzdpktJEpyBLdmA56c4tYDgo5zZHQZ41HK5J59UTBup2rPlV31M2i76CR4fvaEgE_67fV2BEGipTDZzARlXc5R-LQ6nFPlzO20YX5riB1KP21uOFITXgeW2a30EEcqF6N0SWfnZ6zHGpt9NeYPadpNBEDg8hZomgxnTYCj1ERvKlo99QV1EQWDPQz2h1DfJsJlzMLHO6x-Yj9JUX2wYjfQQgj2m0FXBIVyNeu31G-vut1P35g8zsx_5-7Pcrrec8g8qwlJ2ymLJumgsFH3T4vqK5wEUyDQkLv2XYq53mnGZE-1vZhD51_57vdCX-FCQdTyPJIKMkL3asum1HCQPdyIy8CHi1gWuXHmkl0KemrvD-heQRIQdnlacyz4PJAiMK1Vjemn6VGFUepczKII7U672bFMX_LHtmTiEDYioou1gHYrPYLjW9nQWmriPGuloTjA7lebSagx5__dkU0vI9OrwTbpcD8xgieiOk0ogOULNJ975NYdD_3DObocSB9CvJeOmznoyjCMfmjIndhnv6I2TP4LhY3_BnXqk9XYTAzs_qK3E1kqtWCxU_kdj81Y6tjldbLkqBFhPB0drNlmUZrM51CSCztCvr2X4TIGutv24uRZMr-8BQG0Ibaya0GLMHBznBt0mM84ekHipT6KRTZZCKcU2iCHHv7gag23-iTTdBAh_8xhuKVtv3S43mBebs5XOcod56HtTM-EGvIABp21aq62cRE1QuTsKar6DPLu3VAt8lihZ0GGbOuWkQ_BcNTuiQdQk8iRkHKoTGRmyQX-FsaSd13JsjvdHc04ipT0s-VE_MMj0SIavWMs-CcBykKMBW7CC6uSkmbdMF_GegTy9_AGTRgAfO8rVPZur-YAqvijn-h3_A5lfviYlS4FETRIxYunCcWLo-6idI3alZ7kKIeJmnkW_09NSvKHPt8jbUt4ojtdqDrwRMBL9JoyoqVdbqcatfQzb3s43uxAvfKMyGtXME2lOAGR66xb_Adq5P52T03GWzhxZRoWGJ4SbxFf4YYgelSh6q-745i8yxmmsJ1Hkr4pgF0vZpTTB4hIhEEwHxQiABlpFV3DhSg-HnKiKYaG2bV9qYAanRTD_jkoW0dW0KCdjlb6xejuTM-BFnd1AGYmYnK1SAAd6625gQc_T409qEdn02ogj6mejj5YY49LWh2I7cpIIGFShOtBCeGBX21l7ImijtORzEpgLrtu9mLovXGV8x2MZXgMSPwoyJRb4HNbxYiTmGL4iJk99c-lHmnI7Yx6Y17hL6tUXx0pfVftWwlC1zkhribQpyva8OtqOWV3tNZt2H8k_AaGMTfOwo9WgLbemeBfHczVcyGS6pEAR_e7-hZWyExjuHxOaRDHTG3AEkP4ysMxqAy8EtylwTx2YAlf3EgNCwNu8RzULg3y8zHyUCmMeXDcYVaf2AZwLMg5NeW1mEv5FrCBionbSHa81w1FwNb9ADvHQsbHAXtnMVyAU7T4elgdd4ifmtj0kjNtTVA6KOKeCfl2tpu7Un_OGU7EpFEQTwy7sXJQLt8Qr-Am2QQwEjHK5KuixHGZwh9YKaxFSQ4I9ik1whmIqZw48SUBW_1IB3EdriKc6GDtC7a1immr2M86iTSFXmvbvxpnmomc7D9V2QObgexFpvbLgOAvGlJbMTuIHQawIfS41nXVrhDkWldHtfKcaD25erphMJlfNoX8cHfnz6_Ub-QtVSzInz97J_PGY3oootldzuODUARfBa0qbQVYPk7Y3ENnA9Cexrks5oqFf7hNFfPCjjd7u-U9cxjzRGyDh4IN7O0_Lh5WgzhOK2jTWJ49OEDneajuy-KqWU6gj0OS6fS9zJhlCWl3gIAR5rITolc1Y-uFgj2WAoR8B6zMnntguDmbpd89LQCXmbIRPxhqrpkWYN8NfiCjVy-FUw5ZYfVZHqDrmSIp6EdqtJ9ozPoQoKKsg90pY2KGdkjblP_lNT7tQ2hwaI0WwLzyZUTbcAIncXsDYKzsgdgUpyGYt1K3ZBh_gxaaNaR6DBptxAupU94QqvjywIo4tsKBMLxBG8nswk1F24HADuFK--7uONSaRpnyvs9R_ynyBG0dQWSgq7wy_86fkxbgt61n1BP66x0_G6Gm8LZHxBArs8LTeGLjNk=","summary":[{"type":"summary_text","text":"**Analyzing broker-to-brain connection behavior**"},{"type":"summary_text","text":"**Planning detailed Windows IO and CPU profiling**"},{"type":"summary_text","text":"**Distinguishing registry stream poison patterns**"}]},{"id":"rs_0e15f89f5b7b4062016a59d3a12c5081948accda55542a9c18","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdOkpSDp9xIGgXQmkUdcK2AuqBCEsdxlLj5LV-iwEy-lp3ywWjkAGOuc58cCaodlV5fELG69Pf0Zkc3ghznJO3Wcjp6lsy4jTiaSMGdYVBNRX5IIbPIGu6Y91oJw-aUNSsGfFdwtz3vb1lvWSMiYIU9PGSsr7AqK0GuCNcgtWt9nxn8k-ka-4S8AB6J4lMrG9kMFk8E5Wem4Uum6q8kz9CMOuDI5i_UKaY_bGUYAscFf3snRyyGI9da9COBX4YaNm5e5Eqw0f1ydSjJdS65E-jKXIj2-LqRv4o_zEtt9zuG6SvXsYxgl0kxyuYUa9t3ZNJgY8N88ahjzgX0YtiHHMXgbizOqj5asPcSfeVRQPxP-flH_Qw-9GMRfJ8dbRGEl0yHbRjCZUqNWkZ745X_6t416o1gYYhAtIKgcAKrZONe-SNlVCm6f_aw4mlq8zqqO1jrwfkKZO3J8kpUjnlopxHi7m_K2JbsNZJZeiyVpZoVtq_aKl2JfurJEblDix7OPVgJ_Mie-Bk-8B3F8fAoHcCwnR4JF0hmck_E_BT33mrfp67j6XZQLyS7SlW7NeHC0O5nvNAN1DlJL4ZXZA1xv-76sIEDvJGwydn1G4MOR598g0Km_qv9JLDj_io4YaCmOcFSwnN18jvDMXec993OuxIyEaskqtBeRjjqPAI3E4d7KuDcDehdDeBQiX33wC7dIH12vQIW9S61LK4vCaKpl13BsR_5G9M2Qn3s1hELcUjJxdF2r5P_s4goQDA_jdi_d-2G57eV0T-Xfa9gTowHAdNzzivq6lSedE2XePfGJGWVbaZ1uVW21ieEuci3rs0xFr6XvNL1FbuN5mDIlv4q4B2jci8Q8FbdS-y6cNek83L6pCeWJyvzNJgspitICdcSV-cegHsdZFp3TUHN-JTaNbDVU_TBu71HPxxXJpHUrKtBrBGU-QgPwbcmEVNdbpb7yXzJi6cF063IcadGlwt8uzEni2kO5R59X8inEUJxD_aQghSZU-6iLN4KMyMWhvKnCsTrrT_X6F9DNxJduX14TVI2JZkUpk4vL80SZPFqqsxiRaS0=","summary":[{"type":"summary_text","text":"**Inspecting connection lifecycle logs**"}]},{"id":"fc_0e15f89f5b7b4062016a59d3a4e16c8194993be4b3fb01975b","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating conn lifecycle format\",\"pattern\":\"fn lifecycle\\\\(|CONN_|conn-lifecycle|event=\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_w2a2tNIgHzaMLJiaQYa70JaA","name":"grep"},{"id":"fc_0e15f89f5b7b4062016a59d3a4e18881949400212128c1a044","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading conn lifecycle logs\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"155-235\"}","call_id":"call_6x1jJS68LG452nxWPy6QHP1H","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3a4e19c8194bee965ab451c01ca","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry snapshot writes\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"730-770\"}","call_id":"call_Ls7Kw1rESvf7c7VozIhpMeA6","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3a4e1a48194829de2d91ef25fd8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatch retry semantics\",\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"selector\":\"210-330\"}","call_id":"call_VtqOtdLDXkvh6YiXvOkeFk8s","name":"read"},{"id":"fc_0e15f89f5b7b4062016a59d3a4e1ac8194a4378e81c3611a2a","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding dispatcher retirement\",\"pattern\":\"net_stream_retire|retire_stream|KIND_NET_STREAM_RETIRE\",\"path\":\"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Q759xD5yIyyTS1tCdRWUaZkv","name":"grep"}]},"duration":37878.61920000613,"ttft":2169.361200004816,"contextSnapshot":{"promptTokens":122989,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_w2a2tNIgHzaMLJiaQYa70JaA|fc_0e15f89f5b7b4062016a59d3a4e16c8194993be4b3fb01975b","toolName":"grep","startedAt":"2026-07-17T07:03:01.682Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Locating conn lifecycle format"},"id":"7e3b83c3","parentId":"68631f7a","timestamp":"2026-07-17T07:03:01.682Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6x1jJS68LG452nxWPy6QHP1H|fc_0e15f89f5b7b4062016a59d3a4e18881949400212128c1a044","toolName":"read","startedAt":"2026-07-17T07:03:01.682Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading conn lifecycle logs"},"id":"dabfa04f","parentId":"7e3b83c3","timestamp":"2026-07-17T07:03:01.682Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Ls7Kw1rESvf7c7VozIhpMeA6|fc_0e15f89f5b7b4062016a59d3a4e19c8194bee965ab451c01ca","toolName":"read","startedAt":"2026-07-17T07:03:01.682Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading registry snapshot writes"},"id":"50a10e89","parentId":"dabfa04f","timestamp":"2026-07-17T07:03:01.682Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VtqOtdLDXkvh6YiXvOkeFk8s|fc_0e15f89f5b7b4062016a59d3a4e1a48194829de2d91ef25fd8","toolName":"read","startedAt":"2026-07-17T07:03:01.683Z","args":{"path":"crates/spt-daemon/src/dispatch.rs"},"intent":"Reading dispatch retry semantics"},"id":"0fd64aec","parentId":"50a10e89","timestamp":"2026-07-17T07:03:01.683Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Q759xD5yIyyTS1tCdRWUaZkv|fc_0e15f89f5b7b4062016a59d3a4e1ac8194a4378e81c3611a2a","toolName":"grep","startedAt":"2026-07-17T07:03:01.683Z","args":{"path":"crates/spt-daemon/src/brain.rs;crates/spt-daemon/src/broker.rs;crates/spt-daemon/src/nethost.rs"},"intent":"Finding dispatcher retirement"},"id":"556c730f","parentId":"0fd64aec","timestamp":"2026-07-17T07:03:01.683Z"}
{"type":"message","id":"79055281","parentId":"556c730f","timestamp":"2026-07-17T07:03:01.685Z","message":{"role":"toolResult","toolCallId":"call_w2a2tNIgHzaMLJiaQYa70JaA|fc_0e15f89f5b7b4062016a59d3a4e16c8194993be4b3fb01975b","toolName":"grep","content":[{"type":"text","text":" 74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n*75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n 78|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*79|static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n 80|\n 81|/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n 82|/// startup ([`init_log_anchor`], both broker and brain entries) so every\n...\n 141|struct Inner {\n*142|    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143|    id: u64,\n 144|    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145|    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n...\n 229|    /// Render the once-per-conn retirement record — the F-039 token split\n*230|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233|    /// already-dead counterpart, the offline-peer dial churn) emits the\n*234|    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n 235|    /// SAME attribution fields — quieter class, never thinner (the retired\n 236|    /// class is what discriminates fresh-carrier churn from interleave\n 237|    /// artifact in the field). Behavior is identical either way: poison,\n...\n 242|            format!(\n*243|                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 244|                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 245|                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 246|                self.attribution(),\n...\n 254|            format!(\n*255|                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n 256|                 broker conn poisoned + retired, never reused (same retire behavior \\\n 257|                 as POISONED — only the failure class differs) \\\n 258|                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n...\n 269|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n*270|    fn lifecycle(&self, event: &str, extra: &str) {\n 271|        let sep = if extra.is_empty() { \"\" } else { \" \" };\n 272|        eprintln!(\n*273|            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n 274|            self.attribution()\n 275|        );\n 276|    }\n...\n 287|            // First poison of this conn: loud, once. The token is class-split\n*288|            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n*289|            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 290|            eprintln!(\n 291|                \"{}\",\n 292|                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n...\n 428|        let inner = Arc::new(Inner {\n*429|            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n 430|            label: Mutex::new(String::new()),\n 431|            first_write_logged: AtomicBool::new(false),\n 432|            gate: Mutex::new(Gate { half: Some(half) }),\n...\n 707|    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*708|    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n*709|    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 710|    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 711|    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n 712|    // monotonic stamps. Quieter class, never thinner.\n...\n 721|        assert!(\n*722|            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n 723|            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 724|        );\n 725|        let retired = conn\n...\n 728|        assert!(\n*729|            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n 730|            \"organic class emits the DISTINCT retired token: {retired}\"\n 731|        );\n 732|        assert!("}],"details":{"scopePath":"crates/spt-daemon/src/conn.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":16,"fileCount":1,"files":["crates/spt-daemon/src/conn.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/conn.rs","count":16}],"truncated":false,"displayContent":"  74│/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n *75│/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n  76│/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n  77│/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n  78│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n *79│static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n  80│\n  81│/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n  82│/// startup ([`init_log_anchor`], both broker and brain entries) so every\n    │...\n 141│struct Inner {\n*142│    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 143│    id: u64,\n 144│    /// Accreted attribution facts (subscriber role, endpoint/session where\n 145│    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    │...\n 229│    /// Render the once-per-conn retirement record — the F-039 token split\n*230│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233│    /// already-dead counterpart, the offline-peer dial churn) emits the\n*234│    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n 235│    /// SAME attribution fields — quieter class, never thinner (the retired\n 236│    /// class is what discriminates fresh-carrier churn from interleave\n 237│    /// artifact in the field). Behavior is identical either way: poison,\n    │...\n 242│            format!(\n*243│                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 244│                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 245│                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 246│                self.attribution(),\n    │...\n 254│            format!(\n*255│                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n 256│                 broker conn poisoned + retired, never reused (same retire behavior \\\n 257│                 as POISONED — only the failure class differs) \\\n 258│                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n    │...\n 269│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n*270│    fn lifecycle(&self, event: &str, extra: &str) {\n 271│        let sep = if extra.is_empty() { \"\" } else { \" \" };\n 272│        eprintln!(\n*273│            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n 274│            self.attribution()\n 275│        );\n 276│    }\n    │...\n 287│            // First poison of this conn: loud, once. The token is class-split\n*288│            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n*289│            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 290│            eprintln!(\n 291│                \"{}\",\n 292│                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n    │...\n 428│        let inner = Arc::new(Inner {\n*429│            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n 430│            label: Mutex::new(String::new()),\n 431│            first_write_logged: AtomicBool::new(false),\n 432│            gate: Mutex::new(Gate { half: Some(half) }),\n    │...\n 707│    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*708│    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n*709│    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 710│    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 711│    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n 712│    // monotonic stamps. Quieter class, never thinner.\n    │...\n 721│        assert!(\n*722│            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n 723│            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 724│        );\n 725│        let retired = conn\n    │...\n 728│        assert!(\n*729│            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n 730│            \"organic class emits the DISTINCT retired token: {retired}\"\n 731│        );\n 732│        assert!("},"isError":false,"timestamp":1784271781684}}
{"type":"message","id":"02a51557","parentId":"79055281","timestamp":"2026-07-17T07:03:01.693Z","message":{"role":"toolResult","toolCallId":"call_Q759xD5yIyyTS1tCdRWUaZkv|fc_0e15f89f5b7b4062016a59d3a4e1ac8194a4378e81c3611a2a","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## brain.rs\n 52|    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n*53|    KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED,\n 54|    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n 55|    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n 56|    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n...\n 1453|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*1454|    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n 1455|        self.send(\n*1456|            KIND_NET_STREAM_RETIRE,\n 1457|            serde_json::to_value(NetStreamRetireReq { stream_id })\n 1458|                .expect(\"NetStreamRetireReq serializes\"),\n 1459|        )?;\n...\n 1461|            match self.read_event()? {\n*1462|                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\n 1463|                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\n 1464|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1465|                    return Ok(reply.removed);\n## broker.rs\n 66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n*67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n 69|    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 70|};\n...\n 3145|                }\n*3146|                KIND_NET_STREAM_RETIRE => {\n*3147|                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\n 3148|                        send_error(&send, &msg);\n 3149|                    }\n 3150|                }\n...\n 4383|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*4384|    fn dispatch_net_stream_retire(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4385|        let req: NetStreamRetireReq = serde_json::from_value(env.payload)\n 4386|            .map_err(|e| format!(\"bad net-stream-retire payload: {e}\"))?;\n 4387|        let host = self.net.get().ok_or(\"net disabled\")?;\n*4388|        let removed = host.retire_stream(req.stream_id);\n 4389|        let frame = Envelope::new(\n*4390|            KIND_NET_STREAM_RETIRED,\n 4391|            serde_json::to_value(NetStreamRetired {\n 4392|                stream_id: req.stream_id,\n 4393|                removed,\n## nethost.rs\n 1835|    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*1836|    pub fn retire_stream(&self, stream_id: u64) -> bool {\n 1837|        let Some(entry) = self\n 1838|            .shared\n 1839|            .streams\n...\n 2413|\n*2414|        assert!(host.retire_stream(shell), \"first retire hides the row\");\n 2415|        let ids: Vec<u64> = host.stream_infos().iter().map(|s| s.stream_id).collect();\n 2416|        assert!(!ids.contains(&shell), \"retired row left the enumeration\");\n 2417|        assert!(ids.contains(&owner), \"the active sibling row stays\");\n*2418|        assert!(!host.retire_stream(shell), \"second retire is a no-op\");\n 2419|        assert!(\n 2420|            host.stream_opener(shell).is_none(),\n 2421|            \"a retired stream answers no-such-stream\"\n...\n 2447|        // Retire FIRST — the ordering the race loses in the field.\n*2448|        assert!(host.retire_stream(shell), \"retire the serving row\");\n 2449|\n 2450|        // The worker's reply lands AFTER retirement and must still deliver.\n 2451|        host.send_stream(shell, b\"{\\\"kind\\\":\\\"reply\\\",\\\"outcome\\\":\\\"edge\\\"}\\n\", true)"}],"details":{"scopePath":"crates/spt-daemon/src/brain.rs, crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/nethost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":14,"fileCount":3,"files":["crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/nethost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brain.rs","count":4},{"path":"crates/spt-daemon/src/broker.rs","count":6},{"path":"crates/spt-daemon/src/nethost.rs","count":4}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## brain.rs\n   52│    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,\n  *53│    KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED,\n   54│    KIND_MET_MEMBER, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET,\n   55│    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n   56│    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n     │...\n 1453│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*1454│    pub fn net_stream_retire(&mut self, stream_id: u64) -> io::Result<bool> {\n 1455│        self.send(\n*1456│            KIND_NET_STREAM_RETIRE,\n 1457│            serde_json::to_value(NetStreamRetireReq { stream_id })\n 1458│                .expect(\"NetStreamRetireReq serializes\"),\n 1459│        )?;\n     │...\n 1461│            match self.read_event()? {\n*1462│                BrokerEvent::Other(env) if env.kind == KIND_NET_STREAM_RETIRED => {\n 1463│                    let reply: NetStreamRetired = serde_json::from_value(env.payload)\n 1464│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1465│                    return Ok(reply.removed);\n## broker.rs\n   66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  *67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n   68│    KIND_MET_MEMBER, KIND_PAIR_CODE_SUBMIT, KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_PAIR_MEET, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,\n   69│    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n   70│};\n     │...\n 3145│                }\n*3146│                KIND_NET_STREAM_RETIRE => {\n*3147│                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\n 3148│                        send_error(&send, &msg);\n 3149│                    }\n 3150│                }\n     │...\n 4383│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*4384│    fn dispatch_net_stream_retire(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4385│        let req: NetStreamRetireReq = serde_json::from_value(env.payload)\n 4386│            .map_err(|e| format!(\"bad net-stream-retire payload: {e}\"))?;\n 4387│        let host = self.net.get().ok_or(\"net disabled\")?;\n*4388│        let removed = host.retire_stream(req.stream_id);\n 4389│        let frame = Envelope::new(\n*4390│            KIND_NET_STREAM_RETIRED,\n 4391│            serde_json::to_value(NetStreamRetired {\n 4392│                stream_id: req.stream_id,\n 4393│                removed,\n## nethost.rs\n 1835│    // [impl->REQ-REDISPATCH-FINISHED-RETIRE]\n*1836│    pub fn retire_stream(&self, stream_id: u64) -> bool {\n 1837│        let Some(entry) = self\n 1838│            .shared\n 1839│            .streams\n     │...\n 2413│\n*2414│        assert!(host.retire_stream(shell), \"first retire hides the row\");\n 2415│        let ids: Vec<u64> = host.stream_infos().iter().map(|s| s.stream_id).collect();\n 2416│        assert!(!ids.contains(&shell), \"retired row left the enumeration\");\n 2417│        assert!(ids.contains(&owner), \"the active sibling row stays\");\n*2418│        assert!(!host.retire_stream(shell), \"second retire is a no-op\");\n 2419│        assert!(\n 2420│            host.stream_opener(shell).is_none(),\n 2421│            \"a retired stream answers no-such-stream\"\n     │...\n 2447│        // Retire FIRST — the ordering the race loses in the field.\n*2448│        assert!(host.retire_stream(shell), \"retire the serving row\");\n 2449│\n 2450│        // The worker's reply lands AFTER retirement and must still deliver.\n 2451│        host.send_stream(shell, b\"{\\\"kind\\\":\\\"reply\\\",\\\"outcome\\\":\\\"edge\\\"}\\n\", true)"},"isError":false,"timestamp":1784271781693}}
{"type":"message","id":"8f444a14","parentId":"02a51557","timestamp":"2026-07-17T07:03:01.698Z","message":{"role":"toolResult","toolCallId":"call_6x1jJS68LG452nxWPy6QHP1H|fc_0e15f89f5b7b4062016a59d3a4e18881949400212128c1a044","toolName":"read","content":[{"type":"text","text":"struct Inner {\n…\n    /// conn refuses every future write and is never re-gated.\n    poisoned: AtomicBool,\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\n    /// an ordinary disconnect.\n    timed_out: AtomicBool,\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n    severed: AtomicBool,\n    /// A retired (poisoned) half parks here so its OS handle stays alive\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\n    graveyard: Mutex<Option<SendHalf>>,\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\n    op_seq: AtomicU64,\n    /// The absolute per-write bound (gate-wait + OS write completion).\n    bound: Duration,\n    /// The raw OS handle/fd of the underlying stream, captured at\n    /// construction for the out-of-band abort. The recv and send halves share\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    /// aborting this handle covers a parked read too. Valid for the whole\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\n    /// `graveyard` until drop.\n    #[cfg(windows)]\n    raw: isize,\n    #[cfg(unix)]\n    raw: i32,\n}\n\n/// Recover a mutex guard even if a holder panicked — the state these guards\n/// protect is a plain slot/flag pair, valid on every panic boundary.\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\n    match m.lock() {\n        Ok(g) => g,\n        Err(p) => p.into_inner(),\n    }\n}\n\n#[cfg(windows)]\nfn raw_of(half: &SendHalf) -> isize {\n    use std::os::windows::io::{AsHandle, AsRawHandle};\n    match half {\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\n    }\n}\n\n#[cfg(unix)]\nfn raw_of(half: &SendHalf) -> i32 {\n    use std::os::fd::{AsFd, AsRawFd};\n    match half {\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\n    }\n}\n\n#[cfg(windows)]\nmod win {\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\n    // (the deelevate.rs / daemon.rs posture).\n    extern \"system\" {\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\n    }\n}\n\nimpl Inner {\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    fn attribution(&self) -> String {\n        let label = recover(&self.label);\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\n    }\n\n    /// Render the once-per-conn retirement record — the F-039 token split\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n    /// already-dead counterpart, the offline-peer dial churn) emits the\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n    /// SAME attribution fields — quieter class, never thinner (the retired\n    /// class is what discriminates fresh-carrier churn from interleave\n    /// artifact in the field). Behavior is identical either way: poison,\n    /// abort, retire, never reuse.\n…\n}\n\n[Showing lines 154-238 of 815. Use :239 to continue]"}],"details":{"truncation":{"content":"    /// conn refuses every future write and is never re-gated.\r\n    poisoned: AtomicBool,\r\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\r\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\r\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\r\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\r\n    /// an ordinary disconnect.\r\n    timed_out: AtomicBool,\r\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\r\n    severed: AtomicBool,\r\n    /// A retired (poisoned) half parks here so its OS handle stays alive\r\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\r\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\r\n    graveyard: Mutex<Option<SendHalf>>,\r\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\r\n    op_seq: AtomicU64,\r\n    /// The absolute per-write bound (gate-wait + OS write completion).\r\n    bound: Duration,\r\n    /// The raw OS handle/fd of the underlying stream, captured at\r\n    /// construction for the out-of-band abort. The recv and send halves share\r\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\r\n    /// aborting this handle covers a parked read too. Valid for the whole\r\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\r\n    /// `graveyard` until drop.\r\n    #[cfg(windows)]\r\n    raw: isize,\r\n    #[cfg(unix)]\r\n    raw: i32,\r\n}\r\n\r\n/// Recover a mutex guard even if a holder panicked — the state these guards\r\n/// protect is a plain slot/flag pair, valid on every panic boundary.\r\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\r\n    match m.lock() {\r\n        Ok(g) => g,\r\n        Err(p) => p.into_inner(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nfn raw_of(half: &SendHalf) -> isize {\r\n    use std::os::windows::io::{AsHandle, AsRawHandle};\r\n    match half {\r\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\r\n    }\r\n}\r\n\r\n#[cfg(unix)]\r\nfn raw_of(half: &SendHalf) -> i32 {\r\n    use std::os::fd::{AsFd, AsRawFd};\r\n    match half {\r\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nmod win {\r\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\r\n    // (the deelevate.rs / daemon.rs posture).\r\n    extern \"system\" {\r\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\r\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\r\n    }\r\n}\r\n\r\nimpl Inner {\r\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\r\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn attribution(&self) -> String {\r\n        let label = recover(&self.label);\r\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\r\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\r\n    }\r\n\r\n    /// Render the once-per-conn retirement record — the F-039 token split\r\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\r\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\r\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\r\n    /// already-dead counterpart, the offline-peer dial churn) emits the\r\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\r\n    /// SAME attribution fields — quieter class, never thinner (the retired\r\n    /// class is what discriminates fresh-carrier churn from interleave\r\n    /// artifact in the field). Behavior is identical either way: poison,\r\n    /// abort, retire, never reuse.\r","truncated":true,"truncatedBy":"lines","totalLines":662,"totalBytes":3717,"outputLines":85,"outputBytes":3717,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"struct Inner {\n…\n    /// conn refuses every future write and is never re-gated.\r\n    poisoned: AtomicBool,\r\n    /// Latched when the retirement was DEADLINE-driven (watchdog fired, or a\r\n    /// gate-waiter exhausted its bound) — as opposed to an organic I/O error\r\n    /// (peer death). Callers see it as `io::ErrorKind::TimedOut` and can\r\n    /// count the wedge (the W2 stall-evict tally) without conflating it with\r\n    /// an ordinary disconnect.\r\n    timed_out: AtomicBool,\r\n    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\r\n    severed: AtomicBool,\r\n    /// A retired (poisoned) half parks here so its OS handle stays alive\r\n    /// until [`BrokerConn::drop`] — after the watchdog is joined, so no raw\r\n    /// `CancelIoEx`/`DisconnectNamedPipe` can race the `CloseHandle`.\r\n    graveyard: Mutex<Option<SendHalf>>,\r\n    /// Monotonic per-write op identity (the watchdog cancels a SPECIFIC op).\r\n    op_seq: AtomicU64,\r\n    /// The absolute per-write bound (gate-wait + OS write completion).\r\n    bound: Duration,\r\n    /// The raw OS handle/fd of the underlying stream, captured at\r\n    /// construction for the out-of-band abort. The recv and send halves share\r\n    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\r\n    /// aborting this handle covers a parked read too. Valid for the whole\r\n    /// conn lifetime: the half lives in `gate`, a writer's stack, or\r\n    /// `graveyard` until drop.\r\n    #[cfg(windows)]\r\n    raw: isize,\r\n    #[cfg(unix)]\r\n    raw: i32,\r\n}\r\n\r\n/// Recover a mutex guard even if a holder panicked — the state these guards\r\n/// protect is a plain slot/flag pair, valid on every panic boundary.\r\nfn recover<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {\r\n    match m.lock() {\r\n        Ok(g) => g,\r\n        Err(p) => p.into_inner(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nfn raw_of(half: &SendHalf) -> isize {\r\n    use std::os::windows::io::{AsHandle, AsRawHandle};\r\n    match half {\r\n        SendHalf::NamedPipe(h) => h.as_handle().as_raw_handle() as isize,\r\n    }\r\n}\r\n\r\n#[cfg(unix)]\r\nfn raw_of(half: &SendHalf) -> i32 {\r\n    use std::os::fd::{AsFd, AsRawFd};\r\n    match half {\r\n        SendHalf::UdSocket(h) => h.as_fd().as_raw_fd(),\r\n    }\r\n}\r\n\r\n#[cfg(windows)]\r\nmod win {\r\n    // kernel32 is in the MSVC default link set — raw externs, no new dep\r\n    // (the deelevate.rs / daemon.rs posture).\r\n    extern \"system\" {\r\n        pub fn CancelIoEx(handle: isize, overlapped: *mut core::ffi::c_void) -> i32;\r\n        pub fn DisconnectNamedPipe(handle: isize) -> i32;\r\n    }\r\n}\r\n\r\nimpl Inner {\r\n    /// The attribution snapshot every lifecycle record carries: stable conn id,\r\n    /// accreted role/endpoint/session facts, wall + monotonic stamps.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    fn attribution(&self) -> String {\r\n        let label = recover(&self.label);\r\n        let facts: &str = if label.is_empty() { \"role=unattributed\" } else { &label };\r\n        format!(\"conn={} {} {}\", self.id, facts, log_stamp())\r\n    }\r\n\r\n    /// Render the once-per-conn retirement record — the F-039 token split\r\n    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\r\n    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\r\n    /// watches); an ORGANIC write failure (peer death / broken pipe on an\r\n    /// already-dead counterpart, the offline-peer dial churn) emits the\r\n    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\r\n    /// SAME attribution fields — quieter class, never thinner (the retired\r\n    /// class is what discriminates fresh-carrier churn from interleave\r\n    /// artifact in the field). Behavior is identical either way: poison,\r\n    /// abort, retire, never reuse.\r\n…\n}","startLine":141,"lineNumbers":[141,null,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,null,410]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":815,"totalBytes":3717,"outputLines":85,"outputBytes":3717,"shownRange":{"start":154,"end":238},"nextOffset":239}}},"isError":false,"timestamp":1784271781698}}
{"type":"message","id":"27267b27","parentId":"8f444a14","timestamp":"2026-07-17T07:03:01.707Z","message":{"role":"toolResult","toolCallId":"call_VtqOtdLDXkvh6YiXvOkeFk8s|fc_0e15f89f5b7b4062016a59d3a4e1a48194829de2d91ef25fd8","toolName":"read","content":[{"type":"text","text":"enum FailClass {\n…\n    Transient,\n    /// The stream's subscriber seat is poisoned or still draining — retrying\n    /// immediately would manufacture another replay victim.\n    SeatBlocked,\n}\n\n/// Classify a worker failure message (the seat-blocked shapes are the two\n/// strings the nethost subscriber discipline emits).\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn classify_failure(msg: &str) -> FailClass {\n    if msg.contains(\"subscriber busy\")\n        || msg.contains(\"replay halted\")\n        || msg.contains(\"lease canceled\")\n    {\n        FailClass::SeatBlocked\n    } else {\n        FailClass::Transient\n    }\n}\n\n/// Whether an opener-query error is the OLD broker's unknown-verb answer —\n/// the ONLY error class that may fall back to the ring peek (ADR-0038\n/// Amendment fix 1). `send_error` on an unknown kind replies exactly\n/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n/// wire fact. Everything else (transport timeout, EOF, poison) is a\n/// transient — Failed → bounded requeue, never a second replay subscriber.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn is_unsupported_verb(e: &io::Error) -> bool {\n    e.to_string().contains(\"unknown command kind\")\n}\n\n/// The dispatcher's GLOBAL circuit breaker (ADR-0038 Amendment fix 5): a\n/// seat-blocked failure trips it — claiming pauses dispatcher-wide for a\n/// doubling window (base 2s, cap 30s), so retry machinery cannot hammer a\n/// wedged conn with fresh replay subscribers. Any successful serve resets it.\n/// PURE (instant-in, decision-out) — unit-tested trip/gate/reset.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n#[derive(Debug, Default)]\nstruct CircuitBreaker {\n    until: Option<Instant>,\n    consecutive: u32,\n}\n\nconst BREAKER_BASE: Duration = Duration::from_secs(2);\nconst BREAKER_CAP: Duration = Duration::from_secs(30);\n\nimpl CircuitBreaker {\n    /// A seat-blocked failure landed: open (or extend) the window.\n    fn trip(&mut self, now: Instant) -> Duration {\n        let window = (BREAKER_BASE * 2u32.saturating_pow(self.consecutive)).min(BREAKER_CAP);\n        self.consecutive = self.consecutive.saturating_add(1);\n        self.until = Some(now + window);\n        window\n    }\n\n    /// Whether claiming is currently paused.\n    fn open(&self, now: Instant) -> bool {\n        self.until.is_some_and(|t| now < t)\n    }\n\n    /// Real progress (a Served outcome): close and reset.\n    fn reset(&mut self) {\n        self.until = None;\n        self.consecutive = 0;\n    }\n}\n\n/// Keyed dispatch telemetry record (ADR-0038 Amendment fix 9,\n/// REQ-DISPATCH-HYGIENE-TELEMETRY): every event names generation, stream,\n/// family (once classified), attempt, and conn — the field-discriminator\n/// surface (poison-window census, per-generation attach counts). PURE\n/// formatting so key completeness is a unit fact.\nfn dispatch_event(\n    gen: u64,\n    stream_id: u64,\n    family: Option<StreamFamily>,\n    attempt: u32,\n    conn_id: u64,\n    event: &str,\n    detail: &str,\n) -> String {\n    format!(\n        \"DISPATCH_EV: gen={gen} stream={stream_id} family={} attempt={attempt} conn={conn_id} event={event} detail={detail}\",\n        family.map_or_else(|| \"unclassified\".to_string(), |f| format!(\"{f:?}\")),\n    )\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n/// A `SeatBlocked` failure requeues WITHOUT burning the bounded budget —\n/// its pacing comes from the global breaker window (`breaker_window`), and\n/// the stream recovers once the wedged seat clears (T5's not-abandonment\n/// discriminator). A `Transient` keeps the ×3 budget (D3 unchanged).\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn outcome_transition(\n    outcome: &DispatchOutcome,\n    attempts: u32,\n    now: Instant,\n    breaker_window: Duration,\n) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(msg) => match classify_failure(msg) {\n            FailClass::SeatBlocked => ClaimState::Retry {\n                attempts,\n                next_at: now + breaker_window,\n            },\n            FailClass::Transient => {\n                let attempts = attempts + 1;\n                if attempts >= MAX_DISPATCH_ATTEMPTS {\n…\n                } else {\n…\n                }\n            }\n        },\n    }\n}\n\n[Showing lines 209-333 of 1558. Use :334 to continue]"}],"details":{"truncation":{"content":"    Transient,\n    /// The stream's subscriber seat is poisoned or still draining — retrying\n    /// immediately would manufacture another replay victim.\n    SeatBlocked,\n}\n\n/// Classify a worker failure message (the seat-blocked shapes are the two\n/// strings the nethost subscriber discipline emits).\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn classify_failure(msg: &str) -> FailClass {\n    if msg.contains(\"subscriber busy\")\n        || msg.contains(\"replay halted\")\n        || msg.contains(\"lease canceled\")\n    {\n        FailClass::SeatBlocked\n    } else {\n        FailClass::Transient\n    }\n}\n\n/// Whether an opener-query error is the OLD broker's unknown-verb answer —\n/// the ONLY error class that may fall back to the ring peek (ADR-0038\n/// Amendment fix 1). `send_error` on an unknown kind replies exactly\n/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n/// wire fact. Everything else (transport timeout, EOF, poison) is a\n/// transient — Failed → bounded requeue, never a second replay subscriber.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn is_unsupported_verb(e: &io::Error) -> bool {\n    e.to_string().contains(\"unknown command kind\")\n}\n\n/// The dispatcher's GLOBAL circuit breaker (ADR-0038 Amendment fix 5): a\n/// seat-blocked failure trips it — claiming pauses dispatcher-wide for a\n/// doubling window (base 2s, cap 30s), so retry machinery cannot hammer a\n/// wedged conn with fresh replay subscribers. Any successful serve resets it.\n/// PURE (instant-in, decision-out) — unit-tested trip/gate/reset.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n#[derive(Debug, Default)]\nstruct CircuitBreaker {\n    until: Option<Instant>,\n    consecutive: u32,\n}\n\nconst BREAKER_BASE: Duration = Duration::from_secs(2);\nconst BREAKER_CAP: Duration = Duration::from_secs(30);\n\nimpl CircuitBreaker {\n    /// A seat-blocked failure landed: open (or extend) the window.\n    fn trip(&mut self, now: Instant) -> Duration {\n        let window = (BREAKER_BASE * 2u32.saturating_pow(self.consecutive)).min(BREAKER_CAP);\n        self.consecutive = self.consecutive.saturating_add(1);\n        self.until = Some(now + window);\n        window\n    }\n\n    /// Whether claiming is currently paused.\n    fn open(&self, now: Instant) -> bool {\n        self.until.is_some_and(|t| now < t)\n    }\n\n    /// Real progress (a Served outcome): close and reset.\n    fn reset(&mut self) {\n        self.until = None;\n        self.consecutive = 0;\n    }\n}\n\n/// Keyed dispatch telemetry record (ADR-0038 Amendment fix 9,\n/// REQ-DISPATCH-HYGIENE-TELEMETRY): every event names generation, stream,\n/// family (once classified), attempt, and conn — the field-discriminator\n/// surface (poison-window census, per-generation attach counts). PURE\n/// formatting so key completeness is a unit fact.\nfn dispatch_event(\n    gen: u64,\n    stream_id: u64,\n    family: Option<StreamFamily>,\n    attempt: u32,\n    conn_id: u64,\n    event: &str,\n    detail: &str,\n) -> String {\n    format!(\n        \"DISPATCH_EV: gen={gen} stream={stream_id} family={} attempt={attempt} conn={conn_id} event={event} detail={detail}\",\n        family.map_or_else(|| \"unclassified\".to_string(), |f| format!(\"{f:?}\")),\n    )\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n/// A `SeatBlocked` failure requeues WITHOUT burning the bounded budget —\n/// its pacing comes from the global breaker window (`breaker_window`), and\n/// the stream recovers once the wedged seat clears (T5's not-abandonment\n/// discriminator). A `Transient` keeps the ×3 budget (D3 unchanged).\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn outcome_transition(\n    outcome: &DispatchOutcome,\n    attempts: u32,\n    now: Instant,\n    breaker_window: Duration,\n) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(msg) => match classify_failure(msg) {\n            FailClass::SeatBlocked => ClaimState::Retry {\n                attempts,\n                next_at: now + breaker_window,\n            },\n            FailClass::Transient => {\n                let attempts = attempts + 1;\n                if attempts >= MAX_DISPATCH_ATTEMPTS {","truncated":true,"truncatedBy":"lines","totalLines":1350,"totalBytes":4927,"outputLines":125,"outputBytes":4927,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"enum FailClass {\n…\n    Transient,\n    /// The stream's subscriber seat is poisoned or still draining — retrying\n    /// immediately would manufacture another replay victim.\n    SeatBlocked,\n}\n\n/// Classify a worker failure message (the seat-blocked shapes are the two\n/// strings the nethost subscriber discipline emits).\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn classify_failure(msg: &str) -> FailClass {\n    if msg.contains(\"subscriber busy\")\n        || msg.contains(\"replay halted\")\n        || msg.contains(\"lease canceled\")\n    {\n        FailClass::SeatBlocked\n    } else {\n        FailClass::Transient\n    }\n}\n\n/// Whether an opener-query error is the OLD broker's unknown-verb answer —\n/// the ONLY error class that may fall back to the ring peek (ADR-0038\n/// Amendment fix 1). `send_error` on an unknown kind replies exactly\n/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n/// wire fact. Everything else (transport timeout, EOF, poison) is a\n/// transient — Failed → bounded requeue, never a second replay subscriber.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn is_unsupported_verb(e: &io::Error) -> bool {\n    e.to_string().contains(\"unknown command kind\")\n}\n\n/// The dispatcher's GLOBAL circuit breaker (ADR-0038 Amendment fix 5): a\n/// seat-blocked failure trips it — claiming pauses dispatcher-wide for a\n/// doubling window (base 2s, cap 30s), so retry machinery cannot hammer a\n/// wedged conn with fresh replay subscribers. Any successful serve resets it.\n/// PURE (instant-in, decision-out) — unit-tested trip/gate/reset.\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n#[derive(Debug, Default)]\nstruct CircuitBreaker {\n    until: Option<Instant>,\n    consecutive: u32,\n}\n\nconst BREAKER_BASE: Duration = Duration::from_secs(2);\nconst BREAKER_CAP: Duration = Duration::from_secs(30);\n\nimpl CircuitBreaker {\n    /// A seat-blocked failure landed: open (or extend) the window.\n    fn trip(&mut self, now: Instant) -> Duration {\n        let window = (BREAKER_BASE * 2u32.saturating_pow(self.consecutive)).min(BREAKER_CAP);\n        self.consecutive = self.consecutive.saturating_add(1);\n        self.until = Some(now + window);\n        window\n    }\n\n    /// Whether claiming is currently paused.\n    fn open(&self, now: Instant) -> bool {\n        self.until.is_some_and(|t| now < t)\n    }\n\n    /// Real progress (a Served outcome): close and reset.\n    fn reset(&mut self) {\n        self.until = None;\n        self.consecutive = 0;\n    }\n}\n\n/// Keyed dispatch telemetry record (ADR-0038 Amendment fix 9,\n/// REQ-DISPATCH-HYGIENE-TELEMETRY): every event names generation, stream,\n/// family (once classified), attempt, and conn — the field-discriminator\n/// surface (poison-window census, per-generation attach counts). PURE\n/// formatting so key completeness is a unit fact.\nfn dispatch_event(\n    gen: u64,\n    stream_id: u64,\n    family: Option<StreamFamily>,\n    attempt: u32,\n    conn_id: u64,\n    event: &str,\n    detail: &str,\n) -> String {\n    format!(\n        \"DISPATCH_EV: gen={gen} stream={stream_id} family={} attempt={attempt} conn={conn_id} event={event} detail={detail}\",\n        family.map_or_else(|| \"unclassified\".to_string(), |f| format!(\"{f:?}\")),\n    )\n}\n\n/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n/// off). The unit-tested seam for the no-hot-loop ruling.\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\nfn should_claim(state: Option<&ClaimState>, now: Instant) -> Option<u32> {\n    match state {\n        None => Some(0),\n        Some(ClaimState::Retry { attempts, next_at }) if now >= *next_at => Some(*attempts),\n        _ => None,\n    }\n}\n\n/// PURE outcome transition: what a finished worker's claim becomes. Served\n/// and Unknown (unclassifiable) are TERMINAL — a terminal classification never\n/// hot-loops. A failure requeues with backoff until the bounded budget is\n/// spent, then goes terminal.\n/// A `SeatBlocked` failure requeues WITHOUT burning the bounded budget —\n/// its pacing comes from the global breaker window (`breaker_window`), and\n/// the stream recovers once the wedged seat clears (T5's not-abandonment\n/// discriminator). A `Transient` keeps the ×3 budget (D3 unchanged).\n// [impl->REQ-DISPATCH-CLAIM-RETRY]\n// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\nfn outcome_transition(\n    outcome: &DispatchOutcome,\n    attempts: u32,\n    now: Instant,\n    breaker_window: Duration,\n) -> ClaimState {\n    match outcome {\n        DispatchOutcome::Served(_) | DispatchOutcome::Unknown => ClaimState::Terminal,\n        DispatchOutcome::Failed(msg) => match classify_failure(msg) {\n            FailClass::SeatBlocked => ClaimState::Retry {\n                attempts,\n                next_at: now + breaker_window,\n            },\n            FailClass::Transient => {\n                let attempts = attempts + 1;\n                if attempts >= MAX_DISPATCH_ATTEMPTS {\n…\n                } else {\n…\n                }\n            }\n        },\n    }\n}","startLine":207,"lineNumbers":[207,null,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,null,335,null,340,341,342,343,344]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\dispatch.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1558,"totalBytes":4927,"outputLines":125,"outputBytes":4927,"shownRange":{"start":209,"end":333},"nextOffset":334}}},"isError":false,"timestamp":1784271781707}}
{"type":"message","id":"3aada049","parentId":"27267b27","timestamp":"2026-07-17T07:03:01.724Z","message":{"role":"toolResult","toolCallId":"call_Ls7Kw1rESvf7c7VozIhpMeA6|fc_0e15f89f5b7b4062016a59d3a4e19c8194bee965ab451c01ca","toolName":"read","content":[{"type":"text","text":"    /// A point-in-time clone of one subnet's registry (resolution input).\n    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\n        self.regs.lock().unwrap().get(subnet).cloned()\n    }\n\n    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\n    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process\n    /// readers. Best-effort: a failed snapshot write must not fail the merge\n    /// that triggered it (readers tolerate staleness by contract).\n    fn write_snapshots(&self) {\n        let regs = self.regs.lock().unwrap();\n        if regs.is_empty() {\n            return;\n        }\n        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {\n            return;\n        }\n        for (subnet, reg) in regs.iter() {\n            let Ok(json) = serde_json::to_vec_pretty(reg) else {\n                continue;\n            };\n            let _ = atomic_write(&self.snapshot_dir.join(format!(\"{subnet}.json\")), &json);\n        }\n        drop(regs);\n        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:\n        // the snapshot reader globs `<subnet>.json`, so this can never shadow\n        // a subnet (even one literally named \"heard\").\n        let heard = self.heard.lock().unwrap();\n        if !heard.is_empty() {\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\n                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\n            }\n        }\n    }\n\n    /// The snapshot path for one subnet (the CLI reader's contract).\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\n        dir.join(format!(\"{subnet}.json\"))\n    }\n\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\n    /// last admitted-feed wall ms`.\n    // [impl->REQ-SUBNET-1]\n    pub fn heard_path(dir: &Path) -> PathBuf {\n        dir.join(\"heard.meta\")\n    }\n\n[Showing lines 729-773 of 2238. Use :774 to continue]"}],"details":{"truncation":{"content":"    /// A point-in-time clone of one subnet's registry (resolution input).\r\n    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\r\n        self.regs.lock().unwrap().get(subnet).cloned()\r\n    }\r\n\r\n    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\r\n    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process\r\n    /// readers. Best-effort: a failed snapshot write must not fail the merge\r\n    /// that triggered it (readers tolerate staleness by contract).\r\n    fn write_snapshots(&self) {\r\n        let regs = self.regs.lock().unwrap();\r\n        if regs.is_empty() {\r\n            return;\r\n        }\r\n        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {\r\n            return;\r\n        }\r\n        for (subnet, reg) in regs.iter() {\r\n            let Ok(json) = serde_json::to_vec_pretty(reg) else {\r\n                continue;\r\n            };\r\n            let _ = atomic_write(&self.snapshot_dir.join(format!(\"{subnet}.json\")), &json);\r\n        }\r\n        drop(regs);\r\n        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:\r\n        // the snapshot reader globs `<subnet>.json`, so this can never shadow\r\n        // a subnet (even one literally named \"heard\").\r\n        let heard = self.heard.lock().unwrap();\r\n        if !heard.is_empty() {\r\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\r\n                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// The snapshot path for one subnet (the CLI reader's contract).\r\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\r\n        dir.join(format!(\"{subnet}.json\"))\r\n    }\r\n\r\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\r\n    /// last admitted-feed wall ms`.\r\n    // [impl->REQ-SUBNET-1]\r\n    pub fn heard_path(dir: &Path) -> PathBuf {\r\n        dir.join(\"heard.meta\")\r","truncated":true,"truncatedBy":"lines","totalLines":1510,"totalBytes":1934,"outputLines":45,"outputBytes":1934,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    /// A point-in-time clone of one subnet's registry (resolution input).\r\n    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {\r\n        self.regs.lock().unwrap().get(subnet).cloned()\r\n    }\r\n\r\n    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`\r\n    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process\r\n    /// readers. Best-effort: a failed snapshot write must not fail the merge\r\n    /// that triggered it (readers tolerate staleness by contract).\r\n    fn write_snapshots(&self) {\r\n        let regs = self.regs.lock().unwrap();\r\n        if regs.is_empty() {\r\n            return;\r\n        }\r\n        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {\r\n            return;\r\n        }\r\n        for (subnet, reg) in regs.iter() {\r\n            let Ok(json) = serde_json::to_vec_pretty(reg) else {\r\n                continue;\r\n            };\r\n            let _ = atomic_write(&self.snapshot_dir.join(format!(\"{subnet}.json\")), &json);\r\n        }\r\n        drop(regs);\r\n        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:\r\n        // the snapshot reader globs `<subnet>.json`, so this can never shadow\r\n        // a subnet (even one literally named \"heard\").\r\n        let heard = self.heard.lock().unwrap();\r\n        if !heard.is_empty() {\r\n            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {\r\n                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);\r\n            }\r\n        }\r\n    }\r\n\r\n    /// The snapshot path for one subnet (the CLI reader's contract).\r\n    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {\r\n        dir.join(format!(\"{subnet}.json\"))\r\n    }\r\n\r\n    /// The heard-map mirror's path (the CLI reader's contract): `node hex →\r\n    /// last admitted-feed wall ms`.\r\n    // [impl->REQ-SUBNET-1]\r\n    pub fn heard_path(dir: &Path) -> PathBuf {\r\n        dir.join(\"heard.meta\")\r\n    }","startLine":729,"lineNumbers":[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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2238,"totalBytes":1934,"outputLines":45,"outputBytes":1934,"shownRange":{"start":729,"end":773},"nextOffset":774}}},"isError":false,"timestamp":1784271781724}}
{"type":"message","id":"98c0194b","parentId":"3aada049","timestamp":"2026-07-17T07:03:14.607Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming CPU thread sources**"},{"type":"toolCall","id":"call_GEoGDECUqWAuqOG4qbeWYzj2|fc_0e15f89f5b7b4062016a59d3ac60048194b1ae14874f82b9cd","name":"irc","arguments":{"i":"Reporting brain root cause","op":"send","to":"Main","message":"Brain RCA is source-proven: brainproc.rs:63 BRAIN_HEARTBEAT=500ms; run_brain heartbeat at 259-297 calls write_ready every tick; write_ready 365-373 calls current_exe_hash; 381-384 does current_exe + fs::read(entire running spt.exe) + SHA-256. Thus rate = 2 × exe length/s; observed 63.91 MiB/s predicts 31.955 MiB deployed exe exactly. Fresh Vec + SHA twice/sec explains sustained brain CPU. Comment at 376-380 falsely says one exe read per brain start. Registry poisons are separate: dispatcher 100ms table poll, workers; registry feed subscribe/read-to-EOF dispatch.rs:970-1048; broker SubscriberSeat writer halts on first failed SharedSend nethost.rs:119-218 with 15s deadline broker.rs:167-188. Repeated 15s poison means brain IPC subscriber not draining; cannot generate 64 MiB/s file reads. I’ll yield full ranked probes/citations.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11051,"output":351,"cacheRead":118272,"cacheWrite":0,"totalTokens":129674,"reasoningTokens":74,"cost":{"input":0.055255000000000006,"output":0.01053,"cacheRead":0.059135999999999994,"cacheWrite":0,"total":0.124921}},"stopReason":"toolUse","timestamp":1784271781764,"responseId":"resp_0e15f89f5b7b4062016a59d3a66ecc819492ab6c72498de788","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d3aa17b481948b13ece45fb22748","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdOsuBHd7JSBX2u2I_iEUDrAhUQCh7GC1tmsz7tdATNvn2GVNeZ1aqzRJpcaID1Mo1SrLrhMQLK7v076_cVhfu08mnYI91d6HrUWRVk6J71IjBkkVs7nnn66wSwrrAQGIOANesrw5Lnlwu4p97WFZzRImdtm0KuO4PVNxGiKDJDlPAqQE_LWgJlyzbEiwcxMcyFhApLlgEsfJRKL-WtZqXVqakUanrrP1GfHfZrDaWHGb8ZyQDL1RThRDkW_IBcCyIXx1qBDMiN1NhRYXSKWUAo-EENL5JUIKpZTfww2TbeweSiXimLvF88l3BBIoiqZF7gVkDHtaLZZFgOtKxog70xW3QrgHidhHuoUFf0PCUEW4BcEJBpGz-TMlRfVsdPYUaYyNjxJHAh4r7fyYhzjKkrkj7kOMBBCB_FfX0zsGox01NB5IxmuX6i20A168O2y7Ma6DuBaAAKAnt28WnksHueBtGj-VDcn1ibuL5LkDUg9X9puDfPRdbzLUIVwoD-C_LHo6RIvmqWxqqYqAZHZmWDHV2DCL9UAdQMvVXGVJu6rl52QtICfomxpmK9BpMwYTbSW2JQhwZtzDJukw8DkY_HxGehp89YGk9O0vs-uglxsQdhgvkpSs9ZB1O_0s-GlKyNV0V77Kv_TcAAsT9JnmqhvcTvHHRsWso1K91d4cpyag4ntSoNUklDVtIYnO_sHrDlaWiWfIL19voqtNpGO1biDfxGBUDwiEiGH2G7k9x5lZNOeZ4v1unzz_aN5AxbTsAt7pNTzSq2ymI2_FNPKqcONNeIwZQo6TTYbWx4t99qWlxrxIHP39_lK3kYri85oMGSxm8pT_O_xMabRLZnqCLkPE0aLHVFzAlFxW1L2-rUiWaEd1bEO8skfoPIvONjWcWAVKPU2G5vOhuEZO6s6eKFKMQ87V0-4CTzzi0vnb6RaW45IpNt-A-mATJ_ncTXrtgezPloOrIq2ftB7AyUATy_O7_34C5w9OCUnRZN3Hs2KciIDsJL-R7ZoAbWzzCVxq7avadM7puPZjsNuPGTxZ0bXrzMieSr2Tc_NZw1WFDQcjnvey3PMzDzBlaotCkWpxxSBWgfSCGI9dwHZ31oq_-AMPHIko6448-J7XDySDE2_hnFnarP8Jy5PC9HJ52IaD_bLOhsmhKfLfiWIM8ENIpTc99Fs7Og5vAj4fhyogEnmDr0zPpD_R2IIn6JmBse6GLSCtpUB5yRbB1JbCclB4ZOD-xrF9D5uIBvAUAFWG1-cErvBTm2Ny6NoveGyCIZlNblSFEJk3N_VcfgnaYwFG0DRczu2O2rZdqbLla80nSLZyUpGn3N17fiLJ6N9JaZdeg8P5VA0TQxze-f9iAvn_WMOwxTZjsWBwfc4OHSQ4TQIZHE=","summary":[{"type":"summary_text","text":"**Confirming CPU thread sources**"}]},{"id":"fc_0e15f89f5b7b4062016a59d3ac60048194b1ae14874f82b9cd","type":"function_call","status":"completed","arguments":"{\"i\":\"Reporting brain root cause\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Brain RCA is source-proven: brainproc.rs:63 BRAIN_HEARTBEAT=500ms; run_brain heartbeat at 259-297 calls write_ready every tick; write_ready 365-373 calls current_exe_hash; 381-384 does current_exe + fs::read(entire running spt.exe) + SHA-256. Thus rate = 2 × exe length/s; observed 63.91 MiB/s predicts 31.955 MiB deployed exe exactly. Fresh Vec + SHA twice/sec explains sustained brain CPU. Comment at 376-380 falsely says one exe read per brain start. Registry poisons are separate: dispatcher 100ms table poll, workers; registry feed subscribe/read-to-EOF dispatch.rs:970-1048; broker SubscriberSeat writer halts on first failed SharedSend nethost.rs:119-218 with 15s deadline broker.rs:167-188. Repeated 15s poison means brain IPC subscriber not draining; cannot generate 64 MiB/s file reads. I’ll yield full ranked probes/citations.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_GEoGDECUqWAuqOG4qbeWYzj2","name":"irc"}]},"duration":12842.20000000298,"ttft":4362.319000005722,"contextSnapshot":{"promptTokens":129323,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GEoGDECUqWAuqOG4qbeWYzj2|fc_0e15f89f5b7b4062016a59d3ac60048194b1ae14874f82b9cd","toolName":"irc","startedAt":"2026-07-17T07:03:14.608Z","intent":"Reporting brain root cause"},"id":"9da0dca6","parentId":"98c0194b","timestamp":"2026-07-17T07:03:14.608Z"}
{"type":"message","id":"a4c05cdb","parentId":"9da0dca6","timestamp":"2026-07-17T07:03:14.608Z","message":{"role":"toolResult","toolCallId":"call_GEoGDECUqWAuqOG4qbeWYzj2|fc_0e15f89f5b7b4062016a59d3ac60048194b1ae14874f82b9cd","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"BrainCpuRca","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784271794608}}
{"type":"message","id":"4a2d3dde","parentId":"a4c05cdb","timestamp":"2026-07-17T07:05:14.610Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Designing detailed architecture field**\n\n**Analyzing thread activity and log patterns**\n\n**Verifying brain.ready write frequency and content**"},{"type":"thinking","thinking":"**Correlating pulse intervals and thread stacks**\n\n**Assessing broker thread behavior**"},{"type":"toolCall","id":"call_uXkAjjUBe4aDGdWT0AfjI9BE|fc_0e15f89f5b7b4062016a59d3c4c564819490c8dbed7f5d2a0d","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"## Ranked findings\n\n### 1 — Confirmed dominant brain read/CPU mechanism: the readiness heartbeat re-reads and hashes the entire running `spt.exe` every 500 ms\n\nThis is the only source mechanism that quantitatively explains the observed sustained **63.91 MiB/s brain reads**.\n\n* `crates/spt-daemon/src/brainproc.rs:63` sets `BRAIN_HEARTBEAT` to **500 ms**.\n* `crates/spt-daemon/src/brainproc.rs:259-297` sleeps one heartbeat, performs `brain.net_status()`, performs `brain.sessions()`, and then calls `write_ready(generation)` on every iteration.\n* `crates/spt-daemon/src/brainproc.rs:365-373` has `write_ready` call `current_exe_hash()` before atomically rewriting `brain.ready`.\n* `crates/spt-daemon/src/brainproc.rs:381-384` implements `current_exe_hash()` as `std::env::current_exe()` followed by `std::fs::read(exe)` of the **entire running executable into a fresh `Vec<u8>`**, followed by SHA-256 over all bytes.\n* Therefore steady-state brain file-read throughput is\n  `2 heartbeats/s × deployed_spt_exe_size`.\n  The observed 63.91 MiB/s predicts a deployed executable size of **31.955 MiB**. That is an unusually exact fit, not merely an order-of-magnitude match.\n* The same path allocates a fresh executable-sized buffer and hashes it twice per second. That explains sustained one-core CPU as well as process I/O read bytes. Windows process Read Bytes includes cached `ReadFile` traffic, so this need not imply 64 MiB/s of physical disk media traffic.\n* The source comment at `brainproc.rs:376-380` says “One exe read + hash per brain start,” but the call at `brainproc.rs:297` makes it one read + hash **per heartbeat**. The initial ready call at `brainproc.rs:221` is indeed once at startup; the later heartbeat call is the regression.\n* `write_ready` also atomically rewrites the small `brain.ready` JSON every 500 ms. `crates/spt-store/src/atomic.rs:57-79` shows that this is a tmp-file write plus rename. It can explain a high operation count and visible metadata/write churn, but not tens of MiB/s by itself.\n\n**Falsifiable live probes**\n\n1. Compute the exact prediction without changing process state:\n   ```powershell\n   $p = Get-Process -Id 58272\n   $bytes = (Get-Item $p.Path).Length\n   [pscustomobject]@{\n     Path = $p.Path\n     Bytes = $bytes\n     MiB = $bytes / 1MB\n     PredictedReadMiBPerSec = 2 * $bytes / 1MB\n   }\n   ```\n   Confirmation is `PredictedReadMiBPerSec` approximately **63.91**. A materially different executable size falsifies this as the complete byte explanation.\n2. In Sysinternals Process Monitor, filter `PID is 58272`, `Operation is ReadFile`, and `Path is <the value of $p.Path>`. Over 10 seconds, expect about **20 full sequential reads**, totaling about `20 × executable length`, spaced near 500 ms. Filter out this one path and recompute brain read bytes; the sustained ~64 MiB/s should disappear.\n3. Add a Process Monitor filter for `brain.ready`. Expect tmp create/write/rename publication at approximately 2 Hz, synchronized with the executable reads. The ready JSON’s `exe_hash` should remain constant while its last-write timestamp advances.\n4. Capture sampled CPU stacks in WPA/Windows Performance Recorder or Process Explorer’s active-thread stack view. The continuously active brain thread should spend samples under the executable read and SHA-256 path. The falsifier is sustained 15.5% CPU entirely on other thread stacks after excluding this path.\n\n### 2 — Plausible secondary, session-count-scaled activity: one 5-second pulse thread per hosted live endpoint; transcript reads are gated, not unconditional\n\nThis explains why a five-second activity census can see roughly one active brain thread per hosted session, but it does **not** fit a flat 63.91 MiB/s nearly as well as mechanism 1.\n\n* `crates/spt-daemon/src/livehost.rs:637-639` starts one `run_pulse_loop` thread per hosted live lifecycle.\n* `crates/spt-daemon/src/config.rs:19-25` gives the default pulse period as **5 seconds**, configurable via `daemon.json`.\n* `crates/spt-daemon/src/lifecycle.rs:701-738` runs each endpoint on that configured deadline grid. Each due tick checks the manifest and calls `pulse_tick`.\n* `crates/spt-daemon/src/lifecycle.rs:465-490` reads and hashes the endpoint adapter’s small `manifest.toml` to detect live updates.\n* `crates/spt-daemon/src/lifecycle.rs:585-666` reads resting/info state, stamps active recency, enumerates configured commune/signoff drop directories, and consumes an echo gate.\n* Crucially, history/transcript access is **not performed on every pulse**. `lifecycle.rs:647-666` calls `fire_echo` only when `take_echo_gate(...)` succeeds. `lifecycle.rs:843-860` fetches history only inside that echo path.\n* When it does fire, `crates/spt-live/src/history.rs:103-129` can read an entire adapter-located raw transcript (`std::fs::read`) or entire perch `history.log` (`read_to_string`), then normalize it. Thus large transcript files can produce bursty large reads, but only on armed echo events, not on quiet five-second ticks.\n\n**Falsifiable live probes**\n\n1. In Process Monitor, exclude the running `spt.exe` path, then group remaining `ReadFile` bytes by path. If pulse history is material, large reads will name an adapter raw transcript or `<perch>\\history.log`, recur on the configured pulse grid, and correlate with echo-gate consumption. If those paths are absent or tiny, pulse threads explain the “five active threads” census but not the byte rate.\n2. Compare `daemon.json`’s `pulse_period_ms` with timestamps of reads to perches’ `info.json`, `resting.json`/rest records, adapter `manifest.toml`, and drop directories. Quiet-session metadata should recur around that period; raw transcript reads should occur only when an echo gate was armed.\n3. Inspect active-thread stacks: normal quiet pulse threads should appear briefly in `pulse_tick`, info/rest reads, directory enumeration, and sleeps. A thread continuously reading one transcript rather than waking once per grid point would falsify normal pulse behavior.\n\n### 3 — Project index: sub-second polling exists, but it is one cheap directory query; repository/session scans are event-driven or every five minutes\n\n* `crates/spt-daemon/src/projwriter.rs:44-53` defines a **500 ms** invalidation poll, **250 ms** debounce, and **300,000 ms / 5 minute** periodic reconcile.\n* `projwriter.rs:622-683` runs exactly one writer thread. On ordinary idle ticks it calls only `projinval::pending_at(...)` and continues.\n* `crates/spt-store/src/projinval.rs:90-95` proves the idle poll is one `read_dir` of `$SPT_HOME/index/invalidations`, short-circuiting after the first entry. It does not read repositories, transcripts, logs, or the project index on every 500 ms tick.\n* When invalidations exist, `projinval.rs:104-119` drains each tiny invalidation JSON file and deletes it, coalescing the batch.\n* At boot, on an invalidation, or at the five-minute backstop, `projwriter.rs:294-348` invokes one branch-tip enumeration of the context store’s bare seed Git repository and scans a `p-*` tree only when that branch tip changed.\n* `projwriter.rs:350-450` enumerates perches and reads each targeted `<perch>/sessions.log`; it derives project identity from recorded cwd values. It is not scanning the project worktree’s contents.\n* `crates/spt-store/src/sessions.rs:28-32,98-107` bounds every `sessions.log` at **64 rows**, so this input cannot grow without bound.\n* The unchanged-fingerprint no-work gate at `projwriter.rs:307-320` exits before tree/perch composition when there is no changed generation and no scoped work.\n\n**Falsifiable live probes**\n\n1. Process Monitor should show `QueryDirectory`/directory metadata traffic against `$SPT_HOME\\index\\invalidations` at about 2 Hz, with negligible bytes. It should not show repository worktree or transcript reads on that cadence.\n2. Watch `$SPT_HOME\\index\\project-index-stats.json`. `last_run_ms` and the `last_cycle` counters should change only after an invalidation/boot reconcile or about every five minutes—not every 500 ms. A continuously advancing `last_run_ms`, nonzero tree scans every tick, or an invalidation file that never drains would identify a separate malfunction.\n3. During a periodic full reconcile, expect one seed-repo branch enumeration, at most one tree scan per changed `p-*` branch, and bounded `sessions.log` reads. Reads of arbitrary repository files or raw transcripts during this phase would falsify the source contract and identify an untraced caller.\n\n### 4 — Dispatcher/registry streams: 100 ms IPC polling and 15-second poison windows can explain thread/telemetry symptoms, but not sustained brain file reads\n\n* `crates/spt-daemon/src/dispatch.rs:47-49` sets the dispatcher’s stream-table poll to **100 ms**.\n* `dispatch.rs:429-588` issues `brain.net_streams()` each tick, skips local/empty rows, claims at most eight workers, and keeps in-flight/retry/terminal state in a per-generation map. The idle path is broker IPC plus sleep, with no filesystem scan.\n* `dispatch.rs:1197-1207` starts exactly one dispatcher loop.\n* `dispatch.rs:970-1048` shows the Registry arm: load the small subnet/roster policy once per feed, subscribe from sequence zero, block on `brain.read_event`, decode rows, apply them, and return on EOF. It is event-driven, not a polling reader.\n* `crates/spt-daemon/src/registryhost.rs:69-80` loads subnet and roster stores once per inbound registry feed.\n* `registryhost.rs:241-344` merges rows under the in-memory registry lock, drops the lock, stamps heard state, and writes snapshots. There is no lock self-deadlock in these apply paths.\n* `registryhost.rs:738-765` serializes current in-memory registries to `$SPT_HOME/identity/registry/<subnet>.json` plus `heard.meta` after an admitted feed. This is the main registry-feed disk I/O. It is proportional to registry snapshot size and feed frequency, not transcript/repository size.\n* `crates/spt-daemon/src/nethost.rs:119-218` gives every installed stream subscriber a bounded queue and its own writer thread. Replay is written first, then live frames; the thread halts on its first `SharedSend::write` failure and marks the seat poisoned.\n* `crates/spt-daemon/src/broker.rs:167-188` fixes the production brain write deadline at **15 seconds**. Therefore a poison appearing almost exactly 15 seconds after subscriber attach is a blocked broker→brain connection write, not a registry scan cadence.\n* `nethost.rs:505-572` refuses a replacement only while a poisoned prior subscriber writer is still draining; every install gets a new lease. `dispatch.rs:209-333` classifies subscriber-busy/replay-halted/lease-canceled failures as seat-blocked and applies a global **2, 4, 8, 16, then 30 second capped** breaker, without burning the transient attempt budget.\n* On successful service, `dispatch.rs:492-510` calls `net_stream_retire`; `crates/spt-daemon/src/nethost.rs:1835-1840` removes the row from future stream-table enumeration. Thus the normal lifecycle is unique feed stream → subscribe → data → EOF → Served → retire. A single served stream should not be replayed indefinitely in one generation.\n\n**Interpretation of the observed Registry subscriber poison telemetry**\n\nRepeated 15-second Registry poison is real abnormal IPC backpressure and can create broker-side writer/watchdog threads and dispatch retries. It is, however, orthogonal to the 64 MiB/s brain `ReadFile` total: registry handling reads small policy files and writes snapshots; it never reads the executable, repository worktree, or transcript. The current-executable heartbeat continues on the brain main thread even while Registry workers are blocked.\n\n**Falsifiable live probes**\n\n1. Correlate `CONN_LIFECYCLE` records. `crates/spt-daemon/src/conn.rs:229-275` emits stable `conn=<id>`, role facts, wall/monotonic stamps, attach, and poison events. For a deadline poison, the same conn should show `stream-sub-attach`, then `CONN_WRITE_POISONED`/`stream-sub-writer-poison` about 15,000 ms later. A fast `CONN_WRITE_RETIRED` instead means organic peer death, not a 15-second black hole.\n2. Group `DISPATCH_EV` by `(gen, stream, family, attempt, conn)`. Unique Registry stream IDs about every peer registry cadence indicate new periodic feeds; repeated attempts for the **same** stream with breaker windows 2→4→8→16→30 seconds indicate recovery from a poisoned subscriber; a Served event followed by that same stream reappearing indicates retirement failed or an older broker lacks the retire verb.\n3. Process Monitor should show registry reads limited to subnet/roster policy files and writes to `identity\\registry\\*.json`/`heard.meta`. If the 64 MiB/s is attributed to registry, there must be comparably large named registry files and repeated full reads; source inspection predicts neither.\n4. Compare poison timestamps with thread count. Subscriber writer threads park on their queue while healthy and exit after clean detach or first failure. A monotonically increasing count without matching detach/writer-exit lifecycle events would establish a thread-retention bug; a sawtooth count around feed arrival/15-second failures is bounded churn rather than a leak.\n\n### 5 — Peer registry pump: 200 ms scheduler/marker checks; full local advertisement every 30 seconds by default\n\n* `crates/spt-daemon/src/pump/mod.rs:96-102` uses a **200 ms** scheduler tick and a **5-second** pump heartbeat write throttle.\n* `crates/spt-daemon/src/config.rs:35-46` sets default registry/notif/sync/update cadences to **30 s / 15 s / 60 s / 3600 s**; registry eviction defaults to 300 s.\n* `pump/mod.rs:583-613` polls workers’ one-shot wake markers on every 200 ms scheduler tick and sleeps immediately when nothing is due.\n* `crates/spt-daemon/src/pump/registry.rs:120-155` consumes repair/advertise markers, performs eviction, rotations, and `advertise_local` once per due/forced registry round.\n* `crates/spt-daemon/src/registryhost.rs:399-632` then enumerates self perches, reads small state/info records, adapter registry data, and each bounded `sessions.log`, updates the in-memory registry, and writes snapshots.\n* `registryhost.rs:893-919` derives at most eight recent project IDs per endpoint from the bounded session ledger. It does not traverse those project directories or read their transcripts.\n\nThis can generate periodic metadata and registry JSON churn, especially after many endpoint transition markers, but the normal cadence is 30 seconds, not sub-second full scanning. The 200 ms path consumes marker existence/removal state; it does not repeatedly scan every perch.\n\n**Falsifiable live probe:** group filesystem activity into 200 ms versus 30-second buckets. Normal idle pump activity is a tiny heartbeat/marker check; perch, session-ledger, adapter-registry, and registry-snapshot traffic appears in a burst at the due/forced registry round. Full perch scans every 200 ms would falsify the expected scheduler behavior.\n\n## Bottom line\n\nThe brain’s sustained read rate has a direct numerical source match: **`write_ready` re-hashes the entire ~31.955 MiB executable twice per second**. The four-to-five hosted sessions add expected five-second pulse-thread activity, which explains the active-thread census but not the exact byte rate. Project index, dispatcher, and registry loops do contain 100–500 ms polls, but their idle work is IPC or one directory/marker query; their repository, ledger, and snapshot work is gated and much less frequent. Registry subscriber poisons are a separate, falsifiable 15-second SharedSend backpressure condition worth tracking for broker thread churn, not a candidate for 64 MiB/s of brain file reads.","files":[{"path":"crates/spt-daemon/src/brainproc.rs","description":"Primary RCA. `BRAIN_HEARTBEAT` is 500 ms (line 63); the steady loop calls `write_ready` every heartbeat (259-297); `write_ready` calls `current_exe_hash` (365-373), which reads and SHA-256 hashes the entire running executable (381-384). Also wires live host, project writer, dispatcher, and peer pump."},{"path":"crates/spt-store/src/atomic.rs","description":"Shows each 500 ms ready publication is a non-durable unique tmp write plus atomic rename; explains small write/metadata churn alongside the executable reads."},{"path":"crates/spt-daemon/src/livehost.rs","description":"Hosts one lifecycle pulse thread per online live endpoint and performs a full live-host reconcile every 5 seconds; useful for explaining 4-5 intermittently active brain threads."},{"path":"crates/spt-daemon/src/lifecycle.rs","description":"Defines configured pulse-loop semantics, per-tick state/drop scans, manifest hashing, and the one-shot echo gate. History/transcript fetch occurs only inside `fire_echo`, not on every quiet pulse."},{"path":"crates/spt-live/src/history.rs","description":"Exact potentially large transcript reads: full raw transcript for `locate_normalize`, or full perch `history.log` for native history. Both are invoked only when lifecycle history is requested."},{"path":"crates/spt-daemon/src/projwriter.rs","description":"Project-index writer: 500 ms invalidation directory poll, 250 ms debounce, five-minute periodic reconcile; branch-tip fingerprint gate and changed-branch-only seed-repo tree scans; bounded session-ledger reads during composition."},{"path":"crates/spt-store/src/projinval.rs","description":"Proves the idle project-index poll is one short-circuiting `read_dir`; draining reads/deletes only queued tiny invalidation files."},{"path":"crates/spt-store/src/sessions.rs","description":"Defines `<perch>/sessions.log` and its 64-row hard bound, ruling out unbounded session-ledger scan volume in registry/project-index loops."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"100 ms stream-table IPC poll; bounded eight-worker claim pool; keyed terminal/retry state; registry subscribe/read-to-EOF arm; success retirement; 2-to-30-second seat-blocked circuit breaker and discriminating telemetry."},{"path":"crates/spt-daemon/src/nethost.rs","description":"StreamLog subscriber lifecycle: per-seat bounded writer queue/thread, replay-before-live, halt on first failed SharedSend write, poison/busy rules, fresh leases, EOF, detach, and retired stream removal."},{"path":"crates/spt-daemon/src/broker.rs","description":"Defines the 15-second production brain write deadline and brokers stream subscribe/retire operations. The exact 15-second poison interval is a write deadline, not a registry cadence."},{"path":"crates/spt-daemon/src/conn.rs","description":"Bounded physical broker connection writes, watchdog, stable connection IDs, and `CONN_LIFECYCLE` / `CONN_WRITE_POISONED` / `CONN_WRITE_RETIRED` telemetry needed to distinguish a deadline black hole from organic disconnect churn."},{"path":"crates/spt-daemon/src/pump/mod.rs","description":"Peer-pump scheduler: 200 ms idle tick, five-second heartbeat write, config-driven workers, and due-only round loads."},{"path":"crates/spt-daemon/src/pump/registry.rs","description":"Registry worker semantics: marker-triggered or cadence-triggered pre-round sweep and one fire-and-forget feed stream per peer/subnet."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"Inbound registry policy/apply/snapshot paths and outbound `advertise_local`; reads bounded perches/session ledgers and writes registry JSON snapshots, with no repository/transcript scan."},{"path":"crates/spt-daemon/src/config.rs","description":"Production default cadences: pulse 5 s, registry 30 s, notif 15 s, sync 60 s, update 3600 s, registry eviction 300 s."}],"architecture":"The deployed brain is a multi-threaded orchestrator around one 500 ms main heartbeat. The main thread owns a long-lived broker IPC client and, every heartbeat, requests net status and sessions, then republishes readiness. Readiness publication currently recomputes the resident executable hash by reading the entire binary, creating the dominant sustained read/CPU path. Separate brain threads host: (1) live-host reconciliation every 5 seconds plus one configured pulse loop per hosted endpoint; (2) one project-index writer whose idle loop performs a 500 ms invalidation-directory query and whose expensive reconcile is event/five-minute gated; (3) one dispatcher polling the broker’s in-memory stream table every 100 ms and spawning at most eight event-blocking workers; and (4) one peer pump with a 200 ms scheduler and 15/30/60/3600-second worker cadences. The dispatcher and peer pump share an `Arc<RegistryHost>`: outbound registry rounds scan bounded local state and push one finite stream per peer/subnet, while inbound dispatcher workers subscribe, merge, snapshot, wait for EOF, and retire served rows. Broker-owned StreamLogs attach each subscriber to a bounded dedicated writer; a broker→brain write blocked for 15 seconds poisons that physical connection and drives the dispatcher’s capped breaker. This IPC poison path can account for broker thread churn and repeated Registry telemetry, but it is independent of the brain main thread’s deterministic twice-per-second executable read/hash."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4255,"output":5763,"cacheRead":125440,"cacheWrite":0,"totalTokens":135458,"reasoningTokens":769,"cost":{"input":0.021275000000000002,"output":0.17289000000000002,"cacheRead":0.06272,"cacheWrite":0,"total":0.25688500000000003}},"stopReason":"toolUse","timestamp":1784271794613,"responseId":"resp_0e15f89f5b7b4062016a59d3b2e88c8194891162292cc784b3","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0e15f89f5b7b4062016a59d3b44b448194b6e39993bcca79ea","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdPC0-RyMbnKZUTzZc-H3buSxs5OGQE3LAF6s-33jCPk7gacrxdD0vZjpgH3ack2fgJ-hqI3kQ0wPhYYRYOXfE4LndT04KbUUYSqQew4XcsxD8O9-f3yil5SxZUOp5xZquLWl422ML8iamhMbFVMMbSyFRWHBLYNvaawjGVXzP19pry8kMwEPNrYdhAnUsUsspQTUlVP1XvVnVwM0ZFYRroxPAcQxjdLLrodWMAS0om9kj7Rs6n14WCjt5QxQlIVk3hrZQlWn_elpzyK3nuWR5y4PppCq7nQuVxDD6y8kS0yjb7m5GrVf3EVrJE5bFfR5d96jt4HWCx_1qg3JFk3Nh-2Mlpc1Ye7wh_f3k5FzQ61IfO3sjV6OBIVRUkyQGTS7NTo0I_gF9RRTIZiBbcWPQgf9EJ_1p2U3xgcvRjFtfh3ylC6kEyXhbuFp3Cv94RtMa0K2VX-94oJqEQiY5SbhMkX0-t5wGsgTTXCaBmB6qKb2aHMBPPSIw9zCsFgVp_3TfEUUeH3W12JeP1Wi73FX4durlbeTEsf5cA1rYwovmA_1cSib9qKmdyIhNf8NGEIJCgpyxi-TDfJhoOyl0lbapbQdUfqt6BfiZqivO3UkZtF4qIArpM8AJip1QEXsxKZzh1W6_9Dhqmoh7Cwl_0Z6GqBbljMpQ4Wgl1jC-840GehjcNtSsK4eVJqkOPNjJwG5h4DYBDV3n33rfEOKhcg-C-lyPw8h994r_fvYwOtBwKF_nqLU5gnTUOPm1XFeyd01hA_QKsBUw41JUcjSnUb_qQTTDvuhwIW9DQkEwa8hvuS8-KGG6DSYOkAbdR8KjYx_4cDsBuOBbBFDLT7cUFqKQs7oKtoupGO6uSTtuJDCEwkioId4PD_VKkpD2K3O5RCLIIKCW6gKU2oi_lw7XL5VgXBaWJkmH7QWSDETDZXUB6-uY31PiJ7nyTMqWnA8YTVwVUong11Wy3me4vCIlioY3L0cLd8_TWw1p-znDGxpRtYFJAgKsU4_XBwetMUxvV4P4XPPmSxC5AnTPNqaKlEvu5vkdhNTr4r8wbUQ9PD-DIIbYjTY3w0THmiFLKIRN1UNbl5MTxBkMhsLrG17ITfpZGNU306Pr5lHUj_k1U6YbChyJaGXkSPzI4eM2G1cIntGlmk5r17HGjdxm9d4wGlnf0fGq1IJY0jF8Pl0dj4az2ho41A3ak_2woeEv266JhmI1OsI05bQfLhEt_QMRreRbkBPYUI4bcLiPiSZ7S-4lYNMPFB_o93-vT1zTchqK07VlRnnPwcV_ref8GvHdY_CAG12IUVs6s07yqVva6nFoaXsxmoloQiK1p15t8b0Q2gjyv7Fo6VfBABKUmgdzV2-Y3MJ-3zDxdvWLUDfg6Cex6H8k3_1DHzOn_wXVmuk4OAm2SXqfASo_LQnYffb_DPJpfyaAY0iQy_QXgmQ5ITRvmjEBBPt0k1hD3Men5BfuH_7ZhGedfnh4w7jcG3hQCAHSb9W77Tk91uVAYievbL-33OADDoRjT1CroFDzxZpEcE-PUqznkVsJ3VlqEUe3EPPAnaHJJkiGBJlCRhPHM5zBtcjXTQFCHQYFsXC8v9sTo8gJCzRsdOlmKJW--XbgQP2uBaVhWJm3kRo7EI0KAu7ENJBtgMxLKw7LM7N7CMTq_Y-bwo0gu01OppIwCgENQzNccezeGQzWwqAkJguEgK8YfuYw-3vtQ5_VrqS65d1UL40dSQ-rvvvaEpk7RrV5WkDLI8hqm-7s5JAn1Mnjpips-70y-8vwxbpIju4FIP6rILMysVYiTNuclMfxtpENhbXMNEREbCmwnlQOQj-PdxZkPQYBVsH0JkPGRBl7sFmMYgN0VINfHCMw56K9faAxcYOgvCLJs-Sc6y5HEwnjtmRU8dG3uIqxHLcOnxEk9J35KlkzI-EvuxgHxvjYdSP5dl203W59bAoAD2eXRIzloMx4vk0KcRiw07YSGVtoDU0seZRdImEMij6gBDBZEeBs1Tm2OQyc2zqLgVNsB8AM1PhLXczDzkvk3QKGMNEG73OkStOwRwtmjpzRZAutsg2NG3HzEKkBXYAEwH7RKIwfGkPlrgnEh_W3KwuWWvky4EBidmVDWuhaIcCZOW3plOa6ln6r7fr_RgnSldiEP6nD8yxt928AYdlyVNvfthHz5fd3OmXd14XOPoY5zp-4knsEuwrTe85791_OMED9AyrW2ULLXNZyEogEgz6IAPydak10ec2InesFNhRZ9aPv8zbDnYHHU8xfZDPezFah1GVw1I9Jk0r9yZ-FKxra59Vrj7923Klelbjf5FPD4Ks7rsyoowGyOMUktd8PLQXFArPs_LzzqkAUrJzPMgftmCRC6-U6AwTDnkE_G65y56I9LIG74lYTUhVswtPskx49hBEkpP1C_LrlDVDSa9JFhKIAR3ArEdsjk4UpcvnrsTtH1unYOms3x1YrPOEqZ7VEInbL4Mgbvj2cdB4KpKaYFq_vAoaG3Mdie6o59Lk62sNg_uDF4zWC-4oZuhqHBGxJ8b63jU9J2AaDoVwGRrC5hhKb9KPXyYAiBvo5_-dETSAMqHLsohv88x6_8GT4p7S4_jhHLXVMUdimcbCYuPqIM08y4A4b_miIi0sWMKIgh2MvkiTxY262U3VEcxl25Zcjm4SLDpjIjMJvbbjsfXKJCULizY1K8w0Dv6WIXxai9nMzYr3Sa7vozr7e9EVu-9AvNtr_aciRvJoeOz0YYjFuEHaKXmF6X1YRobplybUkq7pmEoQT928gZ3Sfvd-SCr_67miPF2Lph2Bl8Z8IZx2t1AQ5Q2tyQJV1iXoSYgPbmdiJiC6H2Q5ECnYOUZmNILn9RqNJHtMUM2TV5gDDRThJ-YqPaQ68FsBK11Sxu-KSl5lf7OuFVv2Rrc5FrODz87Lz0IqgUxB6MP6e6aQQODqCZT3RUtoBrFkNkzoZq8bDefyh6tN6C_9zUXtdbcZ1fZHuiOVRhg5KExGM_nu2Ex7AekjfjwUAdHCwpeANkuLVIHDh2y6yn7BJgSPtXzs7RTeiRDwSJTK0_bDe-QxJ7Vt3Cg4mVi8kuCgEVvnYmWMFftmqevQLMwqF4cOp5KXjHSgpDQcwB81egCK0Qe8CE-mvGtZH_BIu4AZ78TIS5tQaJ1pBynZV2KSC0cA8WjZkAcu4gHAgGFp0G9946bNTrOvm56IWfR_zSrgfMaqdhTgUz-AZqgCf2q9Cxzn5n6Tx06iC2ofk_lB8OhiKnD-O3E944lf5HvKC7Rtjo4f7MpxPda5KFarWBfp8Fg9AIoCDykxJFBttbjoJhDRXPvmqPJDW8BHyCq1LrCKxXCKBmFj9n61toSKMfhlyVqa-B8UrEwGLcta8BxgXVSoo-VK1ptBdfPLz8OeDX2Y6diZccSu4uT-KP3Yy4F2z3BpfzZTOXVh90UZYvCvfFhGWCma0Co6v5EiFj24ft_1YykAB0LJdgKgVQXrCfTzm9jHhm_vK0Q_WzoJcCYEqMWFZgqRXqEzVC2wK0ga_FvpC5htA7gbXFphEdoGVZ5lLFNe0GBsicWcCxDjmbfQHmUrpWmQ-9aGtVp7BC39pzIZcbJ0_zcBiZp7BjODexaCo8S7ZRI2efJJDP6tVbpXlobZ2oQ5CQByhSctccntwUYIhpRWTtFnwtOPT3pU4D1I981nnW5SnLUJ3-KWExESB-eTzQrfDBCUWoV0DW5xzSn7fSCdVoSAXLZel8fGHsEkdwUXq5RZmpegjoHEp8LKpQk0QmzX9uM_ze34tmTS4HuxyAcdQ-wMWVUrNxgV3WfCgiGVZgJg1ZrT5-zzdR3D60EqyKASK0w2IZheXcJ_2xJjFVh","summary":[{"type":"summary_text","text":"**Designing detailed architecture field**"},{"type":"summary_text","text":"**Analyzing thread activity and log patterns**"},{"type":"summary_text","text":"**Verifying brain.ready write frequency and content**"}]},{"id":"rs_0e15f89f5b7b4062016a59d3c21b488194a98a313441f0ea6c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdPEZQueNzN9LPWAFnWI-ntDhSMjsR2LPUksdRyGKD3o672GcriqAtMmbyjVpzKkGOQgeN-dRGCV-U51BW6yb_GOiExXF4yARu8Az7mxAMt2b9AGrtEn4Dp9oecJhenptzeKwSJPuMRBaw4fZcAkBys4_piLGkiiv3LntXyXvhGMgBTseLOIefj3qIzMUXrVRJ7a2ZZyFnFoDa2Sffj4Ryx_w3Op0C18jaag94tpqWuAjLZii9VI8ubL3dy5KFI-y8z8H-C8yc96_LLndcCHQLYheaK6EtfrOPMwNIq_EOoIzd0DjsZMVCmhF5QDuSxeQtwkp8_8pcOVpBjkGyHnN8v_SQhwLMMz1ai7ED0JxTREK8txNS1nLYE9olKp6FrHqhx1gqR2pLPmll4lb88elOGM02zj93UIgnKz_ls-BoL800i7EthnGFZfJ8k1a8OvpW_vLMqivT7417opDruaayb4AFgfe6RMx5kMwxUgt8QLUf_d_d6yAP0I8NgVrm7zuN5SQg9MofYiKtVvhjRFh75thDva3xwvBpjGbLHTfjPx2-LQKItd_jj-7YFeCjMUZ52CHtMdfM6Zsjl5Yuiu4aawCYfNlkFvJTb71xy9TJv2pP6pFZDdbmMP5H1yzsIvYZsA_0I7UnHsNqoa4L0kdFwnTiucypfD4XK-x5HiSBpAO6ysRpM_qtzHAH24KVLyHz7QRaBKJgSItAxsPRNwuk9UoElEgi7-lzhiie_mFpqiYVoWS0GbVF-pRu03_ojJkBwiW_NAkBE6gR5SFDuZT12R0ypHjH5oKeah7WZR4sFUUFPOWj3TWPcMemJtH_cTK6ISYQs7pipPPDGMvjNMjDgMMM-xemGwXbfw1gZwhZjIKQTwbQgbw21qKj6_d8SqewktrZTAoU28OvSzQQ1AA9oW3xDtD-jDgyvROQ-ddRIz3vTvz3W77jc_i-ehrqNnL6iGBa3Giv437YLU1IWMVZYzYc1YTj0jIXunuFeiE9pjob05NLDTbnP8RUt7S7Nd0T23_VtpJ-pELg5CkxecKp5nEd7mXhnS5wisAVC4Dx7FlckiVx5k3YfsLEYEAPSH4FGOQbNtALoAg09NWGkLZHgDi-lmVnZJ-aiXtDhSAUAZoAsTKkZz2FYzJQhr4He_EAuXlz0qQlYTSiX0OEq8kuwrPyFrep-gvAbW-rKSkAoH-gso7GnUGoL0ZKhzTKrYATg6bzCzu4SZL1-H3n40OO0lSbUZPq50r0yMjNchW7H4SL79OCH78m8H5IbHrnwjiUCY-LG7F_amaHtXhVjJy4RToUiwZ10eyzbILY6gRXMspPXKkOaZ_txcRhnKg6z6gjvTbx3oL7QmaMDm1EEa83YyC7j2A9pqdbe_44yayR_-zfo6-PzzwDk8GaHTWgONJRdkllc42tZ68DqsYgO__jClZouX1L5uhwMuSbEEIrbAWa0TxKIpwcDYI7u_Bu5qR_i062WKR9XztWAz2bn62Vtff6T-Ajs_qVSo19pCfytb7qNh00q-DQ4K_9JwXAmRWqgJcS4WdtLYDKEkI8kYv-0AJESCHk8lcf5M3jZdK7wE0zdweyVsFSYflboV1AbH3AfN_2s6QW_vuKlxQ1_kO7-rIxEEvUzGFd3OymukrUFM58UjQtxHfgfiJQ3nHcW0JFrbh6WfEtkVtFDBKLCuM3-jOlOWTUGYNeRgdDWQUYAjBX5vDJvtNCLqvqTOCW46Rzkx2QFr0YA3jnR_BHbBvgWRb2ucH855KVXdiQt78cYcAytbWn-HPoOZjN22DbXiy5xX7wwaRwYwcMg9kVW4vaa6WH8kaFyN_gSYRnkrnaCbGYkHhYO9oidhw4YCcYBcKnYahuMunrmzbnpD332zTnVgDfYREief7YDLxbWquV2oolOTp9TUvR49KijDemj88CYiyIAYpp5xf1n1v5jIF3ARJslu_MZNE5uBeiKjqBTpm9gzp-3aNYh1D-8uNYMshkUfuGicJKDqd8L_XMiHeH7aPl0RbSXy4o7HcVMWeDfdBkEzQJEgID_cptmt3YPIC0uu-UIfea8CpvHXLTNnIZxve4OyZEP4rvX6QbMK2qxPZEpQqyEJ-XsGmgBiKM0o72aKs7JojcIpytHzzTtFmQCoF55E3R9LbvT9QYUFmxK-2yX9Br8QMev3X-qDN0sZjcSlogvZlDpm08HlS5TVcgL-FfGqu3o8IKMfQzxy0-iWQspeTKtb1pNIOaR-gLjiuV3TqJ78HM11_b7VtSrpZ176aFK88TGOJdxG4DkCsj6Cd7SiJOvlcz9nWtRp71EyiRzSuvy0ohkEBqrQXk6Jy4VjNQ4nsrMn8mNEaTM3gKUT8HPfitwTpzTzgGXGTaSrQMh2-zDv60WNGLVhAgOgsi9sZ5NBsGMYabizXonU17NJjy64WSj1OTI0UjnCw88PN6ET","summary":[{"type":"summary_text","text":"**Correlating pulse intervals and thread stacks**"},{"type":"summary_text","text":"**Assessing broker thread behavior**"}]},{"id":"fc_0e15f89f5b7b4062016a59d3c4c564819490c8dbed7f5d2a0d","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"## Ranked findings\\n\\n### 1 — Confirmed dominant brain read/CPU mechanism: the readiness heartbeat re-reads and hashes the entire running `spt.exe` every 500 ms\\n\\nThis is the only source mechanism that quantitatively explains the observed sustained **63.91 MiB/s brain reads**.\\n\\n* `crates/spt-daemon/src/brainproc.rs:63` sets `BRAIN_HEARTBEAT` to **500 ms**.\\n* `crates/spt-daemon/src/brainproc.rs:259-297` sleeps one heartbeat, performs `brain.net_status()`, performs `brain.sessions()`, and then calls `write_ready(generation)` on every iteration.\\n* `crates/spt-daemon/src/brainproc.rs:365-373` has `write_ready` call `current_exe_hash()` before atomically rewriting `brain.ready`.\\n* `crates/spt-daemon/src/brainproc.rs:381-384` implements `current_exe_hash()` as `std::env::current_exe()` followed by `std::fs::read(exe)` of the **entire running executable into a fresh `Vec<u8>`**, followed by SHA-256 over all bytes.\\n* Therefore steady-state brain file-read throughput is\\n  `2 heartbeats/s × deployed_spt_exe_size`.\\n  The observed 63.91 MiB/s predicts a deployed executable size of **31.955 MiB**. That is an unusually exact fit, not merely an order-of-magnitude match.\\n* The same path allocates a fresh executable-sized buffer and hashes it twice per second. That explains sustained one-core CPU as well as process I/O read bytes. Windows process Read Bytes includes cached `ReadFile` traffic, so this need not imply 64 MiB/s of physical disk media traffic.\\n* The source comment at `brainproc.rs:376-380` says “One exe read + hash per brain start,” but the call at `brainproc.rs:297` makes it one read + hash **per heartbeat**. The initial ready call at `brainproc.rs:221` is indeed once at startup; the later heartbeat call is the regression.\\n* `write_ready` also atomically rewrites the small `brain.ready` JSON every 500 ms. `crates/spt-store/src/atomic.rs:57-79` shows that this is a tmp-file write plus rename. It can explain a high operation count and visible metadata/write churn, but not tens of MiB/s by itself.\\n\\n**Falsifiable live probes**\\n\\n1. Compute the exact prediction without changing process state:\\n   ```powershell\\n   $p = Get-Process -Id 58272\\n   $bytes = (Get-Item $p.Path).Length\\n   [pscustomobject]@{\\n     Path = $p.Path\\n     Bytes = $bytes\\n     MiB = $bytes / 1MB\\n     PredictedReadMiBPerSec = 2 * $bytes / 1MB\\n   }\\n   ```\\n   Confirmation is `PredictedReadMiBPerSec` approximately **63.91**. A materially different executable size falsifies this as the complete byte explanation.\\n2. In Sysinternals Process Monitor, filter `PID is 58272`, `Operation is ReadFile`, and `Path is <the value of $p.Path>`. Over 10 seconds, expect about **20 full sequential reads**, totaling about `20 × executable length`, spaced near 500 ms. Filter out this one path and recompute brain read bytes; the sustained ~64 MiB/s should disappear.\\n3. Add a Process Monitor filter for `brain.ready`. Expect tmp create/write/rename publication at approximately 2 Hz, synchronized with the executable reads. The ready JSON’s `exe_hash` should remain constant while its last-write timestamp advances.\\n4. Capture sampled CPU stacks in WPA/Windows Performance Recorder or Process Explorer’s active-thread stack view. The continuously active brain thread should spend samples under the executable read and SHA-256 path. The falsifier is sustained 15.5% CPU entirely on other thread stacks after excluding this path.\\n\\n### 2 — Plausible secondary, session-count-scaled activity: one 5-second pulse thread per hosted live endpoint; transcript reads are gated, not unconditional\\n\\nThis explains why a five-second activity census can see roughly one active brain thread per hosted session, but it does **not** fit a flat 63.91 MiB/s nearly as well as mechanism 1.\\n\\n* `crates/spt-daemon/src/livehost.rs:637-639` starts one `run_pulse_loop` thread per hosted live lifecycle.\\n* `crates/spt-daemon/src/config.rs:19-25` gives the default pulse period as **5 seconds**, configurable via `daemon.json`.\\n* `crates/spt-daemon/src/lifecycle.rs:701-738` runs each endpoint on that configured deadline grid. Each due tick checks the manifest and calls `pulse_tick`.\\n* `crates/spt-daemon/src/lifecycle.rs:465-490` reads and hashes the endpoint adapter’s small `manifest.toml` to detect live updates.\\n* `crates/spt-daemon/src/lifecycle.rs:585-666` reads resting/info state, stamps active recency, enumerates configured commune/signoff drop directories, and consumes an echo gate.\\n* Crucially, history/transcript access is **not performed on every pulse**. `lifecycle.rs:647-666` calls `fire_echo` only when `take_echo_gate(...)` succeeds. `lifecycle.rs:843-860` fetches history only inside that echo path.\\n* When it does fire, `crates/spt-live/src/history.rs:103-129` can read an entire adapter-located raw transcript (`std::fs::read`) or entire perch `history.log` (`read_to_string`), then normalize it. Thus large transcript files can produce bursty large reads, but only on armed echo events, not on quiet five-second ticks.\\n\\n**Falsifiable live probes**\\n\\n1. In Process Monitor, exclude the running `spt.exe` path, then group remaining `ReadFile` bytes by path. If pulse history is material, large reads will name an adapter raw transcript or `<perch>\\\\history.log`, recur on the configured pulse grid, and correlate with echo-gate consumption. If those paths are absent or tiny, pulse threads explain the “five active threads” census but not the byte rate.\\n2. Compare `daemon.json`’s `pulse_period_ms` with timestamps of reads to perches’ `info.json`, `resting.json`/rest records, adapter `manifest.toml`, and drop directories. Quiet-session metadata should recur around that period; raw transcript reads should occur only when an echo gate was armed.\\n3. Inspect active-thread stacks: normal quiet pulse threads should appear briefly in `pulse_tick`, info/rest reads, directory enumeration, and sleeps. A thread continuously reading one transcript rather than waking once per grid point would falsify normal pulse behavior.\\n\\n### 3 — Project index: sub-second polling exists, but it is one cheap directory query; repository/session scans are event-driven or every five minutes\\n\\n* `crates/spt-daemon/src/projwriter.rs:44-53` defines a **500 ms** invalidation poll, **250 ms** debounce, and **300,000 ms / 5 minute** periodic reconcile.\\n* `projwriter.rs:622-683` runs exactly one writer thread. On ordinary idle ticks it calls only `projinval::pending_at(...)` and continues.\\n* `crates/spt-store/src/projinval.rs:90-95` proves the idle poll is one `read_dir` of `$SPT_HOME/index/invalidations`, short-circuiting after the first entry. It does not read repositories, transcripts, logs, or the project index on every 500 ms tick.\\n* When invalidations exist, `projinval.rs:104-119` drains each tiny invalidation JSON file and deletes it, coalescing the batch.\\n* At boot, on an invalidation, or at the five-minute backstop, `projwriter.rs:294-348` invokes one branch-tip enumeration of the context store’s bare seed Git repository and scans a `p-*` tree only when that branch tip changed.\\n* `projwriter.rs:350-450` enumerates perches and reads each targeted `<perch>/sessions.log`; it derives project identity from recorded cwd values. It is not scanning the project worktree’s contents.\\n* `crates/spt-store/src/sessions.rs:28-32,98-107` bounds every `sessions.log` at **64 rows**, so this input cannot grow without bound.\\n* The unchanged-fingerprint no-work gate at `projwriter.rs:307-320` exits before tree/perch composition when there is no changed generation and no scoped work.\\n\\n**Falsifiable live probes**\\n\\n1. Process Monitor should show `QueryDirectory`/directory metadata traffic against `$SPT_HOME\\\\index\\\\invalidations` at about 2 Hz, with negligible bytes. It should not show repository worktree or transcript reads on that cadence.\\n2. Watch `$SPT_HOME\\\\index\\\\project-index-stats.json`. `last_run_ms` and the `last_cycle` counters should change only after an invalidation/boot reconcile or about every five minutes—not every 500 ms. A continuously advancing `last_run_ms`, nonzero tree scans every tick, or an invalidation file that never drains would identify a separate malfunction.\\n3. During a periodic full reconcile, expect one seed-repo branch enumeration, at most one tree scan per changed `p-*` branch, and bounded `sessions.log` reads. Reads of arbitrary repository files or raw transcripts during this phase would falsify the source contract and identify an untraced caller.\\n\\n### 4 — Dispatcher/registry streams: 100 ms IPC polling and 15-second poison windows can explain thread/telemetry symptoms, but not sustained brain file reads\\n\\n* `crates/spt-daemon/src/dispatch.rs:47-49` sets the dispatcher’s stream-table poll to **100 ms**.\\n* `dispatch.rs:429-588` issues `brain.net_streams()` each tick, skips local/empty rows, claims at most eight workers, and keeps in-flight/retry/terminal state in a per-generation map. The idle path is broker IPC plus sleep, with no filesystem scan.\\n* `dispatch.rs:1197-1207` starts exactly one dispatcher loop.\\n* `dispatch.rs:970-1048` shows the Registry arm: load the small subnet/roster policy once per feed, subscribe from sequence zero, block on `brain.read_event`, decode rows, apply them, and return on EOF. It is event-driven, not a polling reader.\\n* `crates/spt-daemon/src/registryhost.rs:69-80` loads subnet and roster stores once per inbound registry feed.\\n* `registryhost.rs:241-344` merges rows under the in-memory registry lock, drops the lock, stamps heard state, and writes snapshots. There is no lock self-deadlock in these apply paths.\\n* `registryhost.rs:738-765` serializes current in-memory registries to `$SPT_HOME/identity/registry/<subnet>.json` plus `heard.meta` after an admitted feed. This is the main registry-feed disk I/O. It is proportional to registry snapshot size and feed frequency, not transcript/repository size.\\n* `crates/spt-daemon/src/nethost.rs:119-218` gives every installed stream subscriber a bounded queue and its own writer thread. Replay is written first, then live frames; the thread halts on its first `SharedSend::write` failure and marks the seat poisoned.\\n* `crates/spt-daemon/src/broker.rs:167-188` fixes the production brain write deadline at **15 seconds**. Therefore a poison appearing almost exactly 15 seconds after subscriber attach is a blocked broker→brain connection write, not a registry scan cadence.\\n* `nethost.rs:505-572` refuses a replacement only while a poisoned prior subscriber writer is still draining; every install gets a new lease. `dispatch.rs:209-333` classifies subscriber-busy/replay-halted/lease-canceled failures as seat-blocked and applies a global **2, 4, 8, 16, then 30 second capped** breaker, without burning the transient attempt budget.\\n* On successful service, `dispatch.rs:492-510` calls `net_stream_retire`; `crates/spt-daemon/src/nethost.rs:1835-1840` removes the row from future stream-table enumeration. Thus the normal lifecycle is unique feed stream → subscribe → data → EOF → Served → retire. A single served stream should not be replayed indefinitely in one generation.\\n\\n**Interpretation of the observed Registry subscriber poison telemetry**\\n\\nRepeated 15-second Registry poison is real abnormal IPC backpressure and can create broker-side writer/watchdog threads and dispatch retries. It is, however, orthogonal to the 64 MiB/s brain `ReadFile` total: registry handling reads small policy files and writes snapshots; it never reads the executable, repository worktree, or transcript. The current-executable heartbeat continues on the brain main thread even while Registry workers are blocked.\\n\\n**Falsifiable live probes**\\n\\n1. Correlate `CONN_LIFECYCLE` records. `crates/spt-daemon/src/conn.rs:229-275` emits stable `conn=<id>`, role facts, wall/monotonic stamps, attach, and poison events. For a deadline poison, the same conn should show `stream-sub-attach`, then `CONN_WRITE_POISONED`/`stream-sub-writer-poison` about 15,000 ms later. A fast `CONN_WRITE_RETIRED` instead means organic peer death, not a 15-second black hole.\\n2. Group `DISPATCH_EV` by `(gen, stream, family, attempt, conn)`. Unique Registry stream IDs about every peer registry cadence indicate new periodic feeds; repeated attempts for the **same** stream with breaker windows 2→4→8→16→30 seconds indicate recovery from a poisoned subscriber; a Served event followed by that same stream reappearing indicates retirement failed or an older broker lacks the retire verb.\\n3. Process Monitor should show registry reads limited to subnet/roster policy files and writes to `identity\\\\registry\\\\*.json`/`heard.meta`. If the 64 MiB/s is attributed to registry, there must be comparably large named registry files and repeated full reads; source inspection predicts neither.\\n4. Compare poison timestamps with thread count. Subscriber writer threads park on their queue while healthy and exit after clean detach or first failure. A monotonically increasing count without matching detach/writer-exit lifecycle events would establish a thread-retention bug; a sawtooth count around feed arrival/15-second failures is bounded churn rather than a leak.\\n\\n### 5 — Peer registry pump: 200 ms scheduler/marker checks; full local advertisement every 30 seconds by default\\n\\n* `crates/spt-daemon/src/pump/mod.rs:96-102` uses a **200 ms** scheduler tick and a **5-second** pump heartbeat write throttle.\\n* `crates/spt-daemon/src/config.rs:35-46` sets default registry/notif/sync/update cadences to **30 s / 15 s / 60 s / 3600 s**; registry eviction defaults to 300 s.\\n* `pump/mod.rs:583-613` polls workers’ one-shot wake markers on every 200 ms scheduler tick and sleeps immediately when nothing is due.\\n* `crates/spt-daemon/src/pump/registry.rs:120-155` consumes repair/advertise markers, performs eviction, rotations, and `advertise_local` once per due/forced registry round.\\n* `crates/spt-daemon/src/registryhost.rs:399-632` then enumerates self perches, reads small state/info records, adapter registry data, and each bounded `sessions.log`, updates the in-memory registry, and writes snapshots.\\n* `registryhost.rs:893-919` derives at most eight recent project IDs per endpoint from the bounded session ledger. It does not traverse those project directories or read their transcripts.\\n\\nThis can generate periodic metadata and registry JSON churn, especially after many endpoint transition markers, but the normal cadence is 30 seconds, not sub-second full scanning. The 200 ms path consumes marker existence/removal state; it does not repeatedly scan every perch.\\n\\n**Falsifiable live probe:** group filesystem activity into 200 ms versus 30-second buckets. Normal idle pump activity is a tiny heartbeat/marker check; perch, session-ledger, adapter-registry, and registry-snapshot traffic appears in a burst at the due/forced registry round. Full perch scans every 200 ms would falsify the expected scheduler behavior.\\n\\n## Bottom line\\n\\nThe brain’s sustained read rate has a direct numerical source match: **`write_ready` re-hashes the entire ~31.955 MiB executable twice per second**. The four-to-five hosted sessions add expected five-second pulse-thread activity, which explains the active-thread census but not the exact byte rate. Project index, dispatcher, and registry loops do contain 100–500 ms polls, but their idle work is IPC or one directory/marker query; their repository, ledger, and snapshot work is gated and much less frequent. Registry subscriber poisons are a separate, falsifiable 15-second SharedSend backpressure condition worth tracking for broker thread churn, not a candidate for 64 MiB/s of brain file reads.\",\"files\":[{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"Primary RCA. `BRAIN_HEARTBEAT` is 500 ms (line 63); the steady loop calls `write_ready` every heartbeat (259-297); `write_ready` calls `current_exe_hash` (365-373), which reads and SHA-256 hashes the entire running executable (381-384). Also wires live host, project writer, dispatcher, and peer pump.\"},{\"path\":\"crates/spt-store/src/atomic.rs\",\"description\":\"Shows each 500 ms ready publication is a non-durable unique tmp write plus atomic rename; explains small write/metadata churn alongside the executable reads.\"},{\"path\":\"crates/spt-daemon/src/livehost.rs\",\"description\":\"Hosts one lifecycle pulse thread per online live endpoint and performs a full live-host reconcile every 5 seconds; useful for explaining 4-5 intermittently active brain threads.\"},{\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"description\":\"Defines configured pulse-loop semantics, per-tick state/drop scans, manifest hashing, and the one-shot echo gate. History/transcript fetch occurs only inside `fire_echo`, not on every quiet pulse.\"},{\"path\":\"crates/spt-live/src/history.rs\",\"description\":\"Exact potentially large transcript reads: full raw transcript for `locate_normalize`, or full perch `history.log` for native history. Both are invoked only when lifecycle history is requested.\"},{\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"description\":\"Project-index writer: 500 ms invalidation directory poll, 250 ms debounce, five-minute periodic reconcile; branch-tip fingerprint gate and changed-branch-only seed-repo tree scans; bounded session-ledger reads during composition.\"},{\"path\":\"crates/spt-store/src/projinval.rs\",\"description\":\"Proves the idle project-index poll is one short-circuiting `read_dir`; draining reads/deletes only queued tiny invalidation files.\"},{\"path\":\"crates/spt-store/src/sessions.rs\",\"description\":\"Defines `<perch>/sessions.log` and its 64-row hard bound, ruling out unbounded session-ledger scan volume in registry/project-index loops.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"100 ms stream-table IPC poll; bounded eight-worker claim pool; keyed terminal/retry state; registry subscribe/read-to-EOF arm; success retirement; 2-to-30-second seat-blocked circuit breaker and discriminating telemetry.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"StreamLog subscriber lifecycle: per-seat bounded writer queue/thread, replay-before-live, halt on first failed SharedSend write, poison/busy rules, fresh leases, EOF, detach, and retired stream removal.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"Defines the 15-second production brain write deadline and brokers stream subscribe/retire operations. The exact 15-second poison interval is a write deadline, not a registry cadence.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"Bounded physical broker connection writes, watchdog, stable connection IDs, and `CONN_LIFECYCLE` / `CONN_WRITE_POISONED` / `CONN_WRITE_RETIRED` telemetry needed to distinguish a deadline black hole from organic disconnect churn.\"},{\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"description\":\"Peer-pump scheduler: 200 ms idle tick, five-second heartbeat write, config-driven workers, and due-only round loads.\"},{\"path\":\"crates/spt-daemon/src/pump/registry.rs\",\"description\":\"Registry worker semantics: marker-triggered or cadence-triggered pre-round sweep and one fire-and-forget feed stream per peer/subnet.\"},{\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"description\":\"Inbound registry policy/apply/snapshot paths and outbound `advertise_local`; reads bounded perches/session ledgers and writes registry JSON snapshots, with no repository/transcript scan.\"},{\"path\":\"crates/spt-daemon/src/config.rs\",\"description\":\"Production default cadences: pulse 5 s, registry 30 s, notif 15 s, sync 60 s, update 3600 s, registry eviction 300 s.\"}],\"architecture\":\"The deployed brain is a multi-threaded orchestrator around one 500 ms main heartbeat. The main thread owns a long-lived broker IPC client and, every heartbeat, requests net status and sessions, then republishes readiness. Readiness publication currently recomputes the resident executable hash by reading the entire binary, creating the dominant sustained read/CPU path. Separate brain threads host: (1) live-host reconciliation every 5 seconds plus one configured pulse loop per hosted endpoint; (2) one project-index writer whose idle loop performs a 500 ms invalidation-directory query and whose expensive reconcile is event/five-minute gated; (3) one dispatcher polling the broker’s in-memory stream table every 100 ms and spawning at most eight event-blocking workers; and (4) one peer pump with a 200 ms scheduler and 15/30/60/3600-second worker cadences. The dispatcher and peer pump share an `Arc<RegistryHost>`: outbound registry rounds scan bounded local state and push one finite stream per peer/subnet, while inbound dispatcher workers subscribe, merge, snapshot, wait for EOF, and retire served rows. Broker-owned StreamLogs attach each subscriber to a bounded dedicated writer; a broker→brain write blocked for 15 seconds poisons that physical connection and drives the dispatcher’s capped breaker. This IPC poison path can account for broker thread churn and repeated Registry telemetry, but it is independent of the brain main thread’s deterministic twice-per-second executable read/hash.\"}}}","call_id":"call_uXkAjjUBe4aDGdWT0AfjI9BE","name":"yield"}]},"duration":119994.98449999094,"ttft":1710.0489999949932,"contextSnapshot":{"promptTokens":129695,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_uXkAjjUBe4aDGdWT0AfjI9BE|fc_0e15f89f5b7b4062016a59d3c4c564819490c8dbed7f5d2a0d","toolName":"yield","startedAt":"2026-07-17T07:05:14.610Z"},"id":"84691793","parentId":"4a2d3dde","timestamp":"2026-07-17T07:05:14.610Z"}
{"type":"message","id":"c77838f1","parentId":"84691793","timestamp":"2026-07-17T07:05:14.611Z","message":{"role":"toolResult","toolCallId":"call_uXkAjjUBe4aDGdWT0AfjI9BE|fc_0e15f89f5b7b4062016a59d3c4c564819490c8dbed7f5d2a0d","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"## Ranked findings\n\n### 1 — Confirmed dominant brain read/CPU mechanism: the readiness heartbeat re-reads and hashes the entire running `spt.exe` every 500 ms\n\nThis is the only source mechanism that quantitatively explains the observed sustained **63.91 MiB/s brain reads**.\n\n* `crates/spt-daemon/src/brainproc.rs:63` sets `BRAIN_HEARTBEAT` to **500 ms**.\n* `crates/spt-daemon/src/brainproc.rs:259-297` sleeps one heartbeat, performs `brain.net_status()`, performs `brain.sessions()`, and then calls `write_ready(generation)` on every iteration.\n* `crates/spt-daemon/src/brainproc.rs:365-373` has `write_ready` call `current_exe_hash()` before atomically rewriting `brain.ready`.\n* `crates/spt-daemon/src/brainproc.rs:381-384` implements `current_exe_hash()` as `std::env::current_exe()` followed by `std::fs::read(exe)` of the **entire running executable into a fresh `Vec<u8>`**, followed by SHA-256 over all bytes.\n* Therefore steady-state brain file-read throughput is\n  `2 heartbeats/s × deployed_spt_exe_size`.\n  The observed 63.91 MiB/s predicts a deployed executable size of **31.955 MiB**. That is an unusually exact fit, not merely an order-of-magnitude match.\n* The same path allocates a fresh executable-sized buffer and hashes it twice per second. That explains sustained one-core CPU as well as process I/O read bytes. Windows process Read Bytes includes cached `ReadFile` traffic, so this need not imply 64 MiB/s of physical disk media traffic.\n* The source comment at `brainproc.rs:376-380` says “One exe read + hash per brain start,” but the call at `brainproc.rs:297` makes it one read + hash **per heartbeat**. The initial ready call at `brainproc.rs:221` is indeed once at startup; the later heartbeat call is the regression.\n* `write_ready` also atomically rewrites the small `brain.ready` JSON every 500 ms. `crates/spt-store/src/atomic.rs:57-79` shows that this is a tmp-file write plus rename. It can explain a high operation count and visible metadata/write churn, but not tens of MiB/s by itself.\n\n**Falsifiable live probes**\n\n1. Compute the exact prediction without changing process state:\n   ```powershell\n   $p = Get-Process -Id 58272\n   $bytes = (Get-Item $p.Path).Length\n   [pscustomobject]@{\n     Path = $p.Path\n     Bytes = $bytes\n     MiB = $bytes / 1MB\n     PredictedReadMiBPerSec = 2 * $bytes / 1MB\n   }\n   ```\n   Confirmation is `PredictedReadMiBPerSec` approximately **63.91**. A materially different executable size falsifies this as the complete byte explanation.\n2. In Sysinternals Process Monitor, filter `PID is 58272`, `Operation is ReadFile`, and `Path is <the value of $p.Path>`. Over 10 seconds, expect about **20 full sequential reads**, totaling about `20 × executable length`, spaced near 500 ms. Filter out this one path and recompute brain read bytes; the sustained ~64 MiB/s should disappear.\n3. Add a Process Monitor filter for `brain.ready`. Expect tmp create/write/rename publication at approximately 2 Hz, synchronized with the executable reads. The ready JSON’s `exe_hash` should remain constant while its last-write timestamp advances.\n4. Capture sampled CPU stacks in WPA/Windows Performance Recorder or Process Explorer’s active-thread stack view. The continuously active brain thread should spend samples under the executable read and SHA-256 path. The falsifier is sustained 15.5% CPU entirely on other thread stacks after excluding this path.\n\n### 2 — Plausible secondary, session-count-scaled activity: one 5-second pulse thread per hosted live endpoint; transcript reads are gated, not unconditional\n\nThis explains why a five-second activity census can see roughly one active brain thread per hosted session, but it does **not** fit a flat 63.91 MiB/s nearly as well as mechanism 1.\n\n* `crates/spt-daemon/src/livehost.rs:637-639` starts one `run_pulse_loop` thread per hosted live lifecycle.\n* `crates/spt-daemon/src/config.rs:19-25` gives the default pulse period as **5 seconds**, configurable via `daemon.json`.\n* `crates/spt-daemon/src/lifecycle.rs:701-738` runs each endpoint on that configured deadline grid. Each due tick checks the manifest and calls `pulse_tick`.\n* `crates/spt-daemon/src/lifecycle.rs:465-490` reads and hashes the endpoint adapter’s small `manifest.toml` to detect live updates.\n* `crates/spt-daemon/src/lifecycle.rs:585-666` reads resting/info state, stamps active recency, enumerates configured commune/signoff drop directories, and consumes an echo gate.\n* Crucially, history/transcript access is **not performed on every pulse**. `lifecycle.rs:647-666` calls `fire_echo` only when `take_echo_gate(...)` succeeds. `lifecycle.rs:843-860` fetches history only inside that echo path.\n* When it does fire, `crates/spt-live/src/history.rs:103-129` can read an entire adapter-located raw transcript (`std::fs::read`) or entire perch `history.log` (`read_to_string`), then normalize it. Thus large transcript files can produce bursty large reads, but only on armed echo events, not on quiet five-second ticks.\n\n**Falsifiable live probes**\n\n1. In Process Monitor, exclude the running `spt.exe` path, then group remaining `ReadFile` bytes by path. If pulse history is material, large reads will name an adapter raw transcript or `<perch>\\history.log`, recur on the configured pulse grid, and correlate with echo-gate consumption. If those paths are absent or tiny, pulse threads explain the “five active threads” census but not the byte rate.\n2. Compare `daemon.json`’s `pulse_period_ms` with timestamps of reads to perches’ `info.json`, `resting.json`/rest records, adapter `manifest.toml`, and drop directories. Quiet-session metadata should recur around that period; raw transcript reads should occur only when an echo gate was armed.\n3. Inspect active-thread stacks: normal quiet pulse threads should appear briefly in `pulse_tick`, info/rest reads, directory enumeration, and sleeps. A thread continuously reading one transcript rather than waking once per grid point would falsify normal pulse behavior.\n\n### 3 — Project index: sub-second polling exists, but it is one cheap directory query; repository/session scans are event-driven or every five minutes\n\n* `crates/spt-daemon/src/projwriter.rs:44-53` defines a **500 ms** invalidation poll, **250 ms** debounce, and **300,000 ms / 5 minute** periodic reconcile.\n* `projwriter.rs:622-683` runs exactly one writer thread. On ordinary idle ticks it calls only `projinval::pending_at(...)` and continues.\n* `crates/spt-store/src/projinval.rs:90-95` proves the idle poll is one `read_dir` of `$SPT_HOME/index/invalidations`, short-circuiting after the first entry. It does not read repositories, transcripts, logs, or the project index on every 500 ms tick.\n* When invalidations exist, `projinval.rs:104-119` drains each tiny invalidation JSON file and deletes it, coalescing the batch.\n* At boot, on an invalidation, or at the five-minute backstop, `projwriter.rs:294-348` invokes one branch-tip enumeration of the context store’s bare seed Git repository and scans a `p-*` tree only when that branch tip changed.\n* `projwriter.rs:350-450` enumerates perches and reads each targeted `<perch>/sessions.log`; it derives project identity from recorded cwd values. It is not scanning the project worktree’s contents.\n* `crates/spt-store/src/sessions.rs:28-32,98-107` bounds every `sessions.log` at **64 rows**, so this input cannot grow without bound.\n* The unchanged-fingerprint no-work gate at `projwriter.rs:307-320` exits before tree/perch composition when there is no changed generation and no scoped work.\n\n**Falsifiable live probes**\n\n1. Process Monitor should show `QueryDirectory`/directory metadata traffic against `$SPT_HOME\\index\\invalidations` at about 2 Hz, with negligible bytes. It should not show repository worktree or transcript reads on that cadence.\n2. Watch `$SPT_HOME\\index\\project-index-stats.json`. `last_run_ms` and the `last_cycle` counters should change only after an invalidation/boot reconcile or about every five minutes—not every 500 ms. A continuously advancing `last_run_ms`, nonzero tree scans every tick, or an invalidation file that never drains would identify a separate malfunction.\n3. During a periodic full reconcile, expect one seed-repo branch enumeration, at most one tree scan per changed `p-*` branch, and bounded `sessions.log` reads. Reads of arbitrary repository files or raw transcripts during this phase would falsify the source contract and identify an untraced caller.\n\n### 4 — Dispatcher/registry streams: 100 ms IPC polling and 15-second poison windows can explain thread/telemetry symptoms, but not sustained brain file reads\n\n* `crates/spt-daemon/src/dispatch.rs:47-49` sets the dispatcher’s stream-table poll to **100 ms**.\n* `dispatch.rs:429-588` issues `brain.net_streams()` each tick, skips local/empty rows, claims at most eight workers, and keeps in-flight/retry/terminal state in a per-generation map. The idle path is broker IPC plus sleep, with no filesystem scan.\n* `dispatch.rs:1197-1207` starts exactly one dispatcher loop.\n* `dispatch.rs:970-1048` shows the Registry arm: load the small subnet/roster policy once per feed, subscribe from sequence zero, block on `brain.read_event`, decode rows, apply them, and return on EOF. It is event-driven, not a polling reader.\n* `crates/spt-daemon/src/registryhost.rs:69-80` loads subnet and roster stores once per inbound registry feed.\n* `registryhost.rs:241-344` merges rows under the in-memory registry lock, drops the lock, stamps heard state, and writes snapshots. There is no lock self-deadlock in these apply paths.\n* `registryhost.rs:738-765` serializes current in-memory registries to `$SPT_HOME/identity/registry/<subnet>.json` plus `heard.meta` after an admitted feed. This is the main registry-feed disk I/O. It is proportional to registry snapshot size and feed frequency, not transcript/repository size.\n* `crates/spt-daemon/src/nethost.rs:119-218` gives every installed stream subscriber a bounded queue and its own writer thread. Replay is written first, then live frames; the thread halts on its first `SharedSend::write` failure and marks the seat poisoned.\n* `crates/spt-daemon/src/broker.rs:167-188` fixes the production brain write deadline at **15 seconds**. Therefore a poison appearing almost exactly 15 seconds after subscriber attach is a blocked broker→brain connection write, not a registry scan cadence.\n* `nethost.rs:505-572` refuses a replacement only while a poisoned prior subscriber writer is still draining; every install gets a new lease. `dispatch.rs:209-333` classifies subscriber-busy/replay-halted/lease-canceled failures as seat-blocked and applies a global **2, 4, 8, 16, then 30 second capped** breaker, without burning the transient attempt budget.\n* On successful service, `dispatch.rs:492-510` calls `net_stream_retire`; `crates/spt-daemon/src/nethost.rs:1835-1840` removes the row from future stream-table enumeration. Thus the normal lifecycle is unique feed stream → subscribe → data → EOF → Served → retire. A single served stream should not be replayed indefinitely in one generation.\n\n**Interpretation of the observed Registry subscriber poison telemetry**\n\nRepeated 15-second Registry poison is real abnormal IPC backpressure and can create broker-side writer/watchdog threads and dispatch retries. It is, however, orthogonal to the 64 MiB/s brain `ReadFile` total: registry handling reads small policy files and writes snapshots; it never reads the executable, repository worktree, or transcript. The current-executable heartbeat continues on the brain main thread even while Registry workers are blocked.\n\n**Falsifiable live probes**\n\n1. Correlate `CONN_LIFECYCLE` records. `crates/spt-daemon/src/conn.rs:229-275` emits stable `conn=<id>`, role facts, wall/monotonic stamps, attach, and poison events. For a deadline poison, the same conn should show `stream-sub-attach`, then `CONN_WRITE_POISONED`/`stream-sub-writer-poison` about 15,000 ms later. A fast `CONN_WRITE_RETIRED` instead means organic peer death, not a 15-second black hole.\n2. Group `DISPATCH_EV` by `(gen, stream, family, attempt, conn)`. Unique Registry stream IDs about every peer registry cadence indicate new periodic feeds; repeated attempts for the **same** stream with breaker windows 2→4→8→16→30 seconds indicate recovery from a poisoned subscriber; a Served event followed by that same stream reappearing indicates retirement failed or an older broker lacks the retire verb.\n3. Process Monitor should show registry reads limited to subnet/roster policy files and writes to `identity\\registry\\*.json`/`heard.meta`. If the 64 MiB/s is attributed to registry, there must be comparably large named registry files and repeated full reads; source inspection predicts neither.\n4. Compare poison timestamps with thread count. Subscriber writer threads park on their queue while healthy and exit after clean detach or first failure. A monotonically increasing count without matching detach/writer-exit lifecycle events would establish a thread-retention bug; a sawtooth count around feed arrival/15-second failures is bounded churn rather than a leak.\n\n### 5 — Peer registry pump: 200 ms scheduler/marker checks; full local advertisement every 30 seconds by default\n\n* `crates/spt-daemon/src/pump/mod.rs:96-102` uses a **200 ms** scheduler tick and a **5-second** pump heartbeat write throttle.\n* `crates/spt-daemon/src/config.rs:35-46` sets default registry/notif/sync/update cadences to **30 s / 15 s / 60 s / 3600 s**; registry eviction defaults to 300 s.\n* `pump/mod.rs:583-613` polls workers’ one-shot wake markers on every 200 ms scheduler tick and sleeps immediately when nothing is due.\n* `crates/spt-daemon/src/pump/registry.rs:120-155` consumes repair/advertise markers, performs eviction, rotations, and `advertise_local` once per due/forced registry round.\n* `crates/spt-daemon/src/registryhost.rs:399-632` then enumerates self perches, reads small state/info records, adapter registry data, and each bounded `sessions.log`, updates the in-memory registry, and writes snapshots.\n* `registryhost.rs:893-919` derives at most eight recent project IDs per endpoint from the bounded session ledger. It does not traverse those project directories or read their transcripts.\n\nThis can generate periodic metadata and registry JSON churn, especially after many endpoint transition markers, but the normal cadence is 30 seconds, not sub-second full scanning. The 200 ms path consumes marker existence/removal state; it does not repeatedly scan every perch.\n\n**Falsifiable live probe:** group filesystem activity into 200 ms versus 30-second buckets. Normal idle pump activity is a tiny heartbeat/marker check; perch, session-ledger, adapter-registry, and registry-snapshot traffic appears in a burst at the due/forced registry round. Full perch scans every 200 ms would falsify the expected scheduler behavior.\n\n## Bottom line\n\nThe brain’s sustained read rate has a direct numerical source match: **`write_ready` re-hashes the entire ~31.955 MiB executable twice per second**. The four-to-five hosted sessions add expected five-second pulse-thread activity, which explains the active-thread census but not the exact byte rate. Project index, dispatcher, and registry loops do contain 100–500 ms polls, but their idle work is IPC or one directory/marker query; their repository, ledger, and snapshot work is gated and much less frequent. Registry subscriber poisons are a separate, falsifiable 15-second SharedSend backpressure condition worth tracking for broker thread churn, not a candidate for 64 MiB/s of brain file reads.","files":[{"path":"crates/spt-daemon/src/brainproc.rs","description":"Primary RCA. `BRAIN_HEARTBEAT` is 500 ms (line 63); the steady loop calls `write_ready` every heartbeat (259-297); `write_ready` calls `current_exe_hash` (365-373), which reads and SHA-256 hashes the entire running executable (381-384). Also wires live host, project writer, dispatcher, and peer pump."},{"path":"crates/spt-store/src/atomic.rs","description":"Shows each 500 ms ready publication is a non-durable unique tmp write plus atomic rename; explains small write/metadata churn alongside the executable reads."},{"path":"crates/spt-daemon/src/livehost.rs","description":"Hosts one lifecycle pulse thread per online live endpoint and performs a full live-host reconcile every 5 seconds; useful for explaining 4-5 intermittently active brain threads."},{"path":"crates/spt-daemon/src/lifecycle.rs","description":"Defines configured pulse-loop semantics, per-tick state/drop scans, manifest hashing, and the one-shot echo gate. History/transcript fetch occurs only inside `fire_echo`, not on every quiet pulse."},{"path":"crates/spt-live/src/history.rs","description":"Exact potentially large transcript reads: full raw transcript for `locate_normalize`, or full perch `history.log` for native history. Both are invoked only when lifecycle history is requested."},{"path":"crates/spt-daemon/src/projwriter.rs","description":"Project-index writer: 500 ms invalidation directory poll, 250 ms debounce, five-minute periodic reconcile; branch-tip fingerprint gate and changed-branch-only seed-repo tree scans; bounded session-ledger reads during composition."},{"path":"crates/spt-store/src/projinval.rs","description":"Proves the idle project-index poll is one short-circuiting `read_dir`; draining reads/deletes only queued tiny invalidation files."},{"path":"crates/spt-store/src/sessions.rs","description":"Defines `<perch>/sessions.log` and its 64-row hard bound, ruling out unbounded session-ledger scan volume in registry/project-index loops."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"100 ms stream-table IPC poll; bounded eight-worker claim pool; keyed terminal/retry state; registry subscribe/read-to-EOF arm; success retirement; 2-to-30-second seat-blocked circuit breaker and discriminating telemetry."},{"path":"crates/spt-daemon/src/nethost.rs","description":"StreamLog subscriber lifecycle: per-seat bounded writer queue/thread, replay-before-live, halt on first failed SharedSend write, poison/busy rules, fresh leases, EOF, detach, and retired stream removal."},{"path":"crates/spt-daemon/src/broker.rs","description":"Defines the 15-second production brain write deadline and brokers stream subscribe/retire operations. The exact 15-second poison interval is a write deadline, not a registry cadence."},{"path":"crates/spt-daemon/src/conn.rs","description":"Bounded physical broker connection writes, watchdog, stable connection IDs, and `CONN_LIFECYCLE` / `CONN_WRITE_POISONED` / `CONN_WRITE_RETIRED` telemetry needed to distinguish a deadline black hole from organic disconnect churn."},{"path":"crates/spt-daemon/src/pump/mod.rs","description":"Peer-pump scheduler: 200 ms idle tick, five-second heartbeat write, config-driven workers, and due-only round loads."},{"path":"crates/spt-daemon/src/pump/registry.rs","description":"Registry worker semantics: marker-triggered or cadence-triggered pre-round sweep and one fire-and-forget feed stream per peer/subnet."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"Inbound registry policy/apply/snapshot paths and outbound `advertise_local`; reads bounded perches/session ledgers and writes registry JSON snapshots, with no repository/transcript scan."},{"path":"crates/spt-daemon/src/config.rs","description":"Production default cadences: pulse 5 s, registry 30 s, notif 15 s, sync 60 s, update 3600 s, registry eviction 300 s."}],"architecture":"The deployed brain is a multi-threaded orchestrator around one 500 ms main heartbeat. The main thread owns a long-lived broker IPC client and, every heartbeat, requests net status and sessions, then republishes readiness. Readiness publication currently recomputes the resident executable hash by reading the entire binary, creating the dominant sustained read/CPU path. Separate brain threads host: (1) live-host reconciliation every 5 seconds plus one configured pulse loop per hosted endpoint; (2) one project-index writer whose idle loop performs a 500 ms invalidation-directory query and whose expensive reconcile is event/five-minute gated; (3) one dispatcher polling the broker’s in-memory stream table every 100 ms and spawning at most eight event-blocking workers; and (4) one peer pump with a 200 ms scheduler and 15/30/60/3600-second worker cadences. The dispatcher and peer pump share an `Arc<RegistryHost>`: outbound registry rounds scan bounded local state and push one finite stream per peer/subnet, while inbound dispatcher workers subscribe, merge, snapshot, wait for EOF, and retire served rows. Broker-owned StreamLogs attach each subscriber to a bounded dedicated writer; a broker→brain write blocked for 15 seconds poisons that physical connection and drives the dispatcher’s capped breaker. This IPC poison path can account for broker thread churn and repeated Registry telemetry, but it is independent of the brain main thread’s deterministic twice-per-second executable read/hash."},"status":"success","type":"result"},"isError":false,"timestamp":1784271914610}}
{"type":"message","id":"157f5ff5","parentId":"c77838f1","timestamp":"2026-07-17T07:05:14.669Z","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":1784271914664}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `RedispatchCpuRca`:\n\nI found dispatcher net_streams 10Hz over unbounded locally-initiated Registry rows. Did your I/O trace support this versus registry sessions.log scans? One-line answer useful.\nIf a response is expected, reply with the `irc` tool (`op: \"send\"`, `to: \"RedispatchCpuRca\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"153306e8485c5f4a","from":"RedispatchCpuRca","message":"I found dispatcher net_streams 10Hz over unbounded locally-initiated Registry rows. Did your I/O trace support this versus registry sessions.log scans? One-line answer useful."},"attribution":"agent","id":"c30d770a","parentId":"157f5ff5","timestamp":"2026-07-17T07:06:55.650Z"}
{"type":"message","id":"b0081238","parentId":"c30d770a","timestamp":"2026-07-17T07:07:09.433Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing net_streams scanning and serialization**"},{"type":"toolCall","id":"call_Yfr0Yib4SAMAthregYgYyUqE|fc_050c9924c02ee1aa016a59d4981b8481959400a94a809e6a1f","name":"irc","arguments":{"i":"Clarifying dispatcher I/O","op":"send","to":"RedispatchCpuRca","message":"Source says 10Hz `brain.net_streams()` serializes the full in-memory table; locally initiated rows are skipped only after transfer (`dispatch.rs:458-465`), so unbounded Registry rows plausibly explain broker pipe writes/CPU, not brain file reads. The 63.91 MiB/s brain `ReadFile` rate matches exactly 2 × deployed exe size via `write_ready`→`current_exe_hash` every 500ms; registry `sessions.log` scans occur only on 30s/forced advertise rounds and ledgers are capped 64 rows.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10138,"output":266,"cacheRead":125440,"cacheWrite":0,"totalTokens":135844,"reasoningTokens":87,"cost":{"input":0.050690000000000006,"output":0.007980000000000001,"cacheRead":0.06272,"cacheWrite":0,"total":0.12139}},"stopReason":"toolUse","timestamp":1784272015669,"responseId":"resp_050c9924c02ee1aa016a59d4900d3c8195aed52c7fe6e88a8a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_050c9924c02ee1aa016a59d49366c4819583400de2e2cb72d0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdSYjqeFEdc-eb-RJ3aU9Y0CLGVru2n1kmeBXM_umNJJEh5Hs48h3xVjHrvbr28y0MJJoxBS0T_2PigmsxP16hgf0RxhWDjfbS4PxXxuMY4zrcY-HMrwEvG1ImhLmZdvn-pKzpPtdqEtQbZXJfQBMguX9GdHAI225hCMFr4TsrWj9v1Dt14GDAFeejw1VODA5bR5z54G2ZCmzMSY5EwiuM-S0J4yD2HZ7fLbUAWjTEpUm4rWwZvFyvQyax4ds6HB8Sx-Aw8eEUB9rmprPPKiy_RyHgn84lC3LPl5xnKhnUbUeYE78hH3Kz-BTwjLFCOknkvHV17oZLVvFMm44Q9pjr7bHbzJAXbW-mSVia1FCYXCy5QQquMKX4GdDtjTkPLYgEOeyqj_dRvtGnFA2F3eZuxnGsz7psw1CAYKeQiHBixFSNjH5Uf3C2C2TH_hHBoIIPFsTtzi6KyBa5GXPOWtcVUoKUAazDUa6vDgde9AVklDX33sbv6uCHRzQ7MVieu6Yv0tEBL6JdF274p405h-gIeHi9tHfKbj1YtgZkTnR8eY9zfcMm2ZcAE849dJi-6HikwBlCMRjRMs-3Ix94rls7YxuPLJw1vEeBuUF3Bvt0NW31kc_RHO9gNJaaaJaihT20PXBloTZouyw5oRmGZHMqxdMpj7ecfrMu2j7j65D_bwD3sY8DyHEKME9XsqPwDGycoLuYanhCzBKwnPrTagY3vO9dWhivhjQLcfrwDpaIvLfHXbknznxUFM3_VwXz519PZpMxSNQbCmkiEZwUs_5iiC8PmTozrwDKtEcuwdSbaXc3gUtN5in-cVgjSoC2UYY2qlqxAXLJDiFfUj3GZ60esop0s0nvd5Xx5EpLO9sn5mDnAya9Iq0o1gcywujKPtxP_qSuckxjh_PDb5Cgz0eyQLs4BY9voY44aQgLVlT_SHchroeo5SxCN-QPp09kq3KM2UlI6LH0_9Tsixmh90A2rVrhRt5n98uifQ33E_IjOcF6tfCaQF_ReOkorzUeJtgleJKw07nDJ1MUmWmMvHpx7q-z0SXlCGQQUGYftQwRuYbz7mBxaLEKtFAC0lWfpt-fuXWXQDBQJ2Lmm7kahMTyG4CzS9jzsKx0OqkFcIjQ-yiS0o-bD3Lb072fA4u0R6xb924l5gBk4jiCtYO7_wubYcMRz9tDn_S96gwhlWfrSxXbdV6zNVv-EVp7SA5wDEBmwP-7buV2avhpH90HYG8xO9dNOqyMRmy15_HIA7wVumt4iTeTqGjC42rlzvC7ikiS8wY4D0XHDECVM-VdpCiSvVSq_2P_kIlg5UCH2whZtcbsZHQ6XyhoUstQ2eThfTnggmcbE1c9uq2QLdXpv5JrXXaajYNRUjIEIwmDlX92GxSzkRpTabFKjiY2Qi_3VlrfPnsmKHwVbJcjpgn4WMcaWiVQ==","summary":[{"type":"summary_text","text":"**Analyzing net_streams scanning and serialization**"}]},{"id":"fc_050c9924c02ee1aa016a59d4981b8481959400a94a809e6a1f","type":"function_call","status":"completed","arguments":"{\"i\":\"Clarifying dispatcher I/O\",\"op\":\"send\",\"to\":\"RedispatchCpuRca\",\"message\":\"Source says 10Hz `brain.net_streams()` serializes the full in-memory table; locally initiated rows are skipped only after transfer (`dispatch.rs:458-465`), so unbounded Registry rows plausibly explain broker pipe writes/CPU, not brain file reads. The 63.91 MiB/s brain `ReadFile` rate matches exactly 2 × deployed exe size via `write_ready`→`current_exe_hash` every 500ms; registry `sessions.log` scans occur only on 30s/forced advertise rounds and ledgers are capped 64 rows.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_Yfr0Yib4SAMAthregYgYyUqE","name":"irc"}]},"duration":13763.260099992156,"ttft":3744.88259999454,"contextSnapshot":{"promptTokens":135578,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Yfr0Yib4SAMAthregYgYyUqE|fc_050c9924c02ee1aa016a59d4981b8481959400a94a809e6a1f","toolName":"irc","startedAt":"2026-07-17T07:07:09.433Z","intent":"Clarifying dispatcher I/O"},"id":"92b2df93","parentId":"b0081238","timestamp":"2026-07-17T07:07:09.433Z"}
{"type":"message","id":"acc42993","parentId":"92b2df93","timestamp":"2026-07-17T07:07:09.434Z","message":{"role":"toolResult","toolCallId":"call_Yfr0Yib4SAMAthregYgYyUqE|fc_050c9924c02ee1aa016a59d4981b8481959400a94a809e6a1f","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- RedispatchCpuRca: injected"}],"details":{"op":"send","from":"BrainCpuRca","to":"RedispatchCpuRca","receipts":[{"to":"RedispatchCpuRca","outcome":"injected"}]},"isError":false,"timestamp":1784272029434}}
{"type":"message","id":"4ade0dc0","parentId":"acc42993","timestamp":"2026-07-17T07:07:13.709Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_050c9924c02ee1aa016a59d4a0bac08195995abbdf2ec3fa46\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4286,"output":4,"cacheRead":131584,"cacheWrite":0,"totalTokens":135874,"cost":{"input":0.02143,"output":0.00012,"cacheRead":0.065792,"cacheWrite":0,"total":0.087342}},"stopReason":"stop","timestamp":1784272029500,"responseId":"resp_050c9924c02ee1aa016a59d49df6448195aa0934a566455356","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_050c9924c02ee1aa016a59d4a0bac08195995abbdf2ec3fa46","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":4208.653899997473,"ttft":3257.1095000058413,"contextSnapshot":{"promptTokens":135870,"nonMessageTokens":15464}}}
{"type":"message","id":"c18a802c","parentId":"acc42993","timestamp":"2026-07-17T07:07:28.459Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming task completion and inbox check**"},{"type":"toolCall","id":"call_ObnQn5AWCRUrwbIPWeO2gQhI|fc_00a7b938fc7a39a6016a59d4b0639481949aaa6cc44dabe67f","name":"irc","arguments":{"i":"Checking investigation requests","op":"inbox","to":null,"message":null,"replyTo":null,"await":null,"from":null,"timeoutMs":null,"peek":false}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4315,"output":89,"cacheRead":131584,"cacheWrite":0,"totalTokens":135988,"reasoningTokens":32,"cost":{"input":0.021575,"output":0.00267,"cacheRead":0.065792,"cacheWrite":0,"total":0.090037}},"stopReason":"toolUse","timestamp":1784272033714,"responseId":"resp_00a7b938fc7a39a6016a59d4a22f688194b32ee16e12d9f103","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_00a7b938fc7a39a6016a59d4a725688194b8472a1fac4b695a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdSwNBcM2t1QuRw4p9QI5RHkQ-6FWQOk38cD8Bso_dnKMvvxydFXPMOhEWQoatvkGKMEdiFmVUwZWCY2PqxcSLo7jhr-O-0Q9cq3gW-k7cLJNSMGLgXK3xRAZZ1T3UGm7sTl7rhQc_gyVKmRls-8sTbxiWSX21DCInTsRWHr805j3xTLZ850QaiKwPZpXxXKAUCpabrj64GJKABZgfWEYBDMqniLS21eKjRqsPBrFQBXfSZ8vmhjCpRjDILdJfa2QEO91E4V1N0Iuyx_sXIxElthHrydI3VeJZTZuoCWNOlo94tVz7_FaRINEfp7oAUc5pIamSzto7d7STSwFJNvyIfkkNSwcdz1N-UeBdfnYoVXCsdKYohwFZRVLWTbQa9M7YtOkhTHV6o5LlG73revXBWCVuqKqrfcXY_2-bOEwo5An-MdnCf0PdU5BaxIpqE79bMEuQtQToHq9fhpsp4UXmBKjw6lco7H7VX-xj6ddRrTzR6EclMvCh_LCVRoOtipQvc4IMvsfXjWkV6aVNCJVK9yB2WTDQMe3lqIPeThnnkJ9QTZ7qBNNkaU3wpbpey3WE9V1ue46qIh5XNrf65lFibdDpPyky-2z0wtbTSv_y9bW1YYS2xfTcSBKRcB6OJCIRQHiFXVzR4OBbeyPL-MxAdZtkkUlAcDZnfXnJGovQ4XBqeyITViB9hjSZ1xCZlFBq_A-cwFvHfvOaqHbq8yAPKm5RiApRpTIXtjfvb1Ofxp0bmW0pD0D0zcDlqRHQ20GRBvQKHA-q_jmKswMVOUSe6Dq1g4753sf_3KWmMxxWICZHJhVT_3ZmKm9pxt1UvCztOrbVZmzZPYZXaYXvRoPBswqxEJ1p9GvovgX1lmTc1Tn2-3qAvVI5TW2e77cWWu7TKjHUTgEld4KShObBF0mlvLHogpgJtNHMb-CmCuZi1MgGlRU708sMbrAiIKxG7NReSppNTDRzyUWfp4kDApMviu2DXDjBzDaJX1kpy2LjZD9fsP58rseQR6bND_b9plihsbsP1ZhXw16p_tujVgj1c5Z-DqFDjGq7s4O5edOttHqycDBVgfuu8HHJGuoJrH0I8j-cAwiH6XCHVXOQDrB0OhkaMPR5YvAJjrRPZll7lckvY=","summary":[{"type":"summary_text","text":"**Confirming task completion and inbox check**"}]},{"id":"fc_00a7b938fc7a39a6016a59d4b0639481949aaa6cc44dabe67f","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking investigation requests\",\"op\":\"inbox\",\"to\":null,\"message\":null,\"replyTo\":null,\"await\":null,\"from\":null,\"timeoutMs\":null,\"peek\":false}","call_id":"call_ObnQn5AWCRUrwbIPWeO2gQhI","name":"irc"}]},"duration":14744.177699998021,"ttft":5474.073799997568,"contextSnapshot":{"promptTokens":135899,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ObnQn5AWCRUrwbIPWeO2gQhI|fc_00a7b938fc7a39a6016a59d4b0639481949aaa6cc44dabe67f","toolName":"irc","startedAt":"2026-07-17T07:07:28.459Z","intent":"Checking investigation requests"},"id":"884aed5a","parentId":"c18a802c","timestamp":"2026-07-17T07:07:28.459Z"}
{"type":"message","id":"9123f97a","parentId":"884aed5a","timestamp":"2026-07-17T07:07:28.460Z","message":{"role":"toolResult","toolCallId":"call_ObnQn5AWCRUrwbIPWeO2gQhI|fc_00a7b938fc7a39a6016a59d4b0639481949aaa6cc44dabe67f","toolName":"irc","content":[{"type":"text","text":"Inbox empty."}],"details":{"op":"inbox","from":"BrainCpuRca","inbox":[]},"isError":false,"useless":true,"timestamp":1784272048459}}
{"type":"message","id":"29ee37a8","parentId":"9123f97a","timestamp":"2026-07-17T07:08:07.794Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Refining concise terminal output**"},{"type":"toolCall","id":"call_SlphdYDDDrtWztKNhcjjQK0w|fc_00a7b938fc7a39a6016a59d4b4dc6081948a047a3107b222c5","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Confirmed brain root cause: `BRAIN_HEARTBEAT` is 500 ms (`crates/spt-daemon/src/brainproc.rs:63`); every steady heartbeat calls `write_ready` (`brainproc.rs:259-297`); `write_ready` calls `current_exe_hash` (`brainproc.rs:365-373`); and `current_exe_hash` reads the entire running executable into a fresh buffer and SHA-256 hashes it (`brainproc.rs:381-384`). Therefore predicted brain process read throughput is exactly `2 × deployed spt.exe length per second`. The observed 63.91 MiB/s predicts a 31.955 MiB executable. This also explains sustained CPU from allocation plus SHA-256. The source comment claiming one executable read per brain start is contradicted by the heartbeat call.\n\nRanked secondary mechanisms:\n\n1. Live sessions: `livehost.rs:637-639` creates one pulse thread per hosted live endpoint; default pulse cadence is 5 s (`config.rs:19-25`), with loop semantics at `lifecycle.rs:701-738`. This explains 4-5 intermittently active brain threads. Quiet ticks read small manifest/info/rest/drop state. Full transcript/history reads are gated, not unconditional: only an armed echo gate calls `fire_echo` (`lifecycle.rs:647-666,843-860`), which may read an entire raw transcript or `history.log` (`spt-live/src/history.rs:103-129`). Thus history can create bursts but does not fit the exact flat 63.91 MiB/s absent continuously re-armed echo events.\n2. Project index: 500 ms invalidation poll, 250 ms debounce, 5-minute backstop (`projwriter.rs:44-53,622-683`). Idle work is one short-circuiting `read_dir` of `$SPT_HOME/index/invalidations` (`spt-store/src/projinval.rs:90-95`). Git seed-store enumeration, changed-branch tree scans, perch enumeration, and `sessions.log` reads happen only on boot/invalidation/5-minute reconcile (`projwriter.rs:294-450`). `sessions.log` is capped at 64 rows (`spt-store/src/sessions.rs:28-32,98-107`). No worktree or transcript is scanned every 500 ms.\n3. Dispatcher: polls `net_streams` at 100 ms (`dispatch.rs:47-49,429-588`). This serializes the whole broker stream table over IPC before locally initiated rows are skipped, so an unbounded table of locally initiated Registry streams can explain broker named-pipe write volume and CPU. It does not cause brain filesystem `ReadFile` bytes. Successful served rows are retired; per-generation claims are terminal/retry bounded.\n4. Registry: outbound scheduler ticks every 200 ms but full registry work defaults to 30 s (`pump/mod.rs:96-102`; `config.rs:35-46`; `pump/registry.rs:120-155`). Full rounds enumerate bounded perch state and session ledgers and write registry snapshots (`registryhost.rs:399-632,738-765,893-919`). Inbound Registry workers subscribe and block on events until EOF (`dispatch.rs:970-1048`); they read small subnet/roster policy and write snapshots, not repositories or transcripts.\n5. Subscriber poison: each stream subscriber has a bounded dedicated writer that halts at its first failed SharedSend write (`nethost.rs:119-218`). Production write deadline is exactly 15 s (`broker.rs:167-188`). Repeated Registry poison at ~15 s therefore indicates a broker→brain IPC connection that is not draining, not a 15-second file scan. It can amplify broker threads and dispatch retries but is orthogonal to the brain executable reads.\n\nLive discriminating probes:\n\n* PowerShell: obtain PID 58272’s executable path/length and calculate `2 * Length / 1MB`; it should be approximately 63.91. This is the strongest quantitative falsifier.\n* Procmon filter `PID=58272`, `Operation=ReadFile`, `Path=<running spt.exe>`: expect about 20 whole-file sequential reads in 10 s. Exclude that path; sustained ~64 MiB/s should vanish.\n* Procmon filter `brain.ready`: expect tmp write + rename near 2 Hz, synchronized with executable reads.\n* Group remaining brain `ReadFile` bytes by path. Transcript or `<perch>\\history.log` reads should appear only on echo events and align to a 5 s pulse boundary; absent large history paths, session pulses explain active-thread count only.\n* Watch `project-index-stats.json`: `last_run_ms` and counters should change on invalidation or every ~5 minutes, not 500 ms. Idle Procmon should show only tiny invalidation-directory queries.\n* Group `DISPATCH_EV` by `(gen,stream,family,attempt,conn)`: unique Registry streams near 30 s are periodic feeds; the same stream at breaker delays 2→4→8→16→30 s is poison recovery; a Served stream reappearing means retirement failed/unsupported.\n* Correlate `CONN_LIFECYCLE` by stable conn ID (`conn.rs:229-275`): attach followed ~15,000 ms later by `CONN_WRITE_POISONED` proves deadline blackhole; immediate `CONN_WRITE_RETIRED` is organic disconnect. Compare attach/detach/writer-exit counts to distinguish bounded churn from a retained-thread leak.\n* Measure `net_streams` reply byte size at 10 Hz and row count. If broker writes scale linearly with accumulated locally initiated Registry rows while the same rows are skipped brain-side, that distinguishes stream-table retention from registry disk scans.","files":[{"path":"crates/spt-daemon/src/brainproc.rs","description":"Primary brain read/CPU cause: 500 ms heartbeat repeatedly recomputes readiness executable hash by reading all of `spt.exe`."},{"path":"crates/spt-daemon/src/livehost.rs","description":"One pulse thread per hosted live endpoint and 5-second live-host reconcile."},{"path":"crates/spt-daemon/src/lifecycle.rs","description":"Per-session pulse work and one-shot echo/history gate."},{"path":"crates/spt-live/src/history.rs","description":"Potential full transcript/history reads, only when history is explicitly fetched."},{"path":"crates/spt-daemon/src/projwriter.rs","description":"500 ms cheap invalidation polling and gated project-index reconciliation."},{"path":"crates/spt-store/src/projinval.rs","description":"Idle index poll is one short-circuiting directory enumeration."},{"path":"crates/spt-store/src/sessions.rs","description":"Session ledger reads are bounded by a 64-row on-disk cap."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"10 Hz full stream-table IPC query, bounded worker claims, Registry event arm, retry/breaker telemetry, and retirement."},{"path":"crates/spt-daemon/src/pump/mod.rs","description":"200 ms scheduler tick and due-only worker rounds."},{"path":"crates/spt-daemon/src/pump/registry.rs","description":"Registry marker/cadence pre-round and finite feed push."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"Bounded local advertisement scans and registry snapshot writes."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Per-subscriber writer thread, replay/live queue, poison and detach semantics."},{"path":"crates/spt-daemon/src/broker.rs","description":"15-second production brain write deadline and stream verbs."},{"path":"crates/spt-daemon/src/conn.rs","description":"Stable connection lifecycle telemetry and deadline-vs-organic poison attribution."},{"path":"crates/spt-store/src/atomic.rs","description":"Ready breadcrumb publication uses tmp write plus atomic rename."},{"path":"crates/spt-daemon/src/config.rs","description":"Default pulse and pump cadences."}],"architecture":"The brain main thread holds one broker IPC client and wakes every 500 ms for net status, session reap-driving, and readiness publication. Readiness publication currently performs a full executable read/hash, which deterministically dominates brain read bytes and CPU. Independent threads run live-host reconciliation plus one 5-second pulse driver per hosted endpoint, a single 500 ms project-invalidation watcher, a 100 ms dispatcher, and a 200 ms peer-pump scheduler. Dispatcher and pump share an in-memory RegistryHost. Outbound Registry rounds are cadence/marker gated; inbound Registry workers block on finite stream events and snapshot merged state. The dispatcher’s 10 Hz stream-table query transfers every row over broker IPC before skipping local rows, so retained locally initiated streams can amplify broker writes. Broker StreamLogs attach bounded per-subscriber writer threads; a 15-second blocked SharedSend poisons that connection and triggers paced redispatch. That subscriber lifecycle is a plausible broker thread/churn issue, but it is separate from the brain’s twice-per-second executable file reads."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4418,"output":1999,"cacheRead":131584,"cacheWrite":0,"totalTokens":138001,"reasoningTokens":63,"cost":{"input":0.022090000000000002,"output":0.05997,"cacheRead":0.065792,"cacheWrite":0,"total":0.147852}},"stopReason":"toolUse","timestamp":1784272048464,"responseId":"resp_00a7b938fc7a39a6016a59d4b0d99081948f87b725f0aa93a5","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_00a7b938fc7a39a6016a59d4b2e0cc8194a0e38178a791c631","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWdS0swHBK3kZWujMdpIVsVjzJ-zw3LeKpSh1H9h6LPvbMlZPbLlGzlykCUolaV2SYzSXm--Z7PWQdURRzrE_DLMpg4gvELe8soBN1pdo56m0iz-Uhilk5HqjezTNR7wWyfWKaxhfYZBJv-iZkD5_faFBRNWCFkXRlxuBLDCUofLTNZO_mBUbeJ5HJxmp0CwzUfASsuXPysBK80wAyz3zW3_4CZArowCI690eCz9sVnL1mkmWgegkauNg6Jk9LyVqY0-9EhL9CCcPLjC6UekvMNYUBWuTUIKtoJxt1nEbLy9x2pjE0xdtT_eV1DH9hVjYWU_Gv057O1qGvp23AhMGXxuK7NtrLE5SXlAFjQOMD8HoGri2gYbV-1ESeI-egZzJaSw0D32LzP7L3yhDqig_ucSEyMLuYiwIP5hPfiJe-k53OeEaVqKh6njrZpq9KhWTjbb8DxIMOYXrYDQe--2LSlADhGsa5NToA4HQeThBiC_YAXQcu3q5g9UOUcNl0fiMamlJpe5L2wF5FpixTrmzCZnd_bTN-i6pQARHEwAQGZi0WnQXoMmxpADJYziI2v7i93bwuN0KSuWMQp5ZCYfn4JN2jGO9mHPutjZvgb0XUdykX5ju2tPq65PK8P1SeQfGyMWHdLNHo7b_F_jKTTtwTEhYBfh7sQj6DWmuL08RbK5QvdOBcUFzCkL9rnmHBEiSMXhyt1n1dR8t_89LIfRMZn-iHbqhfYW2gywTVBpYkWaZK_g-4SgUmbc5w6JB6AjKh1qkUL0EwKK8RktOhmhxS6QyyoN-IWK7rJ7eoR6VsqPt48iG0uRtahBSqunzaWxZzUMZPvALdzRow4ZdeeIDKcji2-LmppmmoxvszLO-tnl1pEZ8L-f7YZkiRLDQwWA4s-W3_A0Srvjpztb-n5gxrE-b_Jj8He0skqM4hZyB4VKKDu3VK4Qs-dhZHRqkbWSKUxfYWOS19qOUAKUO9kDBovnniM5ePtPRN27Se0XRWXS1HNr-976Y6cNWWTP9WuXZFZ60pxeSE4VuJ8oD8fq7V4FyxqJPgo7R4znbE5OHN8SkHUn67gzQ0ibDWac_JwInPJfUFERTgYiZVm_jbjnIWU1P4A20iN8IFklpqChblvWvlcyarxCAO8AEJaeF0kEmEBCzZ5CIt7ZeWw_OpKoyrEUl_sf8he0w0cpI8NLkLeEyWYNLnPvwrb6EsZaujjfSk2zjW6U_K2M1NQlz6ibiegfZxjlfkB5yTOO0oegvQW8uQQXnydl_HcNrsUjfGeAp3jqGL9Z8YD9LNdLKSybDdZE2NzCJ5A9mj3KxWLJDoRijO9UjMwDwd36nqIOqSGE3bCUzzmiQ7FOJJnt69YoTy73RKw==","summary":[{"type":"summary_text","text":"**Refining concise terminal output**"}]},{"id":"fc_00a7b938fc7a39a6016a59d4b4dc6081948a047a3107b222c5","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Confirmed brain root cause: `BRAIN_HEARTBEAT` is 500 ms (`crates/spt-daemon/src/brainproc.rs:63`); every steady heartbeat calls `write_ready` (`brainproc.rs:259-297`); `write_ready` calls `current_exe_hash` (`brainproc.rs:365-373`); and `current_exe_hash` reads the entire running executable into a fresh buffer and SHA-256 hashes it (`brainproc.rs:381-384`). Therefore predicted brain process read throughput is exactly `2 × deployed spt.exe length per second`. The observed 63.91 MiB/s predicts a 31.955 MiB executable. This also explains sustained CPU from allocation plus SHA-256. The source comment claiming one executable read per brain start is contradicted by the heartbeat call.\\n\\nRanked secondary mechanisms:\\n\\n1. Live sessions: `livehost.rs:637-639` creates one pulse thread per hosted live endpoint; default pulse cadence is 5 s (`config.rs:19-25`), with loop semantics at `lifecycle.rs:701-738`. This explains 4-5 intermittently active brain threads. Quiet ticks read small manifest/info/rest/drop state. Full transcript/history reads are gated, not unconditional: only an armed echo gate calls `fire_echo` (`lifecycle.rs:647-666,843-860`), which may read an entire raw transcript or `history.log` (`spt-live/src/history.rs:103-129`). Thus history can create bursts but does not fit the exact flat 63.91 MiB/s absent continuously re-armed echo events.\\n2. Project index: 500 ms invalidation poll, 250 ms debounce, 5-minute backstop (`projwriter.rs:44-53,622-683`). Idle work is one short-circuiting `read_dir` of `$SPT_HOME/index/invalidations` (`spt-store/src/projinval.rs:90-95`). Git seed-store enumeration, changed-branch tree scans, perch enumeration, and `sessions.log` reads happen only on boot/invalidation/5-minute reconcile (`projwriter.rs:294-450`). `sessions.log` is capped at 64 rows (`spt-store/src/sessions.rs:28-32,98-107`). No worktree or transcript is scanned every 500 ms.\\n3. Dispatcher: polls `net_streams` at 100 ms (`dispatch.rs:47-49,429-588`). This serializes the whole broker stream table over IPC before locally initiated rows are skipped, so an unbounded table of locally initiated Registry streams can explain broker named-pipe write volume and CPU. It does not cause brain filesystem `ReadFile` bytes. Successful served rows are retired; per-generation claims are terminal/retry bounded.\\n4. Registry: outbound scheduler ticks every 200 ms but full registry work defaults to 30 s (`pump/mod.rs:96-102`; `config.rs:35-46`; `pump/registry.rs:120-155`). Full rounds enumerate bounded perch state and session ledgers and write registry snapshots (`registryhost.rs:399-632,738-765,893-919`). Inbound Registry workers subscribe and block on events until EOF (`dispatch.rs:970-1048`); they read small subnet/roster policy and write snapshots, not repositories or transcripts.\\n5. Subscriber poison: each stream subscriber has a bounded dedicated writer that halts at its first failed SharedSend write (`nethost.rs:119-218`). Production write deadline is exactly 15 s (`broker.rs:167-188`). Repeated Registry poison at ~15 s therefore indicates a broker→brain IPC connection that is not draining, not a 15-second file scan. It can amplify broker threads and dispatch retries but is orthogonal to the brain executable reads.\\n\\nLive discriminating probes:\\n\\n* PowerShell: obtain PID 58272’s executable path/length and calculate `2 * Length / 1MB`; it should be approximately 63.91. This is the strongest quantitative falsifier.\\n* Procmon filter `PID=58272`, `Operation=ReadFile`, `Path=<running spt.exe>`: expect about 20 whole-file sequential reads in 10 s. Exclude that path; sustained ~64 MiB/s should vanish.\\n* Procmon filter `brain.ready`: expect tmp write + rename near 2 Hz, synchronized with executable reads.\\n* Group remaining brain `ReadFile` bytes by path. Transcript or `<perch>\\\\history.log` reads should appear only on echo events and align to a 5 s pulse boundary; absent large history paths, session pulses explain active-thread count only.\\n* Watch `project-index-stats.json`: `last_run_ms` and counters should change on invalidation or every ~5 minutes, not 500 ms. Idle Procmon should show only tiny invalidation-directory queries.\\n* Group `DISPATCH_EV` by `(gen,stream,family,attempt,conn)`: unique Registry streams near 30 s are periodic feeds; the same stream at breaker delays 2→4→8→16→30 s is poison recovery; a Served stream reappearing means retirement failed/unsupported.\\n* Correlate `CONN_LIFECYCLE` by stable conn ID (`conn.rs:229-275`): attach followed ~15,000 ms later by `CONN_WRITE_POISONED` proves deadline blackhole; immediate `CONN_WRITE_RETIRED` is organic disconnect. Compare attach/detach/writer-exit counts to distinguish bounded churn from a retained-thread leak.\\n* Measure `net_streams` reply byte size at 10 Hz and row count. If broker writes scale linearly with accumulated locally initiated Registry rows while the same rows are skipped brain-side, that distinguishes stream-table retention from registry disk scans.\",\"files\":[{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"Primary brain read/CPU cause: 500 ms heartbeat repeatedly recomputes readiness executable hash by reading all of `spt.exe`.\"},{\"path\":\"crates/spt-daemon/src/livehost.rs\",\"description\":\"One pulse thread per hosted live endpoint and 5-second live-host reconcile.\"},{\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"description\":\"Per-session pulse work and one-shot echo/history gate.\"},{\"path\":\"crates/spt-live/src/history.rs\",\"description\":\"Potential full transcript/history reads, only when history is explicitly fetched.\"},{\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"description\":\"500 ms cheap invalidation polling and gated project-index reconciliation.\"},{\"path\":\"crates/spt-store/src/projinval.rs\",\"description\":\"Idle index poll is one short-circuiting directory enumeration.\"},{\"path\":\"crates/spt-store/src/sessions.rs\",\"description\":\"Session ledger reads are bounded by a 64-row on-disk cap.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"10 Hz full stream-table IPC query, bounded worker claims, Registry event arm, retry/breaker telemetry, and retirement.\"},{\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"description\":\"200 ms scheduler tick and due-only worker rounds.\"},{\"path\":\"crates/spt-daemon/src/pump/registry.rs\",\"description\":\"Registry marker/cadence pre-round and finite feed push.\"},{\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"description\":\"Bounded local advertisement scans and registry snapshot writes.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Per-subscriber writer thread, replay/live queue, poison and detach semantics.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"15-second production brain write deadline and stream verbs.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"Stable connection lifecycle telemetry and deadline-vs-organic poison attribution.\"},{\"path\":\"crates/spt-store/src/atomic.rs\",\"description\":\"Ready breadcrumb publication uses tmp write plus atomic rename.\"},{\"path\":\"crates/spt-daemon/src/config.rs\",\"description\":\"Default pulse and pump cadences.\"}],\"architecture\":\"The brain main thread holds one broker IPC client and wakes every 500 ms for net status, session reap-driving, and readiness publication. Readiness publication currently performs a full executable read/hash, which deterministically dominates brain read bytes and CPU. Independent threads run live-host reconciliation plus one 5-second pulse driver per hosted endpoint, a single 500 ms project-invalidation watcher, a 100 ms dispatcher, and a 200 ms peer-pump scheduler. Dispatcher and pump share an in-memory RegistryHost. Outbound Registry rounds are cadence/marker gated; inbound Registry workers block on finite stream events and snapshot merged state. The dispatcher’s 10 Hz stream-table query transfers every row over broker IPC before skipping local rows, so retained locally initiated streams can amplify broker writes. Broker StreamLogs attach bounded per-subscriber writer threads; a 15-second blocked SharedSend poisons that connection and triggers paced redispatch. That subscriber lifecycle is a plausible broker thread/churn issue, but it is separate from the brain’s twice-per-second executable file reads.\"}}}","call_id":"call_SlphdYDDDrtWztKNhcjjQK0w","name":"yield"}]},"duration":39327.769899994135,"ttft":2424.1735000014305,"contextSnapshot":{"promptTokens":136002,"nonMessageTokens":15464}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_SlphdYDDDrtWztKNhcjjQK0w|fc_00a7b938fc7a39a6016a59d4b4dc6081948a047a3107b222c5","toolName":"yield","startedAt":"2026-07-17T07:08:07.794Z"},"id":"102315bd","parentId":"29ee37a8","timestamp":"2026-07-17T07:08:07.794Z"}
{"type":"message","id":"2e66ff30","parentId":"102315bd","timestamp":"2026-07-17T07:08:07.794Z","message":{"role":"toolResult","toolCallId":"call_SlphdYDDDrtWztKNhcjjQK0w|fc_00a7b938fc7a39a6016a59d4b4dc6081948a047a3107b222c5","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Confirmed brain root cause: `BRAIN_HEARTBEAT` is 500 ms (`crates/spt-daemon/src/brainproc.rs:63`); every steady heartbeat calls `write_ready` (`brainproc.rs:259-297`); `write_ready` calls `current_exe_hash` (`brainproc.rs:365-373`); and `current_exe_hash` reads the entire running executable into a fresh buffer and SHA-256 hashes it (`brainproc.rs:381-384`). Therefore predicted brain process read throughput is exactly `2 × deployed spt.exe length per second`. The observed 63.91 MiB/s predicts a 31.955 MiB executable. This also explains sustained CPU from allocation plus SHA-256. The source comment claiming one executable read per brain start is contradicted by the heartbeat call.\n\nRanked secondary mechanisms:\n\n1. Live sessions: `livehost.rs:637-639` creates one pulse thread per hosted live endpoint; default pulse cadence is 5 s (`config.rs:19-25`), with loop semantics at `lifecycle.rs:701-738`. This explains 4-5 intermittently active brain threads. Quiet ticks read small manifest/info/rest/drop state. Full transcript/history reads are gated, not unconditional: only an armed echo gate calls `fire_echo` (`lifecycle.rs:647-666,843-860`), which may read an entire raw transcript or `history.log` (`spt-live/src/history.rs:103-129`). Thus history can create bursts but does not fit the exact flat 63.91 MiB/s absent continuously re-armed echo events.\n2. Project index: 500 ms invalidation poll, 250 ms debounce, 5-minute backstop (`projwriter.rs:44-53,622-683`). Idle work is one short-circuiting `read_dir` of `$SPT_HOME/index/invalidations` (`spt-store/src/projinval.rs:90-95`). Git seed-store enumeration, changed-branch tree scans, perch enumeration, and `sessions.log` reads happen only on boot/invalidation/5-minute reconcile (`projwriter.rs:294-450`). `sessions.log` is capped at 64 rows (`spt-store/src/sessions.rs:28-32,98-107`). No worktree or transcript is scanned every 500 ms.\n3. Dispatcher: polls `net_streams` at 100 ms (`dispatch.rs:47-49,429-588`). This serializes the whole broker stream table over IPC before locally initiated rows are skipped, so an unbounded table of locally initiated Registry streams can explain broker named-pipe write volume and CPU. It does not cause brain filesystem `ReadFile` bytes. Successful served rows are retired; per-generation claims are terminal/retry bounded.\n4. Registry: outbound scheduler ticks every 200 ms but full registry work defaults to 30 s (`pump/mod.rs:96-102`; `config.rs:35-46`; `pump/registry.rs:120-155`). Full rounds enumerate bounded perch state and session ledgers and write registry snapshots (`registryhost.rs:399-632,738-765,893-919`). Inbound Registry workers subscribe and block on events until EOF (`dispatch.rs:970-1048`); they read small subnet/roster policy and write snapshots, not repositories or transcripts.\n5. Subscriber poison: each stream subscriber has a bounded dedicated writer that halts at its first failed SharedSend write (`nethost.rs:119-218`). Production write deadline is exactly 15 s (`broker.rs:167-188`). Repeated Registry poison at ~15 s therefore indicates a broker→brain IPC connection that is not draining, not a 15-second file scan. It can amplify broker threads and dispatch retries but is orthogonal to the brain executable reads.\n\nLive discriminating probes:\n\n* PowerShell: obtain PID 58272’s executable path/length and calculate `2 * Length / 1MB`; it should be approximately 63.91. This is the strongest quantitative falsifier.\n* Procmon filter `PID=58272`, `Operation=ReadFile`, `Path=<running spt.exe>`: expect about 20 whole-file sequential reads in 10 s. Exclude that path; sustained ~64 MiB/s should vanish.\n* Procmon filter `brain.ready`: expect tmp write + rename near 2 Hz, synchronized with executable reads.\n* Group remaining brain `ReadFile` bytes by path. Transcript or `<perch>\\history.log` reads should appear only on echo events and align to a 5 s pulse boundary; absent large history paths, session pulses explain active-thread count only.\n* Watch `project-index-stats.json`: `last_run_ms` and counters should change on invalidation or every ~5 minutes, not 500 ms. Idle Procmon should show only tiny invalidation-directory queries.\n* Group `DISPATCH_EV` by `(gen,stream,family,attempt,conn)`: unique Registry streams near 30 s are periodic feeds; the same stream at breaker delays 2→4→8→16→30 s is poison recovery; a Served stream reappearing means retirement failed/unsupported.\n* Correlate `CONN_LIFECYCLE` by stable conn ID (`conn.rs:229-275`): attach followed ~15,000 ms later by `CONN_WRITE_POISONED` proves deadline blackhole; immediate `CONN_WRITE_RETIRED` is organic disconnect. Compare attach/detach/writer-exit counts to distinguish bounded churn from a retained-thread leak.\n* Measure `net_streams` reply byte size at 10 Hz and row count. If broker writes scale linearly with accumulated locally initiated Registry rows while the same rows are skipped brain-side, that distinguishes stream-table retention from registry disk scans.","files":[{"path":"crates/spt-daemon/src/brainproc.rs","description":"Primary brain read/CPU cause: 500 ms heartbeat repeatedly recomputes readiness executable hash by reading all of `spt.exe`."},{"path":"crates/spt-daemon/src/livehost.rs","description":"One pulse thread per hosted live endpoint and 5-second live-host reconcile."},{"path":"crates/spt-daemon/src/lifecycle.rs","description":"Per-session pulse work and one-shot echo/history gate."},{"path":"crates/spt-live/src/history.rs","description":"Potential full transcript/history reads, only when history is explicitly fetched."},{"path":"crates/spt-daemon/src/projwriter.rs","description":"500 ms cheap invalidation polling and gated project-index reconciliation."},{"path":"crates/spt-store/src/projinval.rs","description":"Idle index poll is one short-circuiting directory enumeration."},{"path":"crates/spt-store/src/sessions.rs","description":"Session ledger reads are bounded by a 64-row on-disk cap."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"10 Hz full stream-table IPC query, bounded worker claims, Registry event arm, retry/breaker telemetry, and retirement."},{"path":"crates/spt-daemon/src/pump/mod.rs","description":"200 ms scheduler tick and due-only worker rounds."},{"path":"crates/spt-daemon/src/pump/registry.rs","description":"Registry marker/cadence pre-round and finite feed push."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"Bounded local advertisement scans and registry snapshot writes."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Per-subscriber writer thread, replay/live queue, poison and detach semantics."},{"path":"crates/spt-daemon/src/broker.rs","description":"15-second production brain write deadline and stream verbs."},{"path":"crates/spt-daemon/src/conn.rs","description":"Stable connection lifecycle telemetry and deadline-vs-organic poison attribution."},{"path":"crates/spt-store/src/atomic.rs","description":"Ready breadcrumb publication uses tmp write plus atomic rename."},{"path":"crates/spt-daemon/src/config.rs","description":"Default pulse and pump cadences."}],"architecture":"The brain main thread holds one broker IPC client and wakes every 500 ms for net status, session reap-driving, and readiness publication. Readiness publication currently performs a full executable read/hash, which deterministically dominates brain read bytes and CPU. Independent threads run live-host reconciliation plus one 5-second pulse driver per hosted endpoint, a single 500 ms project-invalidation watcher, a 100 ms dispatcher, and a 200 ms peer-pump scheduler. Dispatcher and pump share an in-memory RegistryHost. Outbound Registry rounds are cadence/marker gated; inbound Registry workers block on finite stream events and snapshot merged state. The dispatcher’s 10 Hz stream-table query transfers every row over broker IPC before skipping local rows, so retained locally initiated streams can amplify broker writes. Broker StreamLogs attach bounded per-subscriber writer threads; a 15-second blocked SharedSend poisons that connection and triggers paced redispatch. That subscriber lifecycle is a plausible broker thread/churn issue, but it is separate from the brain’s twice-per-second executable file reads."},"status":"success","type":"result"},"isError":false,"timestamp":1784272087794}}
{"type":"message","id":"eb768466","parentId":"2e66ff30","timestamp":"2026-07-17T07:08:11.237Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_00a7b938fc7a39a6016a59d4da5a7481948b5ebaec9218faf0\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":6431,"output":4,"cacheRead":131584,"cacheWrite":0,"totalTokens":138019,"cost":{"input":0.032155,"output":0.00012,"cacheRead":0.065792,"cacheWrite":0,"total":0.09806700000000002}},"stopReason":"stop","timestamp":1784272087806,"responseId":"resp_00a7b938fc7a39a6016a59d4d853408194ac010f20404c8f33","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_00a7b938fc7a39a6016a59d4da5a7481948b5ebaec9218faf0","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":3431.209100008011,"ttft":2562.3540000021458,"contextSnapshot":{"promptTokens":138015,"nonMessageTokens":15464}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-17T07:15:11.252Z"},"id":"321408a7","parentId":"eb768466","timestamp":"2026-07-17T07:15:11.252Z"}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `V037GaugeProbe`:\n\nI’m tracing v0.37 broker thread census/gauges. Any field probe/log anchors or interpretation of 47–56 total, 23–26 unnamed, 3 sub-writer threads? Concise reply appreciated.\nIf a response is expected, reply with the `irc` tool (`op: \"send\"`, `to: \"V037GaugeProbe\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"1533574ea69c5f59","from":"V037GaugeProbe","message":"I’m tracing v0.37 broker thread census/gauges. Any field probe/log anchors or interpretation of 47–56 total, 23–26 unnamed, 3 sub-writer threads? Concise reply appreciated."},"attribution":"agent","id":"fad52002","parentId":"321408a7","timestamp":"2026-07-17T12:58:12.051Z"}
{"type":"message","id":"36848ac4","parentId":"fad52002","timestamp":"2026-07-17T12:58:28.145Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing subscriber writer thread counts**"},{"type":"toolCall","id":"call_ZYs5LmAzm7IiMgPjIge2qiMx|fc_0300178329ef0cc5016a5a26ed4690819085059940a05bbce8","name":"irc","arguments":{"i":"Interpreting thread census","op":"send","to":"V037GaugeProbe","message":"`SubscriberSeat::install` uses unnamed `std::thread::spawn` (`nethost.rs:180-212`), so 3 sub-writer threads is consistent with 3 currently installed stream/presence seats, not itself a leak. Healthy discriminator: `CONN_LIFECYCLE ... event=stream-sub-attach` must pair with conn detach/retirement; poison shows `stream-sub-writer-poison`; same conn/stream accumulating writers or total unnamed rising monotonically is leak. `conn-watchdog` is explicitly named per physical broker conn (`conn.rs:449-453`), so 47–56 total / 23–26 unnamed is dramatically bounded versus 550; correlate named watchdog count with physical conn count and 10Hz stream-table row count. Registry feed streams should serve EOF+retire; retained locally initiated rows inflate table/IPC but should not each retain a subscriber writer.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":138142,"output":368,"cacheRead":0,"cacheWrite":0,"totalTokens":138510,"reasoningTokens":122,"cost":{"input":0.69071,"output":0.01104,"cacheRead":0,"cacheWrite":0,"total":0.7017500000000001}},"stopReason":"toolUse","timestamp":1784293092092,"responseId":"resp_0300178329ef0cc5016a5a26e4e0348190bc81d4da27d625db","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0300178329ef0cc5016a5a26e80cf48190963a6bb7efc66700","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWibtq6WJIcTnEbxNS6RZTfsPB0dI6vc7vNgH5SFlX2uowKMToUWgzFzqTcjs4rx2WMftz7MqCEQ6lwcCXh6A2F8q6_FR8d-iOav2FBKgmEgoKUDGUAAgPymLZadINBsCByviwdYiHy5s6b8FCE6OE89DEAs0RV8NkNszjqn4jX0ULjIrhc_JLJ5hM7bcR97I0uaBGU-aVXpRsK39fPv1ucnFP0ueBXpaz1RzonBkhwQ3RdraB_lVwwR9eQV59-kj15o3YE1MIEHKZLmoELjM7pQ1N8DA-o8uLZeMM4Y5ViH4Mhr3L_rnMGZCP8i1QOmXl10WELWNnLBYRlmtxAowKKfsSqpBZ1KN4mO13qFIXF99reH9Zi3DbpHpE43NJmmyfWsU3Q5NxWbReij8b9dSwB8frQ63nKzKZ8xhTUJJ6y5fvpJayfxnevAtZQQr50iUfR4gbrg32a_N0JB0AOfXmY-vXPmhu6XSuwaI0SSLvCyVobhnBut5SUgej2Oyw_Ku_gWTLxbModuwVXkOecUi6C0gAQniLgIw4DYAI4V51Qiht-L3uq0uEP1FiC1ekSs7O8pOWzA9HXUJT4kFCWSKguktZWDsJP8m38NRHS2KthWU2-doA9UlONap6HaqqBCuk8xTBq0QhuC3eg4k4wuMZ9Y3HaD3pyc8h5asDwPpNJG6weTdiDmYTICP7KZEnXs3L_G5qRZGqynsckqk8TR3_OFXC0w0bneQbNehlh9Coxx8iT-7NiY4q9bo3avMXA2MHXpGb1UIsnBzA9rQKJy32MVtbYDBzeK-pzYCus-M_6dxCg6Obh7XFug53_zgBWLaNxG1KZxhmyxQ7H1OZuRIPmg-tI3OLJ0FosceOXvb4vm-sS6OM4AI1iblTlRyCdYOWYJNSFAynIqqq7X40F9svYanD5CaKXeFFPNLMLCF_HRAJdXjwOjQMwLcpbTyifGlZgVFHpOuURli-z4ociLNfRgDQW__dpajXIUCo65NzPJMOt-XRqtwBw2VZoSvPKbyMOXmrBUduyLBgCUrLx44_uds05lbRRiPqmMga9rwM7o-8fCIJVPjO2Z7C-VCjsJII5ppMU0SUQ1mwMu6CVnGi5bdpvD7vmFZY451F5cqYTw7Rrh4rStQQXF1dDMLmh9KcoTc7A2cPWkNw5KMh_ZIadqFFY69Q_tYtnqgy5RjFiWxUQ0kpoKS-YwZiD841_PIE9b8-F12_bu561om4JLgTiJMIXRzcrvMz4ycmFl3Y8Yc9u3-rBUNAzxoBwapLLqL6LAPvHeB16QiM7fGoDi-55Krt5q0qK7veFSjddWixKXHbOGSKreHo7LLKPbK2rcMFOMMCyGVQ0GkfSSkeinOcsvbabgK7V5518BYZAP60xr-ioevCBNbFwaHXj4ffm0idwWM0iiQQQ6ut8LOVxZnoj92YTO30Kt-7cJ3ArgFE1HdRx5wdIeA_Ja72tlt8z3KNoUSQR7-liTvTzY3RJdnxada1GjFjZ8upZ1ABmDGUPZ5YyolJh9Uw9lPZ2FZVg5z8S6VlF9HIj76BIl_41iGI_Q-0tPdJ1XASI4PPJCEsKLDP7AwmPmlO1LcG4n2qqZFiEgWUXpqbUikSGILuyvYs-H1Lhb-ZNU0ok_XRdBYajEeXuBVIgMzfnz33Pkfuq4Su3bH","summary":[{"type":"summary_text","text":"**Assessing subscriber writer thread counts**"}]},{"id":"fc_0300178329ef0cc5016a5a26ed4690819085059940a05bbce8","type":"function_call","status":"completed","arguments":"{\"i\":\"Interpreting thread census\",\"op\":\"send\",\"to\":\"V037GaugeProbe\",\"message\":\"`SubscriberSeat::install` uses unnamed `std::thread::spawn` (`nethost.rs:180-212`), so 3 sub-writer threads is consistent with 3 currently installed stream/presence seats, not itself a leak. Healthy discriminator: `CONN_LIFECYCLE ... event=stream-sub-attach` must pair with conn detach/retirement; poison shows `stream-sub-writer-poison`; same conn/stream accumulating writers or total unnamed rising monotonically is leak. `conn-watchdog` is explicitly named per physical broker conn (`conn.rs:449-453`), so 47–56 total / 23–26 unnamed is dramatically bounded versus 550; correlate named watchdog count with physical conn count and 10Hz stream-table row count. Registry feed streams should serve EOF+retire; retained locally initiated rows inflate table/IPC but should not each retain a subscriber writer.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_ZYs5LmAzm7IiMgPjIge2qiMx","name":"irc"}]},"duration":16050.845500007272,"ttft":3974.0606999993324,"contextSnapshot":{"promptTokens":138142,"nonMessageTokens":15513}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZYs5LmAzm7IiMgPjIge2qiMx|fc_0300178329ef0cc5016a5a26ed4690819085059940a05bbce8","toolName":"irc","startedAt":"2026-07-17T12:58:28.145Z","intent":"Interpreting thread census"},"id":"d3aa2bd5","parentId":"36848ac4","timestamp":"2026-07-17T12:58:28.145Z"}
{"type":"message","id":"209666f9","parentId":"d3aa2bd5","timestamp":"2026-07-17T12:58:28.145Z","message":{"role":"toolResult","toolCallId":"call_ZYs5LmAzm7IiMgPjIge2qiMx|fc_0300178329ef0cc5016a5a26ed4690819085059940a05bbce8","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- V037GaugeProbe: injected"}],"details":{"op":"send","from":"BrainCpuRca","to":"V037GaugeProbe","receipts":[{"to":"V037GaugeProbe","outcome":"injected"}]},"isError":false,"timestamp":1784293108145}}
{"type":"message","id":"dde24030","parentId":"209666f9","timestamp":"2026-07-17T12:58:32.016Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming function completion without yield**"},{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_0300178329ef0cc5016a5a26f761608190868fedf6ec6ea898\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4903,"output":29,"cacheRead":133632,"cacheWrite":0,"totalTokens":138564,"reasoningTokens":23,"cost":{"input":0.024515000000000002,"output":0.00087,"cacheRead":0.066816,"cacheWrite":0,"total":0.092201}},"stopReason":"stop","timestamp":1784293108157,"responseId":"resp_0300178329ef0cc5016a5a26f492908190b08cc09f226e4aa1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0300178329ef0cc5016a5a26f60e2c8190a13684dd9b9c6540","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWib3CrpsQrJMBST4XpCPLq-eX6XqLD3KTQfsUYxdcxF1-oCLKiVDeuVWTX4RUkmcTqFfMX59yUyZi8Nn5fESZcVLmUEFh_-dUnDuDRc9uL-N3cEE4C1lVX7Wn70KIrGf0jIF3GE4zcG-b4pP_IQmH-IiWM5z9jPLuMnKHE8jWxG_o4rj7hLrgrPf7LADYKeDzkkCuegjpC5PySbZ3C8Rumj6j0mB8hCt2qIJfurW47lZ9U8mNIGLDySUgCKFcHf7i7rp4DaZZqbhTkw0mdMo8RIS6-Zd0hQfza_6Dxi2cN_LfMml_y4-lu4zA0R7nqS3adumwa5at8H1HXFBaEZKj58UpBYqBswF2i-MzqQ-vwmXfF0BZ4YHEZhZqfpquqfrJ3ihPfY76B15y8B2_bJ2yWR3A0dtIY4q20noq8lVWqqWcNU9Dlwx2ePCWsNEMgsjQFCMLFcSagpWxtMbnPuC0NiRONeMggBZsikXAW3c_6kaVcRVXxwySzek-eG45xHS4U3Keys4-vRslb8KGDyJWHfYwr2MycsMyhWcQnvkyL-O0nyVn4f9s0EGKr9FI2_kc767PKTAutjhmw6NFY2EkWlazU8usj-aSQemXv6BtaDHPuJMhlfrmr5EvpwWls8rdPuZnn3-LxdS9B5sbwrvQVQnxqpN-hjcpQ4g_gcqneBsuOgY3g-Z8nwKMYrMS0UabpQL_PEnhk6MTLNYYSj5LIjjx4VL6GJMdSFNJdEutKuoE0KktikW74UZ1ABWIgLxnREJEJIqXT3_j40WoswDaJhUdXmxLO8Sili4-UIdUV9-7N8EV8bQpTkOftX7xkrw6XRvz6p9JV7BTKuYbeB4UzLG8oBML4FsW9UMpGGSnfGco6zt23YxZCNoFKPz8FKTQayqf33iqbhSNZGClVSw7HrX_EFwH3XSDOElA4k2MUrheKC4NsIZs-XjhxDH7Xc99o-evLJTw9UqelqzOeLzLvPmoHtjEzMRXiPMnRceZ0RQ4LEt_AaNGPbC23tDiLbZmogz-IBPQDtHITVf3u-GZUc8sDzSMrFi2yC2jF2yQcFyaMI=","summary":[{"type":"summary_text","text":"**Confirming function completion without yield**"}]},{"id":"msg_0300178329ef0cc5016a5a26f761608190868fedf6ec6ea898","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":3858.4686000049114,"ttft":1914.7203000038862,"contextSnapshot":{"promptTokens":138535,"nonMessageTokens":15513}}}
{"type":"message","id":"3fa30a53","parentId":"209666f9","timestamp":"2026-07-17T12:58:42.920Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning dispatcher update and tool continuation**"},{"type":"toolCall","id":"call_STNGIoq467DZTjSJGqwJValJ|fc_097e09b8a38fb850016a5a26fe08ec8193a0cb70c2b664b4d9","name":"irc","arguments":{"i":"Updating complete brain findings","op":"send","to":"Main","message":"Addendum from Redispatch comparison: dispatcher `net_streams()` at 10Hz serializes the entire in-memory stream table before brain skips `initiated_locally` rows (`dispatch.rs:458-465`). Thus accumulated locally initiated Registry rows plausibly explain broker pipe-write CPU/I/O, while brain filesystem 63.91 MiB/s remains exact 2×exe heartbeat hash. Registry `sessions.log` scans are only 30s/forced advertise and capped 64 rows. Full structured result yielded.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3908,"output":204,"cacheRead":134656,"cacheWrite":0,"totalTokens":138768,"reasoningTokens":46,"cost":{"input":0.019540000000000002,"output":0.0061200000000000004,"cacheRead":0.067328,"cacheWrite":0,"total":0.092988}},"stopReason":"toolUse","timestamp":1784293112019,"responseId":"resp_097e09b8a38fb850016a5a26f872a881939da57ce6ec5536bc","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_097e09b8a38fb850016a5a26fc16c88193b3da758102098d64","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWib-g3ApPCWQPBw40yRxCSRWN1UYQ-yciL-76z1ePFkUiHGdyxHA1NlyV_2Aj-o5U_ZDfy8Gb1jLfBGRWx8RPy46D9OjG1qliLdTieAxAstGxVnXH6v28a0_oqn1oGO4VLCNNbc-9SPRfSZnM8lnQb1j0lhwifPJ-gMv4UuWZYbh006ZNimv-CMwuNRsLYuwD4PDX0j2m0S3lkXYpsaMxSqTAp-C7892UWlhyrQiO5UsHvDqp07JhAHguhPNxtNoq_IbnyAwW-9c7gIOW6RnW4fqg5WpCtTzneW3i9XsPYfG_FvkA05nRkQGW3cxd7KvfUeFzF3Bm5JmqXteQ9DiOO-misJE3BPifr4ul0suUyhsQkjYQ-Im0maRg4e-fJgB4jNJ0J_LvhA8Ce-ggj3lltwtCDi3xkj_MMNNYfAV9SUWnTjQqclNlAu2R1h_UIw02XTF5Aq6EOHRAGnIIYnH8Hnb7v_IDDCfGB_ADM-azj51PWen7G4uLt8GGZRIowrgA9LRIVR2ANJ72XdjYtNSrjb7bvIk-doFfx_jYVNRnPjf-hCFmkqZCwumZUk-fg7pCFMyaI2aA_nDToHtLFAhisOI89kwjBV_mY757mmx5Fm0aX8uFhfETQH8qK7lRqXB356ynVBNKtGNm207sXSOsX4Xv3NOBAh7oR-03bqYuwfPHAK4o8dLm_xJBxeO_ex0XG3T-WC8iva4HsvWUTlC6tdccBU5KHLTk79yDD-ucdqHrBxHal-Xh6nMLYmr84VTVqqd3g0LD8DHX3_ae9WiVcdTRy_vmHg1rn-XRQwEuu-piTDj3Q9wJTUakvc9GneM5bDiQOodZAE9MA73V9XWHs3mUgLn8V47VODUTeMLNw5A68n7GMMsSLiTWvwGENh6fUojhdV4g48XbkagW0JSadQSNWBw8jbsN2p8FS5f_hJpTIB7luTn2cuAzLn9jL5F4720qbHxxOikfItK4-oTjrP7NLmOlQnBd_UIlM2tevrddjlgqWx28G0g5Rf59HGyBFvBhq4ExLOdpC2OJPnpkLdb03GTDXvikIOhn2_VDOvAXqweQmrxwJG0CrJnf8MoLxsOYJyDyq8FyjjG8MGjrKgmvHq3pHamjmC3iKDHSP9DM-fSxyg8iXd0I-tk9i62Sb-8RV4fh3Q95rPlNmqRy36DZgYdASFB7kgiu3UhXs5uCtsF8y6B0WM9p4LOnlvLRLUv","summary":[{"type":"summary_text","text":"**Planning dispatcher update and tool continuation**"}]},{"id":"fc_097e09b8a38fb850016a5a26fe08ec8193a0cb70c2b664b4d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Updating complete brain findings\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Addendum from Redispatch comparison: dispatcher `net_streams()` at 10Hz serializes the entire in-memory stream table before brain skips `initiated_locally` rows (`dispatch.rs:458-465`). Thus accumulated locally initiated Registry rows plausibly explain broker pipe-write CPU/I/O, while brain filesystem 63.91 MiB/s remains exact 2×exe heartbeat hash. Registry `sessions.log` scans are only 30s/forced advertise and capped 64 rows. Full structured result yielded.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_STNGIoq467DZTjSJGqwJValJ","name":"irc"}]},"duration":10899.593899995089,"ttft":4084.117699995637,"contextSnapshot":{"promptTokens":138564,"nonMessageTokens":15513}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_STNGIoq467DZTjSJGqwJValJ|fc_097e09b8a38fb850016a5a26fe08ec8193a0cb70c2b664b4d9","toolName":"irc","startedAt":"2026-07-17T12:58:42.920Z","intent":"Updating complete brain findings"},"id":"2bd3d93d","parentId":"3fa30a53","timestamp":"2026-07-17T12:58:42.920Z"}
{"type":"message","id":"3499504d","parentId":"2bd3d93d","timestamp":"2026-07-17T12:58:42.920Z","message":{"role":"toolResult","toolCallId":"call_STNGIoq467DZTjSJGqwJValJ|fc_097e09b8a38fb850016a5a26fe08ec8193a0cb70c2b664b4d9","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"BrainCpuRca","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784293122920}}
{"type":"message","id":"073c398f","parentId":"3499504d","timestamp":"2026-07-17T12:59:19.832Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning minimal terminal schema extraction**"},{"type":"toolCall","id":"call_5KaR8lo7ati1ehaJh52g9Xeh|fc_097e09b8a38fb850016a5a2727c2c08193b88d9aee3e4e02c3","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Closed. Primary brain cause is exact and source-proven: `crates/spt-daemon/src/brainproc.rs:63` defines a 500 ms heartbeat; `run_brain` calls `write_ready` every heartbeat at lines 259-297; `write_ready` calls `current_exe_hash` at 365-373; `current_exe_hash` reads the entire running executable and SHA-256 hashes it at 381-384. Predicted process read rate is `2 × spt.exe size/s`; 63.91 MiB/s predicts a 31.955 MiB executable. Procmon should show two full sequential reads of the executable per second; excluding that path should remove essentially all sustained brain read bytes. The fresh buffer and full SHA twice per second explain CPU.\n\nOther loop findings: one pulse thread per hosted endpoint runs at the configured 5 s default (`livehost.rs:637-639`, `config.rs:19-25`, `lifecycle.rs:701-738`), explaining the active-thread census. Full transcript or `history.log` reads occur only after a one-shot echo gate (`lifecycle.rs:647-666,843-860`; `spt-live/src/history.rs:103-129`), so they are burst candidates, not an unconditional scan. Project indexing polls only the invalidation directory every 500 ms and performs actual seed-Git/perch/session-ledger work on boot, invalidation, or the 5-minute backstop (`projwriter.rs:44-53,622-683`; `projinval.rs:90-119`); session ledgers are capped at 64 rows (`sessions.rs:28-32`). Registry scheduling checks at 200 ms but full advertisement defaults to 30 s (`pump/mod.rs:96-102`; `config.rs:35-46`); it reads bounded perch/session state and writes snapshots, not repositories or transcripts.\n\nSeparate broker amplifier: dispatcher polls `net_streams` at 10 Hz and receives the entire stream table before skipping locally initiated rows (`dispatch.rs:47-49,458-465`). Accumulated locally initiated Registry rows can therefore explain broker named-pipe writes/CPU, not brain file-read bytes. Subscriber writers are unnamed bounded per-seat threads (`nethost.rs:119-218`) and poison after the exact 15 s broker write deadline (`broker.rs:167-188`). Correlate `CONN_LIFECYCLE` attach/poison/detach by stable conn ID and `DISPATCH_EV` by stream/attempt: same-stream breaker delays 2→4→8→16→30 s prove poison recovery; unique streams near 30 s are new registry feeds; Served streams reappearing prove retirement failure. A bounded 3 subscriber-writer census is consistent with 3 installed seats; monotonically rising unnamed threads without matching detach/retirement is the leak discriminator.","files":[{"path":"crates/spt-daemon/src/brainproc.rs","description":"500 ms steady heartbeat repeatedly reads and hashes the entire executable through readiness publication."},{"path":"crates/spt-daemon/src/livehost.rs","description":"One lifecycle pulse thread per hosted live endpoint and 5-second reconciliation."},{"path":"crates/spt-daemon/src/lifecycle.rs","description":"Pulse loop, small recurring state scans, and one-shot-gated history/echo path."},{"path":"crates/spt-live/src/history.rs","description":"Potential full transcript/history reads, only on explicit history fetch."},{"path":"crates/spt-daemon/src/projwriter.rs","description":"500 ms cheap invalidation poll and gated five-minute/event reconcile."},{"path":"crates/spt-store/src/projinval.rs","description":"Idle project-index operation is one short-circuiting directory enumeration."},{"path":"crates/spt-store/src/sessions.rs","description":"Sessions ledger is bounded to 64 rows."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"10 Hz full stream-table IPC query, bounded dispatch claims, Registry feed lifecycle, retirement, and breaker."},{"path":"crates/spt-daemon/src/pump/mod.rs","description":"200 ms pump scheduler and due-only work."},{"path":"crates/spt-daemon/src/pump/registry.rs","description":"30-second/marker-triggered Registry advertisement rounds."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"Bounded perch/session scans and registry snapshot writes."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Unnamed per-subscriber writer threads and poison/detach semantics."},{"path":"crates/spt-daemon/src/broker.rs","description":"15-second brain-connection write deadline."},{"path":"crates/spt-daemon/src/conn.rs","description":"Stable connection lifecycle and poison attribution telemetry."}],"architecture":"The brain main thread wakes every 500 ms for broker status/session probes and readiness publication; the latter mistakenly re-reads and hashes the full executable. Independent threads host one 5-second pulse driver per live endpoint, one 500 ms project-invalidation watcher, one 100 ms dispatcher, and one 200 ms peer-pump scheduler. Dispatcher and pump share RegistryHost. Registry work is cadence/event driven and bounded; subscriber delivery uses dedicated bounded writer threads with a 15-second connection deadline. Separately, the dispatcher’s 10 Hz full stream-table transfer can amplify broker IPC writes when locally initiated rows accumulate, even though those rows are skipped after receipt."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4134,"output":1180,"cacheRead":134656,"cacheWrite":0,"totalTokens":139970,"reasoningTokens":13,"cost":{"input":0.02067,"output":0.0354,"cacheRead":0.067328,"cacheWrite":0,"total":0.12339800000000001}},"stopReason":"toolUse","timestamp":1784293122924,"responseId":"resp_097e09b8a38fb850016a5a27035c808193a777de8966b2a547","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_097e09b8a38fb850016a5a27057b948193aa6de0021159d4d0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWicngZ5eJW_NzkRlY2yQZhujDjKeHeYAx8PEndX5DBu1UUizRz1BDBtYobPx4aIZpNshRIx1DrJ7aUPQtwC5hL4DdJD6HyIyVOi5-akKzuFJm2hVvRTwLJ6hkJ_s2H6r7VaXxUhcNM9nOFIT3qNKrezYgCfMie0o7TypUfeFhIALXhfm4KNJnRURQbGFVp0gZY5JqPBN-gzPk-ll7nnK_iJbIPT1Ye6DuMBKofUnCxHMwO4Sqmt5_w-gUG7B5mSzZ02RAsjLfH8bA9rrQnNlTZS9gCkmt3hl4ORMHUfRcigyBGHQFwk7te9Azqu5Aoudu4beEZlcK7HfeCKKBmPrMc3YRPBV67zlY3aLtgVYekPlzcfeqIXO3y5F5WxUMJlRQKqRiyR2IU5BAp7KYECeZWIEep3KdD1VzMwHTqw1Uh_i6YUMl9X_Nl_3vLTQpsNsJlp3HigDoasICNrKaRCiXA_WYkyq2XtUmGzhFwCUbAS3bDP07xwVyzWn9IUDZKiBQX35Ts9Rp-W9GwO_Rh_RkqbFyX_y_6ilkbFVOdXXOca2wWlYXMs5I-RfLp5Nmor5xG4mGuwcsA1vnAs8nM_AREHLT7IsUL9jme0kdlJKg8euG_Q0n4JKpYzF8ufP63QyEd3JL_QHRFa0NQMXXvC2LCU-JJaWCIOPd8wuFEgV64Ovb7twPBA6zfKVd31TGoklt_hrFROpL5axLAJIxULiKsUqB2Wkgu6cQue_WscNT64gc7ShiEPe-9WsimiBqVfKHWwx1tHZ5UxHHwPRfom4my7PNW9EdQV-97LW-ftmKgveKUMPReE4fiRN_Nd3L26KgYktLpHyC50irZrvbydvzWxzCrPHTBBu5TOSsl4A1ik78ObEOalirINPSAVKnX6NmNhK2kr3Rpu8Rad-KgrQtYMjYyumE9b9I3WBhBadrkvj4m1QOabjkR0q_TbzFLnjZZbCQ-ULQNzAllWgNQGqcx5ySR_UalRnhZ6CAfXDzP-B7cA=","summary":[{"type":"summary_text","text":"**Planning minimal terminal schema extraction**"}]},{"id":"fc_097e09b8a38fb850016a5a2727c2c08193b88d9aee3e4e02c3","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Closed. Primary brain cause is exact and source-proven: `crates/spt-daemon/src/brainproc.rs:63` defines a 500 ms heartbeat; `run_brain` calls `write_ready` every heartbeat at lines 259-297; `write_ready` calls `current_exe_hash` at 365-373; `current_exe_hash` reads the entire running executable and SHA-256 hashes it at 381-384. Predicted process read rate is `2 × spt.exe size/s`; 63.91 MiB/s predicts a 31.955 MiB executable. Procmon should show two full sequential reads of the executable per second; excluding that path should remove essentially all sustained brain read bytes. The fresh buffer and full SHA twice per second explain CPU.\\n\\nOther loop findings: one pulse thread per hosted endpoint runs at the configured 5 s default (`livehost.rs:637-639`, `config.rs:19-25`, `lifecycle.rs:701-738`), explaining the active-thread census. Full transcript or `history.log` reads occur only after a one-shot echo gate (`lifecycle.rs:647-666,843-860`; `spt-live/src/history.rs:103-129`), so they are burst candidates, not an unconditional scan. Project indexing polls only the invalidation directory every 500 ms and performs actual seed-Git/perch/session-ledger work on boot, invalidation, or the 5-minute backstop (`projwriter.rs:44-53,622-683`; `projinval.rs:90-119`); session ledgers are capped at 64 rows (`sessions.rs:28-32`). Registry scheduling checks at 200 ms but full advertisement defaults to 30 s (`pump/mod.rs:96-102`; `config.rs:35-46`); it reads bounded perch/session state and writes snapshots, not repositories or transcripts.\\n\\nSeparate broker amplifier: dispatcher polls `net_streams` at 10 Hz and receives the entire stream table before skipping locally initiated rows (`dispatch.rs:47-49,458-465`). Accumulated locally initiated Registry rows can therefore explain broker named-pipe writes/CPU, not brain file-read bytes. Subscriber writers are unnamed bounded per-seat threads (`nethost.rs:119-218`) and poison after the exact 15 s broker write deadline (`broker.rs:167-188`). Correlate `CONN_LIFECYCLE` attach/poison/detach by stable conn ID and `DISPATCH_EV` by stream/attempt: same-stream breaker delays 2→4→8→16→30 s prove poison recovery; unique streams near 30 s are new registry feeds; Served streams reappearing prove retirement failure. A bounded 3 subscriber-writer census is consistent with 3 installed seats; monotonically rising unnamed threads without matching detach/retirement is the leak discriminator.\",\"files\":[{\"path\":\"crates/spt-daemon/src/brainproc.rs\",\"description\":\"500 ms steady heartbeat repeatedly reads and hashes the entire executable through readiness publication.\"},{\"path\":\"crates/spt-daemon/src/livehost.rs\",\"description\":\"One lifecycle pulse thread per hosted live endpoint and 5-second reconciliation.\"},{\"path\":\"crates/spt-daemon/src/lifecycle.rs\",\"description\":\"Pulse loop, small recurring state scans, and one-shot-gated history/echo path.\"},{\"path\":\"crates/spt-live/src/history.rs\",\"description\":\"Potential full transcript/history reads, only on explicit history fetch.\"},{\"path\":\"crates/spt-daemon/src/projwriter.rs\",\"description\":\"500 ms cheap invalidation poll and gated five-minute/event reconcile.\"},{\"path\":\"crates/spt-store/src/projinval.rs\",\"description\":\"Idle project-index operation is one short-circuiting directory enumeration.\"},{\"path\":\"crates/spt-store/src/sessions.rs\",\"description\":\"Sessions ledger is bounded to 64 rows.\"},{\"path\":\"crates/spt-daemon/src/dispatch.rs\",\"description\":\"10 Hz full stream-table IPC query, bounded dispatch claims, Registry feed lifecycle, retirement, and breaker.\"},{\"path\":\"crates/spt-daemon/src/pump/mod.rs\",\"description\":\"200 ms pump scheduler and due-only work.\"},{\"path\":\"crates/spt-daemon/src/pump/registry.rs\",\"description\":\"30-second/marker-triggered Registry advertisement rounds.\"},{\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"description\":\"Bounded perch/session scans and registry snapshot writes.\"},{\"path\":\"crates/spt-daemon/src/nethost.rs\",\"description\":\"Unnamed per-subscriber writer threads and poison/detach semantics.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"15-second brain-connection write deadline.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"Stable connection lifecycle and poison attribution telemetry.\"}],\"architecture\":\"The brain main thread wakes every 500 ms for broker status/session probes and readiness publication; the latter mistakenly re-reads and hashes the full executable. Independent threads host one 5-second pulse driver per live endpoint, one 500 ms project-invalidation watcher, one 100 ms dispatcher, and one 200 ms peer-pump scheduler. Dispatcher and pump share RegistryHost. Registry work is cadence/event driven and bounded; subscriber delivery uses dedicated bounded writer threads with a 15-second connection deadline. Separately, the dispatcher’s 10 Hz full stream-table transfer can amplify broker IPC writes when locally initiated rows accumulate, even though those rows are skipped after receipt.\"}}}","call_id":"call_5KaR8lo7ati1ehaJh52g9Xeh","name":"yield"}]},"duration":36906.539900004864,"ttft":2573.0249000042677,"contextSnapshot":{"promptTokens":138790,"nonMessageTokens":15513}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5KaR8lo7ati1ehaJh52g9Xeh|fc_097e09b8a38fb850016a5a2727c2c08193b88d9aee3e4e02c3","toolName":"yield","startedAt":"2026-07-17T12:59:19.832Z"},"id":"2071c20e","parentId":"073c398f","timestamp":"2026-07-17T12:59:19.832Z"}
{"type":"message","id":"430d8ae8","parentId":"2071c20e","timestamp":"2026-07-17T12:59:19.833Z","message":{"role":"toolResult","toolCallId":"call_5KaR8lo7ati1ehaJh52g9Xeh|fc_097e09b8a38fb850016a5a2727c2c08193b88d9aee3e4e02c3","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Closed. Primary brain cause is exact and source-proven: `crates/spt-daemon/src/brainproc.rs:63` defines a 500 ms heartbeat; `run_brain` calls `write_ready` every heartbeat at lines 259-297; `write_ready` calls `current_exe_hash` at 365-373; `current_exe_hash` reads the entire running executable and SHA-256 hashes it at 381-384. Predicted process read rate is `2 × spt.exe size/s`; 63.91 MiB/s predicts a 31.955 MiB executable. Procmon should show two full sequential reads of the executable per second; excluding that path should remove essentially all sustained brain read bytes. The fresh buffer and full SHA twice per second explain CPU.\n\nOther loop findings: one pulse thread per hosted endpoint runs at the configured 5 s default (`livehost.rs:637-639`, `config.rs:19-25`, `lifecycle.rs:701-738`), explaining the active-thread census. Full transcript or `history.log` reads occur only after a one-shot echo gate (`lifecycle.rs:647-666,843-860`; `spt-live/src/history.rs:103-129`), so they are burst candidates, not an unconditional scan. Project indexing polls only the invalidation directory every 500 ms and performs actual seed-Git/perch/session-ledger work on boot, invalidation, or the 5-minute backstop (`projwriter.rs:44-53,622-683`; `projinval.rs:90-119`); session ledgers are capped at 64 rows (`sessions.rs:28-32`). Registry scheduling checks at 200 ms but full advertisement defaults to 30 s (`pump/mod.rs:96-102`; `config.rs:35-46`); it reads bounded perch/session state and writes snapshots, not repositories or transcripts.\n\nSeparate broker amplifier: dispatcher polls `net_streams` at 10 Hz and receives the entire stream table before skipping locally initiated rows (`dispatch.rs:47-49,458-465`). Accumulated locally initiated Registry rows can therefore explain broker named-pipe writes/CPU, not brain file-read bytes. Subscriber writers are unnamed bounded per-seat threads (`nethost.rs:119-218`) and poison after the exact 15 s broker write deadline (`broker.rs:167-188`). Correlate `CONN_LIFECYCLE` attach/poison/detach by stable conn ID and `DISPATCH_EV` by stream/attempt: same-stream breaker delays 2→4→8→16→30 s prove poison recovery; unique streams near 30 s are new registry feeds; Served streams reappearing prove retirement failure. A bounded 3 subscriber-writer census is consistent with 3 installed seats; monotonically rising unnamed threads without matching detach/retirement is the leak discriminator.","files":[{"path":"crates/spt-daemon/src/brainproc.rs","description":"500 ms steady heartbeat repeatedly reads and hashes the entire executable through readiness publication."},{"path":"crates/spt-daemon/src/livehost.rs","description":"One lifecycle pulse thread per hosted live endpoint and 5-second reconciliation."},{"path":"crates/spt-daemon/src/lifecycle.rs","description":"Pulse loop, small recurring state scans, and one-shot-gated history/echo path."},{"path":"crates/spt-live/src/history.rs","description":"Potential full transcript/history reads, only on explicit history fetch."},{"path":"crates/spt-daemon/src/projwriter.rs","description":"500 ms cheap invalidation poll and gated five-minute/event reconcile."},{"path":"crates/spt-store/src/projinval.rs","description":"Idle project-index operation is one short-circuiting directory enumeration."},{"path":"crates/spt-store/src/sessions.rs","description":"Sessions ledger is bounded to 64 rows."},{"path":"crates/spt-daemon/src/dispatch.rs","description":"10 Hz full stream-table IPC query, bounded dispatch claims, Registry feed lifecycle, retirement, and breaker."},{"path":"crates/spt-daemon/src/pump/mod.rs","description":"200 ms pump scheduler and due-only work."},{"path":"crates/spt-daemon/src/pump/registry.rs","description":"30-second/marker-triggered Registry advertisement rounds."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"Bounded perch/session scans and registry snapshot writes."},{"path":"crates/spt-daemon/src/nethost.rs","description":"Unnamed per-subscriber writer threads and poison/detach semantics."},{"path":"crates/spt-daemon/src/broker.rs","description":"15-second brain-connection write deadline."},{"path":"crates/spt-daemon/src/conn.rs","description":"Stable connection lifecycle and poison attribution telemetry."}],"architecture":"The brain main thread wakes every 500 ms for broker status/session probes and readiness publication; the latter mistakenly re-reads and hashes the full executable. Independent threads host one 5-second pulse driver per live endpoint, one 500 ms project-invalidation watcher, one 100 ms dispatcher, and one 200 ms peer-pump scheduler. Dispatcher and pump share RegistryHost. Registry work is cadence/event driven and bounded; subscriber delivery uses dedicated bounded writer threads with a 15-second connection deadline. Separately, the dispatcher’s 10 Hz full stream-table transfer can amplify broker IPC writes when locally initiated rows accumulate, even though those rows are skipped after receipt."},"status":"success","type":"result"},"isError":false,"timestamp":1784293159832}}
{"type":"message","id":"df527cf1","parentId":"430d8ae8","timestamp":"2026-07-17T12:59:23.525Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_097e09b8a38fb850016a5a272aa3ec8193823cdca785a84738\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5328,"output":4,"cacheRead":134656,"cacheWrite":0,"totalTokens":139988,"cost":{"input":0.02664,"output":0.00012,"cacheRead":0.067328,"cacheWrite":0,"total":0.094088}},"stopReason":"stop","timestamp":1784293159836,"responseId":"resp_097e09b8a38fb850016a5a272851dc8193b2332e5ee5f8ecb1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_097e09b8a38fb850016a5a272aa3ec8193823cdca785a84738","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":3688.542700007558,"ttft":2819.8671000003815,"contextSnapshot":{"promptTokens":139984,"nonMessageTokens":15513}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-17T13:06:23.537Z"},"id":"5f70b908","parentId":"df527cf1","timestamp":"2026-07-17T13:06:23.537Z"}
