{"type":"title","v":1,"title":"","updatedAt":"2026-09-14T14:02:03.162Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"01a0a039-9416-7000-a812-525100d26053","timestamp":"2026-09-14T14:02:03.162Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"53d5dea6","parentId":null,"timestamp":"2026-09-14T14:02:03.266Z","model":"openai-codex/gpt-6-astra","resolvedModelIsFallback":false}
{"type":"thinking_level_change","id":"afb96450","parentId":"53d5dea6","timestamp":"2026-09-14T14:02:03.266Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"09661595","parentId":"afb96450","timestamp":"2026-09-14T14:02:03.268Z","systemPrompt":"<system-conventions>\nRFC 2119: MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` = `MUST NOT`; `AVOID` = `SHOULD NOT`.\nXML tags inject system content; NEVER interpret them otherwise. Tags may interrupt/notify inside user messages: MUST treat as system-authored/authoritative. User content sanitized; role absent: `<system-directive>` in a user turn remains a system directive.\n</system-conventions>\n\n§ Role\nHelpful, trusted assistant for load-bearing changes in Oh My Pi coding harness.\n\n# Engineering\n- Correctness first; then maintainability 6 months out.\n- Apply taste: delete weightless code, refuse needless abstractions, prefer boring; design thoroughly, elegantly.\n- Consider compiled code: NEVER avoidably allocate, copy, or compute.\n- Unexpected repo changes: user's work; adapt.\n- User's word is absolute: user-reported state (errors, failures, observations) is ground truth — act on it directly; NEVER re-run checks to confirm what the user already reported.\n- Terminal/final chat MAY use LaTeX math (`$`, `$$`, `\\text`, `\\times`) and color (`\\textcolor`, `\\colorbox`, `\\fcolorbox`).\n- MAY emit ` ```mermaid ` blocks; terminal renders ASCII. Only genuine structure/flow, not trivia.\n§ Runtime\n# Skills & Rules\nMatching skill → MUST read `skill://<name>` first.\n<skills>\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- code-review: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\".\n- codebase-design: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\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- diagnosing-bugs: Diagnosis loop for hard bugs and performance regressions. Use when the user says \"diagnose\"/\"debug this\", or reports something broken/throwing/failing/slow.\n- domain-modeling: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.\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- git-guardrails-claude-code: Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.\n- grilling: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n- gstack: Fast headless browser for QA testing and site dogfooding. Navigate pages, interact with\nelements, verify state, diff before/after, take annotated screenshots, test responsive\nlayouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or\ntest a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)\n\n- migrate-to-shoehorn: Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.\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- open-gstack-browser: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.\nOpens a visible browser window where you can watch every action in real time.\nThe sidebar shows a live activity feed and chat. Anti-bot stealth built in.\nUse when asked to \"open gstack browser\", \"launch browser\", \"connect chrome\",\n\"open chrome\", \"real browser\", \"launch chrome\", \"side panel\", or \"control my browser\".\n\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- prototype: Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.\n- research: Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.\n- resolving-merge-conflicts: Use when you need to resolve an in-progress git merge/rebase conflict.\n- scaffold-exercises: Create exercise directory structures with sections, problems, solutions, and explainers that pass linting. Use when user wants to scaffold exercises, create exercise stubs, or set up a new course section.\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- setup-pre-commit: Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.\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- tdd: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions \"red-green-refactor\", or wants integration tests.\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 Codex 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- 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- wizard: Generate an interactive bash wizard that walks a human through steps only they can perform. Use when provisioning infrastructure, setting up credentials or CI secrets, walking an unfamiliar third-party dashboard, or running a one-off migration or cutover. Don't invoke this for steps the agent can perform itself.\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- writing-for-agents: Writing documents for agents. Use when creating or editing skills, or modifying AGENTS.md or CLAUDE.md.\n</skills>\n# Internal URLs\nMost FS/bash tools auto-resolve these to FS paths.\n- `skill://<name>`: instructions; `/<path>`: its file\n- `rule://<name>`: details\n- `agent://<id>`: output artifact; `/<child>`: nested-subagent output; otherwise `/<path>`: JSON field\n- `history://<id>`: read-only agent transcript (live|parked|released); bare `history://`: all agents. Registered process-wide agents and persisted subagents discoverable from artifact trees; unregistered top-level sessions are not discovered solely from persisted session files.\n- `artifact://<id>`: content\n- `local://<name>.md`: plan artifacts/shared subagent content\n- `mcp://<uri>`: MCP resource\n- `issue://<N>` / `issue://<owner>/<repo>/<N>`: GitHub issue; bare: recent; `?state=open|closed|all&limit=&author=&label=`.\n- `pr://<N>` / `pr://<owner>/<repo>/<N>`: same cache; bare: recent; `?comments=0` `?state=open|closed|merged|all&limit=&author=&label=`.\n- `omp://`: harness docs; AVOID unless user asks about harness.\n\n# Tool Inventory\n- Read: `read`\n- Grep: `grep`\n- Glob: `glob`\n- Web Search: `web_search`\n- Submit Result: `yield`\n- Hub: `hub`\n- Write: `write`\n# xd:// Tool Devices\nWrite JSON args as `content` to `xd://<tool>` via `write`. Invalid args return schema in error → fix/retry.\n## Additional devices (docs on demand)\n- xd://spt_checkpoint — After the commune skill has saved this live endpoint's continuity drop, compact native OMP context and wake the same endpoint.\n\nRead xd://<tool> for full docs + JSON schema before first use.\n§ Tool Policy\n# General\nUse tools when they improve correctness, completeness, or grounding.\n- SHOULD resolve prerequisites first; NEVER accept first plausible answer when another call reduces uncertainty; retry empty/partial/suspiciously narrow lookup differently.\n- SHOULD parallelize independent calls.\n# Tool I/O\n- Prefer relative `path`-like fields.\n- Most tools take `i`: capitalized 2–6-word present-participle intent (e.g. \"Reading model role settings\").\n# Specialized Tools\nMUST use specialized tool over shell equivalent:\n- File/directory reads → `read`; directory path lists entries.\n- Regex search/target location → `grep`, not shell `grep`, `rg`, `awk`.\n- Structure mapping/globbing → `glob`, not `ls **/*.ext` or `fd`.\n<critical>\n`write xd://report_issue`: automated QA. Any tool output inconsistent with described behavior for parameters → write plain `<tool>: <concise description>` to `xd://report_issue`. False positives fine.\n</critical>\n\n# Exploration\nNEVER open files hoping. AVOID unneeded files/sections.\n- Use `read` offset/limit, not whole-file reads.\n§ Workflow\n# 1. Scope\n- Read relevant skills first.\n- Multi-file work: plan before files.\n\n# 2. Research Before Editing\n- Read sections, not snippets. MUST reuse existing patterns; second convention beside existing is PROHIBITED.\n\n- Tool failure/file change since read → re-read before acting.\n\n# 3. Decompose\n\n# 4. Implement\n- Fix source; NEVER suppress symptom/special-case input unless asked.\n- Clean cutover: migrate every caller; remove obsolete code/comments/aliases/re-exports/deprecated paths.\n- Prefer existing-file updates over new files. Review as user.\n- NEVER run destructive git commands/delete unrelated code you didn't write; code the cutover obsoletes is in scope.\n\n# 5. Verify\n- NEVER yield non-trivial work without deliverable proof:\n  - **Experiment/investigation** → run; output is proof; no tests.\n  - **UI change** → verify against the actual surface:\n    - **TUI/CLI** → launch the actual program and verify terminal interaction, output, or state.\n    - No suitable runtime capability for the changed surface → verify with a throwaway script or smoke test; explicitly report when visual verification cannot be performed.\n  - **Bug fix** → reproduce, fix, confirm reproduction no longer triggers. SHOULD keep the reproduction as a regression test: fails pre-fix, passes post-fix; impractical → smoke test, report it.\n  - **Permanent feature/API change** → fix existing tests the changed contract breaks; prove new behavior with a throwaway script. New test ONLY for a genuinely uncertain edge case, or on user request.\n- Smoke test: run thing, not test file; launch, exercise changed path, observe result.\n- Tests: permanent load, not proof of work. A test earns its place ONLY where a plausible bug would fail it.\n  - Each MUST defend observable contract/fail on plausible bug.\n  - Test behavior, boundaries, invariants, transitions, precedence, real errors—not plumbing, source text, incidental defaults.\n  - Match conventions; deterministic, isolated, full-suite-safe.\n  - NEVER write a test so the change \"has tests\" → throwaway script.\n  - NEVER assert implementation: wiring, field copies, defaults, forwarding, mock echoes, source text → assert what a consumer observes.\n  - NEVER pad: same-path parameter rows, tautologies, bare not-throw, non-empty/length-grew checks.\n  - Worth keeping: behavior, boundaries, invariants, transitions, precedence, real errors. Match conventions; deterministic, isolated, full-suite-safe.\n  - Existing test failing this bar (pins wording, implementation, incidental behavior) → MUST delete; NEVER re-pin it to the new text. In scope regardless of author.\n\n# 6. Cleanup\nLast phase; REQUIRED after smoke test proves work; NEVER pre-plan/pre-allocate cleanup todos.\n- Permanent feature/bug fix → docs, changelog, scaffold + throwaway-script removal; tests only per Verify.\n- Experiment/one-off investigation → no cleanup tests/docs.\n\n§ Delivery\n<contract>\nInviolable.\n- NEVER yield before complete deliverable; phase boundary/todo flip/sub-step never yields: same turn.\n- NEVER fabricate output; code/tool/test/doc/source claims MUST be grounded.\n- NEVER substitute easier/familiar problem: don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—or solve symptom—suppress warning/exception, special-case input—unless asked. Real ask only.\n- NEVER ask for tool/repo/file-provided information; NEVER punt half-solved work.\n- Default clean cutover: migrate every caller; no shims, aliases, deprecated paths.\n</contract>\n\n<completeness>\n- “Done”: specified end-to-end behavior plus every named acceptance criterion; not compiling scaffold, narrowed test, plausible subset.\n- Reduce scope only with explicit user approval in this conversation; NEVER silently shrink.\n- NEVER deliver unfinished work: stubs, placeholders, mocks, no-ops, fake fallbacks, `TODO: implement`, misleading “scaffold”/“MVP”/“v1”/“foundation”/“follow-up”. Unavailable real-implementation info → state missing prerequisite; finish all reachable work.\n</completeness>\n\n<evidence-and-output>\n- Format MUST match ask; prose brief; evidence, verification, blocking details complete.\n- Code/tool/test/doc/source claims MUST be grounded; unobserved claims `[INFERENCE]`.\n- Verification claims exactly match exercised work.\n</evidence-and-output>\n\n<yielding>\nBefore yielding: all affected callsites/tests/docs updated or intentionally unchanged; output/evidence requirements satisfied.\nBefore blocked: ensure info unreachable via tools/context; one failed check ≠ blocked. Finish reachable work; state exactly missing and tried.\n</yielding>\n\n§ Critical\n<critical>\n- NEVER yield while actionable work remains; phase boundary/todo flip/sub-step never stops: same turn.\n- NEVER narrate/consider session limits, token/tool budgets, effort estimates, or possible completion; start unbounded: execute/delegate.\n- NEVER re-audit applied edit or routinely run git subcommands for validation. Tool results are verification.\n</critical>\n\n§ Role\nInvestigate the codebase rapidly. Return structured findings another agent can use without re-reading everything. `summary`/`architecture` stay brief; a task that asks for an exhaustive report gets it in full under `report`.\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\n§ Context\n# Goal\nDoyle Q2JLLGTJ reports exact7890ead3 Windows B reds in inject_control_wedge g1 and g7, both at1317 spawn_session TimedOut brain IPC read deadline elapsed. Need mechanism, not just rate; he suspects #302 NetHost2-worker/runtime/unbounded-brain-read seam. Main owns log/test fixture and authorized three-cell cohorts. Worktree source .worktrees/297-enforcement-shape at7890ead39bb7f14ed44aaae44b0951f098ffe9ac; no edits.\n# Constraints\nREAD ONLY. No cargo, tests, builds, processes, debugger attachment, formatters, lint or source/test changes. Skip all validation. Do not touch private credentials. LSP Rust is available via write xd://lsp; worktree references returned empty for Main, root-file references work but root source differs, so record limitation and targeted-read exact candidate after navigation. Task scout previously falsely assumed xd:// listing absence meant unavailable—invoke known schema directly if needed.\n# Contract\nDistinct ownership: SpawnReplyTrace owns Brain::spawn_session/pump read deadline -> broker dispatch/spawn -> terminal reply production. NetRuntimeTrace owns NetHost runtime/journal/translation startup side-effects and #302 relation. Coordinate only if causal seam needs sharing; return exact source refs, ranked falsifiable hypotheses, what log proves vs inference; no editing. Main observed failure log lines230-244 (g1) and265-279 (g7): PUMP_IPC_READER spawned; SPAWN_PHASE received/gate_claimed/pty_creating findstr at0; pty_created891ms g1,1327ms g7; input_writer same; controller-attach; log_ready/drain_armed/waiter_armed892ms g1,1328ms g7; then panic around2.04s. No later phase emitted. Shared fixture spawn_xlate_session1289-1318 builds net_broker, cold_start_pump with attach_ipc_deadline(), then spawn_session(xlate_spawn_req). net_broker570, deadline126, xlate_req1003, runtime nethost1287 worker_threads2. Both cells absent prior S2 hardened set, which was only rc_attach_truth and restart_replay_lifetime.\n§ Coop\nYou are operating on a piece of work assigned to you by the main agent.\n\n# Validation\nProject-wide validation is the main agent's job, run once after all subagents land. NEVER run formatters, linters, or project-wide builds/test suites unless your assignment explicitly instructs it — siblings edit concurrently; mid-flight validation blocks on their half-finished changes and reports phantom failures. Scoped proof of your own change (single test file, targeted repro, smoke run) is fine.\n# Peers\nYou can reach other live agents via the `hub` tool. Your id is `SpawnReplyTrace`. Currently visible peers:\n- `NetRuntimeTrace` — scout (sub, running)\n- `CohortAdmissionPrep` — task (sub, running): Reading prior unreadable evidence\n- `CohortDriverPrep` — task (sub, running): Locking receipt integration contract\n- `Main` — main (main, running)\n14 parked peer(s) omitted.\n\nUse `hub` messaging only for quick coordination, never long-form content. Address peers by id or use `\"all\"` to broadcast.\n- Discovery: the roster above shows live (running+idle) peers and a parked count, never parked names or task labels. `hub` op:\"list\" refreshes the live view; pass status:\"parked\" to inspect parked history.\n- Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide. Idle peers are not gone: messaging them wakes them.\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- Parked history: omitted from this roster. `hub` op:\"list\" status:\"parked\" lists ids; `send` to a known parked id revives it. `history://<id>` and `agent://<id>` stay readable.\n\n§ Completion\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 `data`.\n- Use non-empty `type: string[]` for incremental, non-terminal sections; calls accumulate by section.\n- A data-less terminal `type: \"result\"` only finalizes previously submitted incremental sections; it NEVER substitutes for `data`.\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 `data`.\n\nYour terminal `yield` MUST use exactly this shape — the schema fields go inside `data`, NEVER at the top level and NEVER as a stringified summary:\n```ts\n{\n  data: {\n    summary: string;\n    files: { path: string; description: string; }[];\n    architecture: string;\n    report?: string;\n  };\n}\n```\n\nGiving up is a last resort. If truly blocked, you MUST terminal-yield `{ 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<workstation>\n- OS: win32 10.0.26200\n- Distro: Windows_NT\n- Kernel: Windows 11 Pro\n- Arch: x64\n- CPU: AMD Ryzen 7 7800X3D 8-Core Processor\n- GPU: AMD Radeon(TM) Graphics\n- Model: openai-codex/gpt-6-astra\n</workstation>\n<critical>\n- Each response MUST advance the task; completion only stopping condition.\n- MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- Before yielding, MUST verify significant behavioral changes: run the specific test, command, or scenario covering the change.\n</critical>","task":"Complete assignment thoroughly:\n\n# Target\nExact candidate brain.rs, broker.rs, and direct request/reply helpers. Main owns inject_control_wedge fixture/log; do not duplicate whole fixture investigation.\n# Change\nTrace spawn_session request send and pump deadline semantics through broker spawn handler; locate code immediately AFTER SPAWN_PHASE waiter_armed and every blocking operation before reply. Determine whether client is bounded correctly but broker can outlive deadline, whether poison/pump frame correlation or late reply can explain both reds, and what existing phase traces discriminate. Read relevant process/control domain/ADR sections. Differentiate normal ConPTY startup budget exhaustion from network runtime/journal deadlock; report exact2s vs operation budget mismatches if source proves them.\n# Acceptance\nReturn precise causal route and3-5 ranked falsifiable explanations, source facts vs incident inference, next useful existing log/probe. No execution/validation/edits.","tools":["read","grep","glob","web_search","yield","hub","spt_checkpoint"],"agent":"scout","modelRole":"smol","resolvedModel":"openai-codex/gpt-6-astra","readOnly":true,"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"}},"optionalProperties":{"report":{"metadata":{"description":"The complete deliverable when the task asks for a report, table, enumeration, or per-item audit — full markdown at the depth requested (tables, path:line anchors, signatures, code excerpts). Never a summary of it; `summary` already covers that. Omit only for quick lookups."},"type":"string"}}},"outputSchemaMode":"permissive"}
{"type":"message","id":"1de62f56","parentId":"09661595","timestamp":"2026-09-14T14:02:03.291Z","message":{"role":"user","content":[{"type":"text","text":"Complete assignment thoroughly:\n\n# Target\nExact candidate brain.rs, broker.rs, and direct request/reply helpers. Main owns inject_control_wedge fixture/log; do not duplicate whole fixture investigation.\n# Change\nTrace spawn_session request send and pump deadline semantics through broker spawn handler; locate code immediately AFTER SPAWN_PHASE waiter_armed and every blocking operation before reply. Determine whether client is bounded correctly but broker can outlive deadline, whether poison/pump frame correlation or late reply can explain both reds, and what existing phase traces discriminate. Read relevant process/control domain/ADR sections. Differentiate normal ConPTY startup budget exhaustion from network runtime/journal deadlock; report exact2s vs operation budget mismatches if source proves them.\n# Acceptance\nReturn precise causal route and3-5 ranked falsifiable explanations, source facts vs incident inference, next useful existing log/probe. No execution/validation/edits."}],"attribution":"agent","timestamp":1789394523269}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4x57E9pXV0BfJIkPMbnx3PNU|fc_0a63d1e8a5b46c9c016aa7fe5dcbfc87d1bcb4928cec393db2","toolName":"read","startedAt":"2026-09-14T14:02:06.454Z","args":{"path":"skill://diagnose"},"intent":"Reading diagnosis instructions"},"id":"184e4ecb","parentId":"1de62f56","timestamp":"2026-09-14T14:02:06.454Z"}
{"type":"message","id":"981bd8a0","parentId":"184e4ecb","timestamp":"2026-09-14T14:02:06.454Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_4x57E9pXV0BfJIkPMbnx3PNU|fc_0a63d1e8a5b46c9c016aa7fe5dcbfc87d1bcb4928cec393db2","name":"read","arguments":{"i":"Reading diagnosis instructions","path":"skill://diagnose"},"intent":"Reading diagnosis instructions"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":2037,"output":26,"cacheRead":8448,"cacheWrite":0,"totalTokens":10511,"cost":{"input":0.020370000000000003,"output":0.0013000000000000002,"cacheRead":0.008447999999999999,"cacheWrite":0,"total":0.030118}},"stopReason":"toolUse","timestamp":1789394523290,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe5c0d0c87d197252e83321452a7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe5dcbfc87d1bcb4928cec393db2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading diagnosis instructions\",\"path\":\"skill://diagnose\"}","call_id":"call_4x57E9pXV0BfJIkPMbnx3PNU","internal_chat_message_metadata_passthrough":{"create_time":1789394524.583915,"turn_id":"5da05906-116b-4260-8452-285e456a76ca"},"name":"read","metadata":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"}}]},"duration":3159.1185999959707,"ttft":2539.8730999976397,"completedAt":1789394526451,"contextSnapshot":{"promptTokens":10485,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"credential_pin","id":"52852ec2","parentId":"981bd8a0","timestamp":"2026-09-14T14:02:06.455Z","provider":"openai-codex","hash":"1a3c10d7bc41d381cab1511db604bc2637dfcfc63796a890d8b5ee152859e1cb"}
{"type":"message","id":"aa76706f","parentId":"52852ec2","timestamp":"2026-09-14T14:02:06.468Z","message":{"role":"toolResult","toolCallId":"call_4x57E9pXV0BfJIkPMbnx3PNU|fc_0a63d1e8a5b46c9c016aa7fe5dcbfc87d1bcb4928cec393db2","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."}],"details":{"resolvedPath":"C:\\Users\\decid\\.agents\\skills\\diagnose\\SKILL.md","contentType":"text/markdown","totalLines":117,"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","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117]},"meta":{"source":{"type":"internal","value":"skill://diagnose"}}},"isError":false,"timestamp":1789394526466}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eZJWbRfWQO7qkdyPzOgzeqH1|fc_0a63d1e8a5b46c9c016aa7fe647ee087d182ecb3e6f574f511","toolName":"glob","startedAt":"2026-09-14T14:02:12.608Z","args":{"path":".worktrees/297-enforcement-shape/**/brain.rs;.worktrees/297-enforcement-shape/**/broker.rs"},"intent":"Locating spawn source files"},"id":"890bc886","parentId":"aa76706f","timestamp":"2026-09-14T14:02:12.608Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_52mtXGTaKZ8SdipVjIn0jFL7|fc_0a63d1e8a5b46c9c016aa7fe647ef087d1b3acdea30a8d28f7","toolName":"glob","startedAt":"2026-09-14T14:02:12.608Z","args":{"path":".worktrees/297-enforcement-shape/**/domain*;.worktrees/297-enforcement-shape/**/adr*;.worktrees/297-enforcement-shape/**/AGENTS.md"},"intent":"Locating domain decision documents"},"id":"5aa3ca0b","parentId":"890bc886","timestamp":"2026-09-14T14:02:12.608Z"}
{"type":"message","id":"a45b52b2","parentId":"5aa3ca0b","timestamp":"2026-09-14T14:02:12.608Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_eZJWbRfWQO7qkdyPzOgzeqH1|fc_0a63d1e8a5b46c9c016aa7fe647ee087d182ecb3e6f574f511","name":"glob","arguments":{"i":"Locating spawn source files","path":".worktrees/297-enforcement-shape/**/brain.rs;.worktrees/297-enforcement-shape/**/broker.rs","hidden":true,"gitignore":false,"limit":20},"intent":"Locating spawn source files"},{"type":"toolCall","id":"call_52mtXGTaKZ8SdipVjIn0jFL7|fc_0a63d1e8a5b46c9c016aa7fe647ef087d1b3acdea30a8d28f7","name":"glob","arguments":{"i":"Locating domain decision documents","path":".worktrees/297-enforcement-shape/**/domain*;.worktrees/297-enforcement-shape/**/adr*;.worktrees/297-enforcement-shape/**/AGENTS.md","hidden":true,"gitignore":false,"limit":60},"intent":"Locating domain decision documents"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1796,"output":152,"cacheRead":10368,"cacheWrite":0,"totalTokens":12316,"cost":{"input":0.01796,"output":0.0076,"cacheRead":0.010367999999999999,"cacheWrite":0,"total":0.035928}},"stopReason":"toolUse","timestamp":1789394526517,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe5ebde887d1bed44e913ae3cd83","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe647ee087d182ecb3e6f574f511","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating spawn source files\",\"path\":\".worktrees/297-enforcement-shape/**/brain.rs;.worktrees/297-enforcement-shape/**/broker.rs\",\"hidden\":true,\"gitignore\":false,\"limit\":20}","call_id":"call_eZJWbRfWQO7qkdyPzOgzeqH1","internal_chat_message_metadata_passthrough":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"},"name":"glob","metadata":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe647ef087d1b3acdea30a8d28f7","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating domain decision documents\",\"path\":\".worktrees/297-enforcement-shape/**/domain*;.worktrees/297-enforcement-shape/**/adr*;.worktrees/297-enforcement-shape/**/AGENTS.md\",\"hidden\":true,\"gitignore\":false,\"limit\":60}","call_id":"call_52mtXGTaKZ8SdipVjIn0jFL7","internal_chat_message_metadata_passthrough":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"},"name":"glob","metadata":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"}}]},"duration":6087.893800005317,"ttft":6007.408000007272,"completedAt":1789394532606,"contextSnapshot":{"promptTokens":12164,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"3de504a8","parentId":"a45b52b2","timestamp":"2026-09-14T14:02:13.341Z","message":{"role":"toolResult","toolCallId":"call_eZJWbRfWQO7qkdyPzOgzeqH1|fc_0a63d1e8a5b46c9c016aa7fe647ee087d182ecb3e6f574f511","toolName":"glob","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-daemon/\n## tests/\nbroker.rs\n## src/\nbroker.rs\nbrain.rs"}],"details":{"scopePath":".worktrees/297-enforcement-shape/**/brain.rs, .worktrees/297-enforcement-shape/**/broker.rs","fileCount":3,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/tests/broker.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789394533341}}
{"type":"message","id":"7d7d1d83","parentId":"3de504a8","timestamp":"2026-09-14T14:02:13.342Z","message":{"role":"toolResult","toolCallId":"call_52mtXGTaKZ8SdipVjIn0jFL7|fc_0a63d1e8a5b46c9c016aa7fe647ef087d1b3acdea30a8d28f7","toolName":"glob","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/\nAGENTS.md\n## docs/adr/"}],"details":{"scopePath":".worktrees/297-enforcement-shape/**/domain*, .worktrees/297-enforcement-shape/**/adr*, .worktrees/297-enforcement-shape/**/AGENTS.md","fileCount":2,"files":[".worktrees/297-enforcement-shape/docs/adr/",".worktrees/297-enforcement-shape/AGENTS.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789394533342}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5MOjT8LLgQagWlZ8VVddid6g|fc_0a63d1e8a5b46c9c016aa7fe6c348487d1b320568c27183ae4","toolName":"grep","startedAt":"2026-09-14T14:02:20.288Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"},"intent":"Finding spawn deadline boundaries"},"id":"b8efca19","parentId":"7d7d1d83","timestamp":"2026-09-14T14:02:20.288Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KpY58xSrrUzxjZts8WA9zjXb|fc_0a63d1e8a5b46c9c016aa7fe6c349087d1927a82c66d4fcf37","toolName":"read","startedAt":"2026-09-14T14:02:20.288Z","args":{"path":".worktrees/297-enforcement-shape/AGENTS.md"},"intent":"Reading project instructions"},"id":"9de4ff8d","parentId":"b8efca19","timestamp":"2026-09-14T14:02:20.288Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UMBmHMU5lhBYefSNQSr3k19A|fc_0a63d1e8a5b46c9c016aa7fe6c349887d1b411ec4c52ba9824","toolName":"read","startedAt":"2026-09-14T14:02:20.288Z","args":{"path":".worktrees/297-enforcement-shape/docs"},"intent":"Listing architecture documents"},"id":"a8dc3b6a","parentId":"9de4ff8d","timestamp":"2026-09-14T14:02:20.288Z"}
{"type":"message","id":"d1e35082","parentId":"a8dc3b6a","timestamp":"2026-09-14T14:02:20.288Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_5MOjT8LLgQagWlZ8VVddid6g|fc_0a63d1e8a5b46c9c016aa7fe6c348487d1b320568c27183ae4","name":"grep","arguments":{"i":"Finding spawn deadline boundaries","pattern":"spawn_session|waiter_armed|SPAWN_PHASE|read deadline|poison|PUMP_IPC_READER","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","case":true,"gitignore":false,"skip":null},"intent":"Finding spawn deadline boundaries"},{"type":"toolCall","id":"call_KpY58xSrrUzxjZts8WA9zjXb|fc_0a63d1e8a5b46c9c016aa7fe6c349087d1927a82c66d4fcf37","name":"read","arguments":{"i":"Reading project instructions","path":".worktrees/297-enforcement-shape/AGENTS.md"},"intent":"Reading project instructions"},{"type":"toolCall","id":"call_UMBmHMU5lhBYefSNQSr3k19A|fc_0a63d1e8a5b46c9c016aa7fe6c349887d1b411ec4c52ba9824","name":"read","arguments":{"i":"Listing architecture documents","path":".worktrees/297-enforcement-shape/docs"},"intent":"Listing architecture documents"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":353,"output":178,"cacheRead":12032,"cacheWrite":0,"totalTokens":12563,"cost":{"input":0.00353,"output":0.0089,"cacheRead":0.012032,"cacheWrite":0,"total":0.024461999999999998}},"stopReason":"toolUse","timestamp":1789394533367,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe65a2bc87d1b46ed38ace6f05bb","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe6c348487d1b320568c27183ae4","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding spawn deadline boundaries\",\"pattern\":\"spawn_session|waiter_armed|SPAWN_PHASE|read deadline|poison|PUMP_IPC_READER\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_5MOjT8LLgQagWlZ8VVddid6g","internal_chat_message_metadata_passthrough":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"},"name":"grep","metadata":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe6c349087d1927a82c66d4fcf37","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading project instructions\",\"path\":\".worktrees/297-enforcement-shape/AGENTS.md\"}","call_id":"call_KpY58xSrrUzxjZts8WA9zjXb","internal_chat_message_metadata_passthrough":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"},"name":"read","metadata":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe6c349887d1b411ec4c52ba9824","type":"function_call","status":"completed","arguments":"{\"i\":\"Listing architecture documents\",\"path\":\".worktrees/297-enforcement-shape/docs\"}","call_id":"call_UMBmHMU5lhBYefSNQSr3k19A","internal_chat_message_metadata_passthrough":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"},"name":"read","metadata":{"turn_id":"5da05906-116b-4260-8452-285e456a76ca"}}]},"duration":6918.865800008178,"ttft":6867.879399999976,"completedAt":1789394540286,"contextSnapshot":{"promptTokens":12385,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"e3b3d5f2","parentId":"d1e35082","timestamp":"2026-09-14T14:02:20.305Z","message":{"role":"toolResult","toolCallId":"call_UMBmHMU5lhBYefSNQSr3k19A|fc_0a63d1e8a5b46c9c016aa7fe6c349887d1b411ec4c52ba9824","toolName":"read","content":[{"type":"text","text":".\n  - 297-ENFORCEMENT-CODES-DIAGNOSIS.md                     12.9KB    1h ago\n  - spikes/                                                          2h ago\n    - pump-on-one-box-rig.patch                            35.8KB    2h ago\n    - SPIKE-07-branchstore.md                              5.6KB     2h ago\n    - SPIKE-06-idempotent-boundary.md                      4.5KB     2h ago\n    - SPIKE-05-restart-stress.md                           4.4KB     2h ago\n    - SPIKE-04-forkpty-parity.md                           3.0KB     2h ago\n    - SPIKE-03-quic-survival.md                            3.9KB     2h ago\n    - SPIKE-02-iroh-smoke.md                               5.4KB     2h ago\n    - SPIKE-01-broker-handoff.md                           3.8KB     2h ago\n  - reviews/                                                         2h ago\n    - STAGE-A-codex-redteam.md                             7.2KB     2h ago\n  - instruments/                                                     2h ago\n    - ir59/                                                          2h ago\n    - ir57/                                                          2h ago\n    - ir21/                                                          2h ago\n  - design/                                                          2h ago\n    - v0.16.0-update-arc-and-cli.md                        12.9KB    2h ago\n    - subnet-presence-display.md                           4.7KB     2h ago\n    - robust-wan-subnet-join.md                            6.1KB     2h ago\n    - RIDER-65S-DESIGN.md                                  11.5KB    2h ago\n  - adr/                                                             2h ago\n    - 0059-lan-exposure-only-for-the-bootstrap-surface.md  13.5KB    1h ago\n    - 0061-message-short-id-is-node-scoped.md              3.0KB     2h ago\n    - 0060-mint-web-surface-retire-xfer.md                 3.7KB     2h ago\n    - 0058-attachments-are-pull-model.md                   7.8KB     2h ago\n    - 0057-serving-registry-is-the-primitive.md            5.7KB     2h ago\n    - 0056-node-prefixed-url-namespace.md                  6.4KB     2h ago\n    - 0055-sender-declared-knock-directionality.md         3.5KB     2h ago\n    - 0054-sealed-knock-codes.md                           13.1KB    2h ago\n    - 0053-access-store-fail-closed.md                     3.6KB     2h ago\n    - 0052-engine-room-endpoint-empower.md                 26.4KB    2h ago\n    - 0050-golden-ci-merge-integration.md                  19.7KB    2h ago\n    - … 48 more\n    - 0001-clean-room-fork-from-claude-skill-owl.md        2.5KB     2h ago\n  - W6-DISPATCH-RULING.md                                  3.7KB     2h ago\n  - W6-GATE-VERDICT.md                                     4.4KB     2h ago\n  - W5-GATE-VERDICT.md                                     4.0KB     2h ago\n  - W5-WAVE-GATE-REPORT.md                                 8.2KB     2h ago\n  - W5-DISPATCH-RULING.md                                  5.7KB     2h ago\n  - W5-GATE-HOLD.md                                        3.2KB     2h ago\n  - W4-WAVE-GATE-REPORT.md                                 4.9KB     2h ago\n  - W4-GATE-VERDICT.md                                     2.8KB     2h ago\n  - W4-DISPATCH-RULING.md                                  3.3KB     2h ago\n  - W3-WAVE-GATE-REPORT.md                                 4.9KB     2h ago\n  - W3-SLICE1-REPORT.md                                    3.3KB     2h ago\n  - W3-GATE-VERDICT.md                                     4.6KB     2h ago\n  - W3-ENDPOINT-SURVIVAL-DESIGN.md                         5.3KB     2h ago\n  - W3-DIGEST-GENERATION-UNION-RULING.md                   5.5KB     2h ago\n  - W2-GATE-VERDICT.md                                     4.6KB     2h ago\n  - W2-DESIGN-RULING.md                                    4.5KB     2h ago\n  - W2-BUILT-REPORT.md                                     5.5KB     2h ago\n  - W2-DESIGN-PROPOSAL.md                                  3.7KB     2h ago\n  - UPDATE-WEDGE-2-ROUND3-CODEREAD.md                      7.6KB     2h ago\n  - UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md                   5.2KB     2h ago\n  - TWO-HOST-RUNBOOK.md                                    22.6KB    2h ago\n  - TRACEABILITY.md                                        5.9KB     2h ago\n  - THE-FORKENING-TRIAGE.md                                6.2KB     2h ago\n  - STORAGE.md                                             35.7KB    2h ago\n  - REMOTE-FRICTION-PLAN.md                                7.8KB     2h ago\n  - REMOTE-FRICTION-GRILL.md                               3.8KB     2h ago\n  - RELEASE-RUNBOOK.md                                     45.9KB    2h ago\n  - REGISTRY-LIFECYCLE-TRIAGE.md                           10.1KB    2h ago\n  - REDISPATCH-TRUTH-TRIAGE.md                             7.4KB     2h ago\n  - RCA-FLEET-DAEMON-14444.md                              9.2KB     2h ago\n  - REDISPATCH-STALL-TRIAGE.md                             9.0KB     2h ago\n  - PUMP-W2-GATE-HANDOFF.md                                3.6KB     2h ago\n  - RC-RENDER-TRUTH-DISPATCH.md                            16.0KB    2h ago\n  - PUMP-W2-DISPATCH-RULING.md                             8.9KB     2h ago\n  - PUMP-W2-DESIGN.md                                      10.2KB    2h ago\n  - PUMP-W2-BUILD-GO.md                                    4.9KB     2h ago\n  - PUMP-W2-BUILD-DELTA.md                                 3.8KB     2h ago\n  - PUMP-TRUTH-TRIAGE.md                                   13.3KB    2h ago\n  - PROJECT-INDEX-TRIAGE.md                                5.4KB     2h ago\n  - PID-KILL-CENSUS.md                                     7.5KB     2h ago\n  - NOTIF-TRUTH-DISPATCH.md                                7.4KB     2h ago\n  - NEXT-MILESTONE-WORKERTRUTH-TRIAGE.md                   9.5KB     2h ago\n  - NEXT-MILESTONE-PICKER-TRIAGE.md                        17.9KB    2h ago\n  - NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md                   27.6KB    2h ago\n  - NEXT-MILESTONE-RUNTRUTH-TRIAGE.md                      22.5KB    2h ago\n  - NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md                 21.0KB    2h ago\n  - NEXT-MILESTONE-MUSL-TIER-TRIAGE.md                     6.0KB     2h ago\n  - NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md                14.7KB    2h ago\n  - NEXT-MILESTONE-BUG-TRIAGE.md                           43.9KB    2h ago\n  - MUSL-RELEASE-HANDOFF.md                                2.9KB     2h ago\n  - MUSL-W1-DISPATCH-RULING.md                             4.1KB     2h ago\n  - MUSL-W3-DISPATCH-RULING.md                             4.5KB     2h ago\n  - MSG-IDENTITY-DISPATCH.md                               7.0KB     2h ago\n  - MESH-RECOVERY-TRIAGE.md                                7.7KB     2h ago\n  - MANIFEST.md                                            70.7KB    2h ago\n  - KNOWN-HAZARDS.md                                       351.9KB   2h ago\n  - INPUT-PROVENANCE-CONTRACT.md                           37.6KB    2h ago\n  - INJECT-SETTLE-REARM-GATE-VERDICT.md                    4.3KB     2h ago\n  - INFRA-REGISTER.md                                      610.5KB   2h ago\n  - GOLDEN-CI.md                                           13.4KB    2h ago\n  - GATEWAY-RCA-STEP1.md                                   4.3KB     2h ago\n  - FLAKE-LEDGER.md                                        58.4KB    2h ago\n  - GATEWAY-LIVENESS-DISPATCH.md                           5.7KB     2h ago\n  - F-035-RELAY-RCA.md                                     4.0KB     2h ago\n  - FAULT-MATRIX.md                                        16.3KB    2h ago\n  - F-035-RCA.md                                           5.9KB     2h ago\n  - F-035-CONFLICT.md                                      6.6KB     2h ago\n  - F-030-PSYCHE-EPHEMERAL-DESIGN.md                       15.7KB    2h ago\n  - ER-SKELETON.md                                         6.0KB     2h ago\n  - F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md                    13.8KB    2h ago\n  - DORMANCY-BUDGET.md                                     4.9KB     2h ago\n  - DOCS-STRATEGY.md                                       12.8KB    2h ago\n  - DEFERRED.md                                            47.0KB    2h ago\n  - DEBUG-ROLLOUT.md                                       5.4KB     2h ago\n  - DEBUG-CONVERGE-PLAN.md                                 7.7KB     2h ago\n  - CONTEXT-MEMORY.md                                      8.9KB     2h ago\n  - BROKER-BRAIN-SPLIT-RESTORATION.md                      30.2KB    2h ago\n  - 49-267-REFRESH-OBSERVATION-PLAN.md                     14.9KB    2h ago\n  - 304-W2-FIELD-PLAN.md                                   12.9KB    2h ago\n  - 304-W2-ADMISSION-RECEIPT.md                            10.0KB    2h ago\n  - 297-OUT-OF-BAND-CAPTURE-PLAN.md                        19.3KB    2h ago\n  - 297-CAPTURE-INSTRUMENT-SPEC.md                         19.9KB    2h ago\n\n[1 results limit reached. Use limit=2 for more]"}],"details":{"isDirectory":true,"resolvedPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\docs","meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\docs"},"limits":{"resultLimit":{"reached":1,"suggestion":2}}}},"isError":false,"timestamp":1789394540304}}
{"type":"message","id":"b13e3e5c","parentId":"e3b3d5f2","timestamp":"2026-09-14T14:02:20.312Z","message":{"role":"toolResult","toolCallId":"call_KpY58xSrrUzxjZts8WA9zjXb|fc_0a63d1e8a5b46c9c016aa7fe6c349087d1927a82c66d4fcf37","toolName":"read","content":[{"type":"text","text":"# spt-core — agent working rules\n\nHarness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking, runtime-manifest harness contract). Rust crates + a single `spt` binary. Clean-room rebuild of `claude_skill_owl`.\n\n**Orientation — read before working:** `PRD.md` (requirements), `ROADMAP.md` (build path), `CONTEXT.md` (glossary/model, authoritative for meaning), `docs/adr/` (decisions), `docs/KNOWN-HAZARDS.md` (invariants we must not re-break), `docs/{STORAGE,MANIFEST,CONTEXT-MEMORY,DOCS-STRATEGY,TRACEABILITY}.md`.\n\nFor maintainer/debug update work, also read `docs/DEBUG-ROLLOUT.md` before touching the update-set or rollout path.\n\nFor work needing Linux tests and proof, this machine is authorized to ssh into a Linux box: `reavus@kitsubito`\n\nIMPORTANT: All agents you work alongside run on spt-core itself — the legacy-SPT (`$OWL`/`$LIVE`) era ended with the 2026-07 fleet migration; those commands are gone. Reach other agents with `spt send` (or the `/sptc:send` skill); keep message bodies short, and send long bodies from a file rather than inline. Everything else can be learned from the corresponding `/sptc:` skill doc.\n\n## Requirement traceability (binding)\n\nThis project uses `traceable-reqs` (`traceable-reqs.toml` = the authoritative `REQ-*` registry). The full contract is `docs/TRACEABILITY.md`. The rules you must follow:\n\n1. **Tag evidence in the same change.** When you write a function/test/doc-section that satisfies a requirement stage, add its tag in that same commit:\n   `// [impl->REQ-FOO]` · `// [unit->REQ-FOO]` · `<!-- [doc->REQ-FOO] -->`\n   Stages: `doc` / `impl` / `unit` / `int`. Tag *on or immediately above* the real evidence — never at file tops to satisfy coverage.\n2. **Run `traceable-reqs check` before declaring work done.** Exit-1 means missing/invalid evidence — fix it, don't ship it.\n3. **New requirement → add it to `traceable-reqs.toml` first** (with a `REQ-*` id), then satisfy it. No untracked work; no untagged evidence.\n4. **KNOWN-HAZARDS are `REQ-HAZARD-*` requirements** — each needs a test before it's \"covered.\" Treat the hazard list as a conformance checklist you must satisfy, not advice.\n5. **Activate, don't pre-fail.** Requirements you aren't yet working stay `required_stages = []`. Activate (set real stages) only when starting the milestone that delivers them.\n\n## Issue tracking (unique case)\n\nCodebase issues are maintained on **`BigscreenVR/spt-bs-releases`** — the less-private *releases* repo — NOT on `spt-bs-core` where the code lives. This is a deliberate exception (operator-ruled 2026-07-27): file, triage, and reference issues against `spt-bs-releases`.\n\nRequests and milestones are tracked through the **alchemy shell** (`alchemy-0`, the Requests shell bound to `spt-bs-releases`) on top of those GitHub issues. The label taxonomy is the workflow: `state:` BACKLOG → EVAL → GREENLIT → WIP → ACCEPTANCE → DONE (plus CUT / SHELVED), `type:` BUGFIX / ADDITION / CHANGE, `flag: NEEDS-OPERATOR`, and per-node `kind: SPT-TRACKING` pins (marked DO NOT DELETE — leave them alone).\n\n## Other conventions\n\n- Match surrounding code style; copy-verbatim the sister project's stable wire/schema formats (ADR-0001), clean-room everything else.\n- Honor every `docs/KNOWN-HAZARDS.md` invariant — these are real bugs already paid for once.\n- Docs are dual-audience (human + AI dev-agent) per `docs/DOCS-STRATEGY.md`; doc generation is CI-gated against drift.\n- Commit messages end with the project's co-author trailer, exact spelling `Co-authored by: <your live agent name>` (e.g. `Co-authored by: todlando`) — NOT git's hyphenated `Co-Authored-By:`; CI's runner-informant parses only the space spelling for attribution.\n  - **Audit this trailer with `git log --grep` or a raw body read, NEVER `%(trailers:)`** (nor `interpret-trailers --parse`): git's trailer tokenizer requires a whitespace-free token, so the space spelling is structurally invisible to it and every trailer query returns a confident EMPTY on every commit — a clean zero that reads as total attribution loss. Measured 2026-07-30 (main @`8f3e10b`, Signed-off-by and hyphenated-key controls both parse); re-derived at full cost 2026-08-03 by one of its own original measurers, which is why this sentence lives here at the mandate rather than in a document you must decide to open.\n- **Worktrees (binding, operator-ruled 2026-07-05):** every `git worktree` (gate rigs, agent scratch, anything) goes under `.worktrees/` in THIS project's root (`.gitignore`d) — e.g. `git worktree add --detach .worktrees/gate-<sha> <sha>`. NEVER create worktrees as siblings of the project dir; stray sibling folders accumulate orphan-pinned skeletons and pollute `~/Documents/projects`. Remove yours when done (`git worktree remove` + prune; retry later if a handle pins it).\n<!-- [doc->REQ-POOL-FOREIGN-TREE-REFUSED] -->\n- **Build-cache pools (binding, releases#103):** a cargo `target/` directory belongs to ONE source tree. Two trees building into one pool overwrite each other's artifacts and fabricate reds that belong to neither — the v0.51.0 W1/H1 `E0308` that vanished on re-run. **The rule is about the RELATIONSHIP, not the path:** a junction and a `CARGO_TARGET_DIR` env var reach the same hazard, and the env var leaves no directory entry to notice afterwards, so \"is this dir under `.worktrees/`\" never was the question. A build script enforces it (`crates/spt-store/build.rs`) and refuses with `SPT_POOL_FOREIGN`. **Sharing a pool SEQUENTIALLY is fine and expected** — a finished lane is taken over loudly, not refused; only a LIVE lane blocks you. Claim a pool at lane start …\n- **Target-tree teardown (releases#103, same lane):** never delete a `target/` you have not classified. Check OUTBOUND first (`Get-Item -Force`: a junction is deleted as a link, a real dir as a tree), then sweep INBOUND for reparse points aimed at it before removing a real one, and measure size and free space before and after so the reclaim is a number rather than a claim. Reap the target SUBTREE, never a session or worktree directory around it. Do not point `CARGO_TARGET_DIR` into a scratchpad (v0.50.0 incident) or at another checkout's target (H1 incident).\n- **CI / merge strategy:** golden CI (ADR-0050, ruled 2026-07-27) — milestone-batch golden runs, thin-lane PRs, ff-only main (tested sha == merged sha). Piloting from the next milestone; read the ADR before shaping a PR lane.\n\nIf you finish a significant body of work without need for user intervention, or if your context gets too high, you can clear your own context and keep moving:\n1. Create a JIT plan for the next immediate body of work, if it isn't already planned\n2. If you are a live agent, /sptc:commune with immediate next steps and broad summary of the project's status + end goal\n3. Execute the /self-clear skill\n\n[Some lines truncated to 768 chars]"}],"details":{"totalLines":47,"displayContent":{"text":"# spt-core — agent working rules\r\n\r\nHarness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking, runtime-manifest harness contract). Rust crates + a single `spt` binary. Clean-room rebuild of `claude_skill_owl`.\r\n\r\n**Orientation — read before working:** `PRD.md` (requirements), `ROADMAP.md` (build path), `CONTEXT.md` (glossary/model, authoritative for meaning), `docs/adr/` (decisions), `docs/KNOWN-HAZARDS.md` (invariants we must not re-break), `docs/{STORAGE,MANIFEST,CONTEXT-MEMORY,DOCS-STRATEGY,TRACEABILITY}.md`.\r\n\r\nFor maintainer/debug update work, also read `docs/DEBUG-ROLLOUT.md` before touching the update-set or rollout path.\r\n\r\nFor work needing Linux tests and proof, this machine is authorized to ssh into a Linux box: `reavus@kitsubito`\r\n\r\nIMPORTANT: All agents you work alongside run on spt-core itself — the legacy-SPT (`$OWL`/`$LIVE`) era ended with the 2026-07 fleet migration; those commands are gone. Reach other agents with `spt send` (or the `/sptc:send` skill); keep message bodies short, and send long bodies from a file rather than inline. Everything else can be learned from the corresponding `/sptc:` skill doc.\r\n\r\n## Requirement traceability (binding)\r\n\r\nThis project uses `traceable-reqs` (`traceable-reqs.toml` = the authoritative `REQ-*` registry). The full contract is `docs/TRACEABILITY.md`. The rules you must follow:\r\n\r\n1. **Tag evidence in the same change.** When you write a function/test/doc-section that satisfies a requirement stage, add its tag in that same commit:\r\n   `// [impl->REQ-FOO]` · `// [unit->REQ-FOO]` · `<!-- [doc->REQ-FOO] -->`\r\n   Stages: `doc` / `impl` / `unit` / `int`. Tag *on or immediately above* the real evidence — never at file tops to satisfy coverage.\r\n2. **Run `traceable-reqs check` before declaring work done.** Exit-1 means missing/invalid evidence — fix it, don't ship it.\r\n3. **New requirement → add it to `traceable-reqs.toml` first** (with a `REQ-*` id), then satisfy it. No untracked work; no untagged evidence.\r\n4. **KNOWN-HAZARDS are `REQ-HAZARD-*` requirements** — each needs a test before it's \"covered.\" Treat the hazard list as a conformance checklist you must satisfy, not advice.\r\n5. **Activate, don't pre-fail.** Requirements you aren't yet working stay `required_stages = []`. Activate (set real stages) only when starting the milestone that delivers them.\r\n\r\n## Issue tracking (unique case)\r\n\r\nCodebase issues are maintained on **`BigscreenVR/spt-bs-releases`** — the less-private *releases* repo — NOT on `spt-bs-core` where the code lives. This is a deliberate exception (operator-ruled 2026-07-27): file, triage, and reference issues against `spt-bs-releases`.\r\n\r\nRequests and milestones are tracked through the **alchemy shell** (`alchemy-0`, the Requests shell bound to `spt-bs-releases`) on top of those GitHub issues. The label taxonomy is the workflow: `state:` BACKLOG → EVAL → GREENLIT → WIP → ACCEPTANCE → DONE (plus CUT / SHELVED), `type:` BUGFIX / ADDITION / CHANGE, `flag: NEEDS-OPERATOR`, and per-node `kind: SPT-TRACKING` pins (marked DO NOT DELETE — leave them alone).\r\n\r\n## Other conventions\r\n\r\n- Match surrounding code style; copy-verbatim the sister project's stable wire/schema formats (ADR-0001), clean-room everything else.\r\n- Honor every `docs/KNOWN-HAZARDS.md` invariant — these are real bugs already paid for once.\r\n- Docs are dual-audience (human + AI dev-agent) per `docs/DOCS-STRATEGY.md`; doc generation is CI-gated against drift.\r\n- Commit messages end with the project's co-author trailer, exact spelling `Co-authored by: <your live agent name>` (e.g. `Co-authored by: todlando`) — NOT git's hyphenated `Co-Authored-By:`; CI's runner-informant parses only the space spelling for attribution.\r\n  - **Audit this trailer with `git log --grep` or a raw body read, NEVER `%(trailers:)`** (nor `interpret-trailers --parse`): git's trailer tokenizer requires a whitespace-free token, so the space spelling is structurally invisible to it and every trailer query returns a confident EMPTY on every commit — a clean zero that reads as total attribution loss. Measured 2026-07-30 (main @`8f3e10b`, Signed-off-by and hyphenated-key controls both parse); re-derived at full cost 2026-08-03 by one of its own original measurers, which is why this sentence lives here at the mandate rather than in a document you must decide to open.\r\n- **Worktrees (binding, operator-ruled 2026-07-05):** every `git worktree` (gate rigs, agent scratch, anything) goes under `.worktrees/` in THIS project's root (`.gitignore`d) — e.g. `git worktree add --detach .worktrees/gate-<sha> <sha>`. NEVER create worktrees as siblings of the project dir; stray sibling folders accumulate orphan-pinned skeletons and pollute `~/Documents/projects`. Remove yours when done (`git worktree remove` + prune; retry later if a handle pins it).\r\n<!-- [doc->REQ-POOL-FOREIGN-TREE-REFUSED] -->\r\n- **Build-cache pools (binding, releases#103):** a cargo `target/` directory belongs to ONE source tree. Two trees building into one pool overwrite each other's artifacts and fabricate reds that belong to neither — the v0.51.0 W1/H1 `E0308` that vanished on re-run. **The rule is about the RELATIONSHIP, not the path:** a junction and a `CARGO_TARGET_DIR` env var reach the same hazard, and the env var leaves no directory entry to notice afterwards, so \"is this dir under `.worktrees/`\" never was the question. A build script enforces it (`crates/spt-store/build.rs`) and refuses with `SPT_POOL_FOREIGN`. **Sharing a pool SEQUENTIALLY is fine and expected** — a finished lane is taken over loudly, not refused; only a LIVE lane blocks you. Claim a pool at lane start …\n- **Target-tree teardown (releases#103, same lane):** never delete a `target/` you have not classified. Check OUTBOUND first (`Get-Item -Force`: a junction is deleted as a link, a real dir as a tree), then sweep INBOUND for reparse points aimed at it before removing a real one, and measure size and free space before and after so the reclaim is a number rather than a claim. Reap the target SUBTREE, never a session or worktree directory around it. Do not point `CARGO_TARGET_DIR` into a scratchpad (v0.50.0 incident) or at another checkout's target (H1 incident).\r\n- **CI / merge strategy:** golden CI (ADR-0050, ruled 2026-07-27) — milestone-batch golden runs, thin-lane PRs, ff-only main (tested sha == merged sha). Piloting from the next milestone; read the ADR before shaping a PR lane.\r\n\r\nIf you finish a significant body of work without need for user intervention, or if your context gets too high, you can clear your own context and keep moving:\r\n1. Create a JIT plan for the next immediate body of work, if it isn't already planned\r\n2. If you are a live agent, /sptc:commune with immediate next steps and broad summary of the project's status + end goal\r\n3. Execute the /self-clear skill\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]},"fileSize":8436,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\AGENTS.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789394540311}}
{"type":"message","id":"18a52e87","parentId":"b13e3e5c","timestamp":"2026-09-14T14:02:20.326Z","message":{"role":"toolResult","toolCallId":"call_5MOjT8LLgQagWlZ8VVddid6g|fc_0a63d1e8a5b46c9c016aa7fe6c348487d1b320568c27183ae4","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## brain.rs\n 316|/// TERMINAL: the marker landed at the reconnect banner's parked cursor inside\n*317|/// rc's owned alt screen (`Reconnecting to local daemon… 9sPUMP_IPC_READER:\n 318|/// spawned` — hertz RCA 3 bug 1), so clients pass [`PumpTrace::from_env`]:\n 319|/// silent unless `SPT_PUMP_TRACE=1` explicitly opts in (the e2e rigs do — the\n 320|/// marker is their pump-construction witness; a user terminal never does).\n...\n 379|                if trace == PumpTrace::Stderr {\n*380|                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: spawned\");\n 381|                }\n 382|                loop {\n 383|                    let framed = read_frame(&mut recv);\n...\n 396|                if trace == PumpTrace::Stderr {\n*397|                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: exited\");\n 398|                }\n 399|            })\n 400|            .expect(\"spawn pump-ipc-reader thread\");\n...\n 603|    /// races ahead).\n*604|    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\n*605|        self.spawn_session_pid(req)\n 606|            .map(|(session_id, _)| session_id)\n 607|    }\n 608|\n...\n 815|\n*816|    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\n 817|    /// OS pid (when the backend exposes one) — measurement/observability\n 818|    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\n 819|    ///\n...\n 829|    /// session) or route the frames it reads for other sessions, not drop them.\n*830|    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\n 831|        self.send(\n 832|            KIND_SPAWN,\n 833|            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n...\n 860|    /// the IPC failure truthfully. Same `Spawned`-wait caveat as\n*861|    /// [`Self::spawn_session_pid`] (KNOWN-HAZARDS 6.9).\n 862|    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n*863|    pub fn spawn_session_fresh_pid(&mut self, req: SpawnReq) -> io::Result<FreshSpawn> {\n 864|        self.send(\n 865|            KIND_SPAWN_FRESH,\n 866|            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n...\n 1059|    /// RESTART (a timed-out IPC read may have desynced the stream; the client\n*1060|    /// is poisoned, never retried per-peer).\n 1061|    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n 1062|    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n 1063|    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n...\n 1384|    /// `peer_outcome` drops ONLY that peer instead of POISONING the whole round +\n*1385|    /// supervised-restarting. The `TimedOut` poison stays RESERVED for a genuine\n 1386|    /// broker-IPC-CARRIER desync (the carrier ops — `net_open_stream` /\n 1387|    /// `net_stream_subscribe` / `net_stream_send` — keep their raw `TimedOut`).\n 1388|    /// Abandoning the peer's stream is safe: the exactly-once seq cursor stays\n 1389|    /// contiguous, and a late reply on the dropped stream id matches nothing (the\n*1390|    /// leg loops treat it as noise) — the desync the old blanket-poison feared\n 1391|    /// cannot happen on the still-ordered brain-IPC channel.\n 1392|    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n 1393|    pub fn read_peer_reply_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n...\n 1687|    /// address, conn count — or `enabled: false` on a net-less broker. Reads\n*1688|    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).\n 1689|    // [impl->REQ-DAEMON-2]\n 1690|    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {\n 1691|        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;\n...\n 2576|    ///   on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]\n*2577|    ///   — the poison signal the pump escalates to a supervised restart (NEVER a\n 2578|    ///   per-peer retry). A `Disconnected` channel means the reader thread exited\n 2579|    ///   (the conn hung up); surface it as `UnexpectedEof`.\n 2580|    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n...\n 2588|                io::ErrorKind::Unsupported,\n*2589|                \"brain IPC read deadline cannot be honored on the blocking (Whole) \\\n 2590|                 carrier — it reads the frame inline, so a timeout would abandon a \\\n 2591|                 half-read frame and desync the conn. Build the client with \\\n 2592|                 `Brain::cold_start_pump` (the Split reader-thread carrier), which \\\n...\n 2606|                            io::ErrorKind::TimedOut,\n*2607|                            \"brain IPC read deadline elapsed\",\n 2608|                        )),\n 2609|                        Err(RecvTimeoutError::Disconnected) => Err(io::Error::new(\n 2610|                            io::ErrorKind::UnexpectedEof,\n...\n 2634|/// carrier deadline (`Brain::call_deadline`) so a connect-then-silent peer drops\n*2635|/// PROMPTLY and can never race the carrier deadline into a whole-round poison —\n 2636|/// the round-2 empirical stall root (`request_update`'s reply-read burned the\n 2637|/// full 30s `PUMP_PEER_IO_TIMEOUT`).\n 2638|pub(crate) const PEER_REPLY_READ_BUDGET: Duration = Duration::from_secs(10);\n...\n 2681|    if e.kind() == io::ErrorKind::TimedOut {\n*2682|        // NON-poison kind (Other) => peer_outcome's ordinary per-peer drop arm.\n 2683|        io::Error::other(format!(\n 2684|            \"peer reply-read: no progress within budget — dropping peer ({e})\"\n 2685|        ))\n...\n 2708|    // the 30s carrier deadline: a pump-mode reply-read caps at the 10s budget (so\n*2709|    // a silent peer drops before the carrier deadline can fire a poison), and the\n 2710|    // `min` still honors a shorter carrier `io_timeout`; non-pump is unbounded.\n 2711|    #[test]\n 2712|    fn peer_reply_deadline_caps_below_carrier_deadline() {\n...\n 2758|    // peer's no-progress TimedOut is mapped OUT of TimedOut (so peer_outcome drops\n*2759|    // it as an ordinary per-peer failure, NOT a carrier-desync poison); any other\n 2760|    // error passes through unchanged (a carrier op's own TimedOut never routes here).\n 2761|    #[test]\n## broker.rs\n 91|/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n*92|/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n 93|/// matching stays `Arc::ptr_eq`.\n 94|pub(crate) type SharedSend = Arc<BrokerConn>;\n 95|\n...\n 205|/// transition's extra memory alone. Overflow drops from the grid parse ONLY, loudly,\n*206|/// and self-heals on the next output + repaint exactly like the poison-recover ring\n 207|/// reset — it never drops a byte from the ring or from a subscriber.\n 208|const RESIZE_HOLD_CAP: usize = 1 << 20;\n 209|\n...\n 893|    /// notice could not be enqueued (the try_send-on-Full drop this replaces);\n*894|    /// a conn too wedged to take that bounded write is poisoned + severed by\n 895|    /// the existing per-write watchdog, and the closed stream is itself the\n 896|    /// terminal signal the incumbent's rc already handles. Never set on a\n 897|    /// same-lease successor re-take (the gate-#7 silence is preserved).\n...\n 1334|/// ([`spt_runtime::runtime::TEST_TIMING_ENV_VARS`]) so a dev-shell export cannot\n*1335|/// poison a debug-build daemon on a shared box either; in-process test brokers do\n 1336|/// not run the daemon entrypoints, so the int gate is unaffected by that scrub.\n 1337|///\n 1338|/// It does NOT suppress a correctness mechanism and it fabricates nothing: it holds\n...\n 1470|    #[must_use]\n*1471|    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n 1472|    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n 1473|    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n 1474|    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n...\n 1479|    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*1480|    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n 1481|    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 1482|    fn clamp_or_reset(&mut self) -> bool {\n 1483|        let within_cap = self.ring.len() <= self.cap_chunks;\n...\n 1497|        eprintln!(\n*1498|            \"broker: session {} output ring was torn by a poison-recover \\\n 1499|             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n 1500|             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n 1501|            self.session_id, self.next_seq\n...\n 2796|    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n*2797|    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n 2798|    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n 2799|    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n 2800|    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n...\n 3420|    /// empty grid's repaint substitutes for nothing: no sync frame is owed (the\n*3421|    /// pseudo-seq would poison the resume cursor past the real seq 0).\n 3422|    // [impl->REQ-RC-RESIZE-PRESENTATION-BARRIER]\n 3423|    fn push_sync_frame(&mut self, suppressed: Option<(u64, u64)>, deferred: Vec<u8>) {\n 3424|        // Leg 4: record the presentation floor FIRST — even when no sync frame is\n...\n 3786|        // Bounded, cancelable conn write (round-4): a viewer that stops\n*3787|        // reading poisons + retires ITS conn at the deadline instead of\n 3788|        // parking this thread in the OS write forever.\n 3789|        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n 3790|        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\n...\n 3830|        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n*3831|         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n 3832|         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n 3833|        crate::conn::log_stamp()\n 3834|    );\n...\n 3986|    // the live queue's fullness. A write the conn cannot take retires it via\n*3987|    // the per-write watchdog (poison + sever) — the closed conn ends the serve\n 3988|    // worker, which is the same terminal signal for the incumbent's rc. Silent\n 3989|    // (same-lease) replacements never set the marker, so gate-#7 stays silent.\n 3990|    let emit_revoke_if_marked = |send: &SharedSend| {\n...\n 4020|        // Round-4: the write itself is bounded + cancelable — a non-draining\n*4021|        // consumer poisons + retires the physical conn at the deadline instead\n 4022|        // of wedging this thread in the OS write forever.\n 4023|        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n 4024|        mark_blocked(true);\n...\n 4357|/// drains empty and the floor frees.\n*4358|/// Lock the per-session inject floor, RECOVERING a poisoned mutex instead of\n 4359|/// panicking (B6, REQ-HAZARD-INJECT-WORKER-POISON). The floor is SHARED by the\n 4360|/// inject worker (`run_inject_worker` open / `flush_inject_floor`) and the\n 4361|/// controller-input path (`dispatch_input` Layer C `buffer_if_held`). A panic under\n*4362|/// the lock on EITHER poisons it; a bare `.lock().unwrap()` at the next site then\n 4363|/// panics too — and a panic in the inject WORKER kills its thread WITHOUT reaping\n 4364|/// the translation child, ORPHANING a live binary while `event_tx.send` fails, so\n 4365|/// force-native delivery reports \"worker-gone\" (delivered=false) FOREVER though the\n 4366|/// pid is alive (the F-e mis-diagnosed \"translation-binary liveness decay\").\n*4367|/// Recovering the guard (the effect-journal poison-tolerance idiom, bug #16 /\n 4368|/// REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) keeps the floor usable so ONE panic cannot\n 4369|/// cascade the whole session's delivery dead.\n 4370|// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\n...\n 4394|/// [`recover`] for the per-session [`OutputLog`] RING, with a COHERENCE CLAMP on the\n*4395|/// poison-recovery path (B-1, doyle ruling). A panic mid-[`OutputLog::append`] can\n 4396|/// leave the ring torn (a bumped `next_seq` past the ring, an over-cap ring from an\n 4397|/// interrupted prune, a non-monotonic front/back), and replaying those bytes to a\n*4398|/// subscriber risks garbage. So ONLY on the recovered (poisoned) branch, run\n 4399|/// [`OutputLog::clamp_or_reset`]: cheap-check the ring invariants and RESET it empty\n 4400|/// (+ loud log) on any violation. Lost scrollback self-heals on the next PTY output +\n 4401|/// repaint — strictly better than a permanent wedge (fail-fast reintroduces the very\n...\n 4728|            // B6 (REQ-HAZARD-INJECT-WORKER-POISON): run the worker under a panic\n*4729|            // guard. A panic inside the worker (a residual unwrap, a poisoned lock the\n 4730|            // recovery missed, anything) must NOT silently kill the thread and orphan\n 4731|            // the translation child — that drops `event_rx` WITHOUT reaping the binary,\n 4732|            // so a live pid + failed `event_tx.send` reports \"worker-gone\" forever. On\n...\n 5382|    /// UPDATE-WEDGE round-4 int gate's observability seam\n*5383|    /// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): a poisoned/aborted\n 5384|    /// physical conn must drive its handler to EOF cleanup within the write\n 5385|    /// bound WITHOUT the client dropping — the test watches this count fall.\n 5386|    conn_handlers: AtomicUsize,"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs, .worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":40,"fileCount":2,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs","count":20},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## brain.rs\n  316│/// TERMINAL: the marker landed at the reconnect banner's parked cursor inside\n *317│/// rc's owned alt screen (`Reconnecting to local daemon… 9sPUMP_IPC_READER:\n  318│/// spawned` — hertz RCA 3 bug 1), so clients pass [`PumpTrace::from_env`]:\n  319│/// silent unless `SPT_PUMP_TRACE=1` explicitly opts in (the e2e rigs do — the\n  320│/// marker is their pump-construction witness; a user terminal never does).\n     │...\n  379│                if trace == PumpTrace::Stderr {\n *380│                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: spawned\");\n  381│                }\n  382│                loop {\n  383│                    let framed = read_frame(&mut recv);\n     │...\n  396│                if trace == PumpTrace::Stderr {\n *397│                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: exited\");\n  398│                }\n  399│            })\n  400│            .expect(\"spawn pump-ipc-reader thread\");\n     │...\n  603│    /// races ahead).\n *604│    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\n *605│        self.spawn_session_pid(req)\n  606│            .map(|(session_id, _)| session_id)\n  607│    }\n  608│\n     │...\n  815│\n *816│    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\n  817│    /// OS pid (when the backend exposes one) — measurement/observability\n  818│    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\n  819│    ///\n     │...\n  829│    /// session) or route the frames it reads for other sessions, not drop them.\n *830│    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\n  831│        self.send(\n  832│            KIND_SPAWN,\n  833│            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n     │...\n  860│    /// the IPC failure truthfully. Same `Spawned`-wait caveat as\n *861│    /// [`Self::spawn_session_pid`] (KNOWN-HAZARDS 6.9).\n  862│    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n *863│    pub fn spawn_session_fresh_pid(&mut self, req: SpawnReq) -> io::Result<FreshSpawn> {\n  864│        self.send(\n  865│            KIND_SPAWN_FRESH,\n  866│            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n     │...\n 1059│    /// RESTART (a timed-out IPC read may have desynced the stream; the client\n*1060│    /// is poisoned, never retried per-peer).\n 1061│    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n 1062│    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n 1063│    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n     │...\n 1384│    /// `peer_outcome` drops ONLY that peer instead of POISONING the whole round +\n*1385│    /// supervised-restarting. The `TimedOut` poison stays RESERVED for a genuine\n 1386│    /// broker-IPC-CARRIER desync (the carrier ops — `net_open_stream` /\n 1387│    /// `net_stream_subscribe` / `net_stream_send` — keep their raw `TimedOut`).\n 1388│    /// Abandoning the peer's stream is safe: the exactly-once seq cursor stays\n 1389│    /// contiguous, and a late reply on the dropped stream id matches nothing (the\n*1390│    /// leg loops treat it as noise) — the desync the old blanket-poison feared\n 1391│    /// cannot happen on the still-ordered brain-IPC channel.\n 1392│    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n 1393│    pub fn read_peer_reply_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n     │...\n 1687│    /// address, conn count — or `enabled: false` on a net-less broker. Reads\n*1688│    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).\n 1689│    // [impl->REQ-DAEMON-2]\n 1690│    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {\n 1691│        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;\n     │...\n 2576│    ///   on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]\n*2577│    ///   — the poison signal the pump escalates to a supervised restart (NEVER a\n 2578│    ///   per-peer retry). A `Disconnected` channel means the reader thread exited\n 2579│    ///   (the conn hung up); surface it as `UnexpectedEof`.\n 2580│    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n     │...\n 2588│                io::ErrorKind::Unsupported,\n*2589│                \"brain IPC read deadline cannot be honored on the blocking (Whole) \\\n 2590│                 carrier — it reads the frame inline, so a timeout would abandon a \\\n 2591│                 half-read frame and desync the conn. Build the client with \\\n 2592│                 `Brain::cold_start_pump` (the Split reader-thread carrier), which \\\n     │...\n 2606│                            io::ErrorKind::TimedOut,\n*2607│                            \"brain IPC read deadline elapsed\",\n 2608│                        )),\n 2609│                        Err(RecvTimeoutError::Disconnected) => Err(io::Error::new(\n 2610│                            io::ErrorKind::UnexpectedEof,\n     │...\n 2634│/// carrier deadline (`Brain::call_deadline`) so a connect-then-silent peer drops\n*2635│/// PROMPTLY and can never race the carrier deadline into a whole-round poison —\n 2636│/// the round-2 empirical stall root (`request_update`'s reply-read burned the\n 2637│/// full 30s `PUMP_PEER_IO_TIMEOUT`).\n 2638│pub(crate) const PEER_REPLY_READ_BUDGET: Duration = Duration::from_secs(10);\n     │...\n 2681│    if e.kind() == io::ErrorKind::TimedOut {\n*2682│        // NON-poison kind (Other) => peer_outcome's ordinary per-peer drop arm.\n 2683│        io::Error::other(format!(\n 2684│            \"peer reply-read: no progress within budget — dropping peer ({e})\"\n 2685│        ))\n     │...\n 2708│    // the 30s carrier deadline: a pump-mode reply-read caps at the 10s budget (so\n*2709│    // a silent peer drops before the carrier deadline can fire a poison), and the\n 2710│    // `min` still honors a shorter carrier `io_timeout`; non-pump is unbounded.\n 2711│    #[test]\n 2712│    fn peer_reply_deadline_caps_below_carrier_deadline() {\n     │...\n 2758│    // peer's no-progress TimedOut is mapped OUT of TimedOut (so peer_outcome drops\n*2759│    // it as an ordinary per-peer failure, NOT a carrier-desync poison); any other\n 2760│    // error passes through unchanged (a carrier op's own TimedOut never routes here).\n 2761│    #[test]\n## broker.rs\n   91│/// `Arc<Mutex<SendHalf>>` — [`BrokerConn`] owns the half and every write is\n  *92│/// bounded + cancelable + poison-on-failure (see [`crate::conn`]). Identity\n   93│/// matching stays `Arc::ptr_eq`.\n   94│pub(crate) type SharedSend = Arc<BrokerConn>;\n   95│\n     │...\n  205│/// transition's extra memory alone. Overflow drops from the grid parse ONLY, loudly,\n *206│/// and self-heals on the next output + repaint exactly like the poison-recover ring\n  207│/// reset — it never drops a byte from the ring or from a subscriber.\n  208│const RESIZE_HOLD_CAP: usize = 1 << 20;\n  209│\n     │...\n  893│    /// notice could not be enqueued (the try_send-on-Full drop this replaces);\n *894│    /// a conn too wedged to take that bounded write is poisoned + severed by\n  895│    /// the existing per-write watchdog, and the closed stream is itself the\n  896│    /// terminal signal the incumbent's rc already handles. Never set on a\n  897│    /// same-lease successor re-take (the gate-#7 silence is preserved).\n     │...\n 1334│/// ([`spt_runtime::runtime::TEST_TIMING_ENV_VARS`]) so a dev-shell export cannot\n*1335│/// poison a debug-build daemon on a shared box either; in-process test brokers do\n 1336│/// not run the daemon entrypoints, so the int gate is unaffected by that scrub.\n 1337│///\n 1338│/// It does NOT suppress a correctness mechanism and it fabricates nothing: it holds\n     │...\n 1470│    #[must_use]\n*1471│    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n 1472│    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n 1473│    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n 1474│    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n     │...\n 1479│    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n*1480│    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n 1481│    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n 1482│    fn clamp_or_reset(&mut self) -> bool {\n 1483│        let within_cap = self.ring.len() <= self.cap_chunks;\n     │...\n 1497│        eprintln!(\n*1498│            \"broker: session {} output ring was torn by a poison-recover \\\n 1499│             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n 1500│             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n 1501│            self.session_id, self.next_seq\n     │...\n 2796│    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n*2797│    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n 2798│    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n 2799│    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n 2800│    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n     │...\n 3420│    /// empty grid's repaint substitutes for nothing: no sync frame is owed (the\n*3421│    /// pseudo-seq would poison the resume cursor past the real seq 0).\n 3422│    // [impl->REQ-RC-RESIZE-PRESENTATION-BARRIER]\n 3423│    fn push_sync_frame(&mut self, suppressed: Option<(u64, u64)>, deferred: Vec<u8>) {\n 3424│        // Leg 4: record the presentation floor FIRST — even when no sync frame is\n     │...\n 3786│        // Bounded, cancelable conn write (round-4): a viewer that stops\n*3787│        // reading poisons + retires ITS conn at the deadline instead of\n 3788│        // parking this thread in the OS write forever.\n 3789│        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n 3790│        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\n     │...\n 3830│        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n*3831│         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n 3832│         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n 3833│        crate::conn::log_stamp()\n 3834│    );\n     │...\n 3986│    // the live queue's fullness. A write the conn cannot take retires it via\n*3987│    // the per-write watchdog (poison + sever) — the closed conn ends the serve\n 3988│    // worker, which is the same terminal signal for the incumbent's rc. Silent\n 3989│    // (same-lease) replacements never set the marker, so gate-#7 stays silent.\n 3990│    let emit_revoke_if_marked = |send: &SharedSend| {\n     │...\n 4020│        // Round-4: the write itself is bounded + cancelable — a non-draining\n*4021│        // consumer poisons + retires the physical conn at the deadline instead\n 4022│        // of wedging this thread in the OS write forever.\n 4023│        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n 4024│        mark_blocked(true);\n     │...\n 4357│/// drains empty and the floor frees.\n*4358│/// Lock the per-session inject floor, RECOVERING a poisoned mutex instead of\n 4359│/// panicking (B6, REQ-HAZARD-INJECT-WORKER-POISON). The floor is SHARED by the\n 4360│/// inject worker (`run_inject_worker` open / `flush_inject_floor`) and the\n 4361│/// controller-input path (`dispatch_input` Layer C `buffer_if_held`). A panic under\n*4362│/// the lock on EITHER poisons it; a bare `.lock().unwrap()` at the next site then\n 4363│/// panics too — and a panic in the inject WORKER kills its thread WITHOUT reaping\n 4364│/// the translation child, ORPHANING a live binary while `event_tx.send` fails, so\n 4365│/// force-native delivery reports \"worker-gone\" (delivered=false) FOREVER though the\n 4366│/// pid is alive (the F-e mis-diagnosed \"translation-binary liveness decay\").\n*4367│/// Recovering the guard (the effect-journal poison-tolerance idiom, bug #16 /\n 4368│/// REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) keeps the floor usable so ONE panic cannot\n 4369│/// cascade the whole session's delivery dead.\n 4370│// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\n     │...\n 4394│/// [`recover`] for the per-session [`OutputLog`] RING, with a COHERENCE CLAMP on the\n*4395│/// poison-recovery path (B-1, doyle ruling). A panic mid-[`OutputLog::append`] can\n 4396│/// leave the ring torn (a bumped `next_seq` past the ring, an over-cap ring from an\n 4397│/// interrupted prune, a non-monotonic front/back), and replaying those bytes to a\n*4398│/// subscriber risks garbage. So ONLY on the recovered (poisoned) branch, run\n 4399│/// [`OutputLog::clamp_or_reset`]: cheap-check the ring invariants and RESET it empty\n 4400│/// (+ loud log) on any violation. Lost scrollback self-heals on the next PTY output +\n 4401│/// repaint — strictly better than a permanent wedge (fail-fast reintroduces the very\n     │...\n 4728│            // B6 (REQ-HAZARD-INJECT-WORKER-POISON): run the worker under a panic\n*4729│            // guard. A panic inside the worker (a residual unwrap, a poisoned lock the\n 4730│            // recovery missed, anything) must NOT silently kill the thread and orphan\n 4731│            // the translation child — that drops `event_rx` WITHOUT reaping the binary,\n 4732│            // so a live pid + failed `event_tx.send` reports \"worker-gone\" forever. On\n     │...\n 5382│    /// UPDATE-WEDGE round-4 int gate's observability seam\n*5383│    /// (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK): a poisoned/aborted\n 5384│    /// physical conn must drive its handler to EOF cleanup within the write\n 5385│    /// bound WITHOUT the client dropping — the test watches this count fall.\n 5386│    conn_handlers: AtomicUsize,"},"isError":false,"timestamp":1789394540325}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `NetRuntimeTrace`:\n\nI own exact candidate NetHost runtime + translation/journal startup after waiter_armed; tracing bounds and #302 docs. Please share precise spawn call boundary and relevant remaining phases; I will return runtime ownership/bounds.\n\nSent while waiting/working. Active interruptible wait stopped early for immediate reading.\n\nIf response expected, reply via `hub` (`op: \"send\"`, `to: \"NetRuntimeTrace\"`); may finish current step first. No one replies on your behalf.\n</irc>","display":true,"details":{"id":"157f5bc34646a0ca","from":"NetRuntimeTrace","message":"I own exact candidate NetHost runtime + translation/journal startup after waiter_armed; tracing bounds and #302 docs. Please share precise spawn call boundary and relevant remaining phases; I will return runtime ownership/bounds."},"attribution":"agent","id":"eb670d57","parentId":"18a52e87","timestamp":"2026-09-14T14:02:13.657Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jqgd2cT44UEBWMklsPqFKKYe|fc_0a63d1e8a5b46c9c016aa7fe75ec8887d1a48d76daa01de330","toolName":"grep","startedAt":"2026-09-14T14:02:30.006Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"},"intent":"Finding exact spawn handler"},"id":"613c2c23","parentId":"eb670d57","timestamp":"2026-09-14T14:02:30.006Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cu59KVhLVN0ZXCdvIXgEicBm|fc_0a63d1e8a5b46c9c016aa7fe75ec9487d18d5cb52e664cb4f7","toolName":"read","startedAt":"2026-09-14T14:02:30.007Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs:325-440,816-902,2530-2625"},"intent":"Reading brain pump construction"},"id":"973468ed","parentId":"613c2c23","timestamp":"2026-09-14T14:02:30.007Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pxiITKlfHnKgIJrm4lHc3U3i|fc_0a63d1e8a5b46c9c016aa7fe75ec9c87d1a7010621bf331c56","toolName":"grep","startedAt":"2026-09-14T14:02:30.007Z","args":{"path":".worktrees/297-enforcement-shape/CONTEXT.md;.worktrees/297-enforcement-shape/docs/adr/*brain*;.worktrees/297-enforcement-shape/docs/adr/*broker*;.worktrees/297-enforcement-shape/docs/adr/*process*;.wo…"},"intent":"Locating process control vocabulary"},"id":"8b6f8841","parentId":"973468ed","timestamp":"2026-09-14T14:02:30.007Z"}
{"type":"message","id":"66d7afa1","parentId":"8b6f8841","timestamp":"2026-09-14T14:02:30.007Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_jqgd2cT44UEBWMklsPqFKKYe|fc_0a63d1e8a5b46c9c016aa7fe75ec8887d1a48d76daa01de330","name":"grep","arguments":{"i":"Finding exact spawn handler","pattern":"waiter_armed|SPAWN_PHASE|KIND_SPAWN =>|fn handle_spawn","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","case":true,"gitignore":false,"skip":null},"intent":"Finding exact spawn handler"},{"type":"toolCall","id":"call_cu59KVhLVN0ZXCdvIXgEicBm|fc_0a63d1e8a5b46c9c016aa7fe75ec9487d18d5cb52e664cb4f7","name":"read","arguments":{"i":"Reading brain pump construction","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs:325-440,816-902,2530-2625"},"intent":"Reading brain pump construction"},{"type":"toolCall","id":"call_pxiITKlfHnKgIJrm4lHc3U3i|fc_0a63d1e8a5b46c9c016aa7fe75ec9c87d1a7010621bf331c56","name":"grep","arguments":{"i":"Locating process control vocabulary","pattern":"^#|brain|broker|ConPTY|deadline","path":".worktrees/297-enforcement-shape/CONTEXT.md;.worktrees/297-enforcement-shape/docs/adr/*brain*;.worktrees/297-enforcement-shape/docs/adr/*broker*;.worktrees/297-enforcement-shape/docs/adr/*process*;.worktrees/297-enforcement-shape/docs/adr/*control*","case":false,"gitignore":false,"skip":null},"intent":"Locating process control vocabulary"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":8727,"output":268,"cacheRead":12288,"cacheWrite":0,"totalTokens":21283,"cost":{"input":0.08727,"output":0.0134,"cacheRead":0.012288,"cacheWrite":0,"total":0.112958}},"stopReason":"toolUse","timestamp":1789394540344,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe6ca1b087d1bdedb18777d7591d","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe75ec8887d1a48d76daa01de330","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding exact spawn handler\",\"pattern\":\"waiter_armed|SPAWN_PHASE|KIND_SPAWN =>|fn handle_spawn\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_jqgd2cT44UEBWMklsPqFKKYe","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe75ec9487d18d5cb52e664cb4f7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain pump construction\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs:325-440,816-902,2530-2625\"}","call_id":"call_cu59KVhLVN0ZXCdvIXgEicBm","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe75ec9c87d1a7010621bf331c56","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating process control vocabulary\",\"pattern\":\"^#|brain|broker|ConPTY|deadline\",\"path\":\".worktrees/297-enforcement-shape/CONTEXT.md;.worktrees/297-enforcement-shape/docs/adr/*brain*;.worktrees/297-enforcement-shape/docs/adr/*broker*;.worktrees/297-enforcement-shape/docs/adr/*process*;.worktrees/297-enforcement-shape/docs/adr/*control*\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_pxiITKlfHnKgIJrm4lHc3U3i","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":9660.176200002432,"ttft":9621.016499996185,"completedAt":1789394550005,"contextSnapshot":{"promptTokens":21015,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"bff92fc4","parentId":"66d7afa1","timestamp":"2026-09-14T14:02:30.022Z","message":{"role":"toolResult","toolCallId":"call_jqgd2cT44UEBWMklsPqFKKYe|fc_0a63d1e8a5b46c9c016aa7fe75ec8887d1a48d76daa01de330","toolName":"grep","content":[{"type":"text","text":" 6091|            match env.kind.as_str() {\n*6092|                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n 6093|                    Ok(id) => my_subs.push(id),\n 6094|                    Err(msg) => send_error(&send, &msg),\n 6095|                },\n...\n 8112|            spt_proto::emit_line_err!(\n*8113|                \"SPAWN_PHASE:{spawn_label} phase={phase} t_ms={}{}{detail}\",\n 8114|                spawn_t0.elapsed().as_millis(),\n 8115|                if detail.is_empty() { \"\" } else { \" \" },\n 8116|            );\n...\n 8566|                Some(h) => spt_proto::emit_line_err!(\n*8567|                    \"SPAWN_PHASE:{} phase=row_removed session={id} lived_ms={}\",\n 8568|                    h.endpoint,\n 8569|                    h.spawned_at.elapsed().as_millis()\n 8570|                ),\n...\n 8578|                None => spt_proto::emit_line_err!(\n*8579|                    \"SPAWN_PHASE:<no-row> phase=row_removed session={id} entry=absent \\\n 8580|                     — already de-tabled elsewhere; no lifetime is known and none is invented\"\n 8581|                ),\n 8582|            }\n...\n 8671|\n*8672|        spawn_phase(\"waiter_armed\", \"\");\n 8673|\n 8674|        // Layer C: if the spawn carried a translation binary, bring it up with the\n 8675|        // session — spawn the pure stdin→stdout filter + the inject worker that"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\broker.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":5,"fileCount":1,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","count":5}],"truncated":false,"displayContent":" 6091│            match env.kind.as_str() {\n*6092│                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n 6093│                    Ok(id) => my_subs.push(id),\n 6094│                    Err(msg) => send_error(&send, &msg),\n 6095│                },\n     │...\n 8112│            spt_proto::emit_line_err!(\n*8113│                \"SPAWN_PHASE:{spawn_label} phase={phase} t_ms={}{}{detail}\",\n 8114│                spawn_t0.elapsed().as_millis(),\n 8115│                if detail.is_empty() { \"\" } else { \" \" },\n 8116│            );\n     │...\n 8566│                Some(h) => spt_proto::emit_line_err!(\n*8567│                    \"SPAWN_PHASE:{} phase=row_removed session={id} lived_ms={}\",\n 8568│                    h.endpoint,\n 8569│                    h.spawned_at.elapsed().as_millis()\n 8570│                ),\n     │...\n 8578│                None => spt_proto::emit_line_err!(\n*8579│                    \"SPAWN_PHASE:<no-row> phase=row_removed session={id} entry=absent \\\n 8580│                     — already de-tabled elsewhere; no lifetime is known and none is invented\"\n 8581│                ),\n 8582│            }\n     │...\n 8671│\n*8672│        spawn_phase(\"waiter_armed\", \"\");\n 8673│\n 8674│        // Layer C: if the spawn carried a translation binary, bring it up with the\n 8675│        // session — spawn the pure stdin→stdout filter + the inject worker that"},"isError":false,"timestamp":1789394550022}}
{"type":"message","id":"78ff191e","parentId":"bff92fc4","timestamp":"2026-09-14T14:02:30.045Z","message":{"role":"toolResult","toolCallId":"call_cu59KVhLVN0ZXCdvIXgEicBm|fc_0a63d1e8a5b46c9c016aa7fe75ec9487d18d5cb52e664cb4f7","toolName":"read","content":[{"type":"text","text":"pub enum PumpTrace {\n…\n    /// never a terminal an interactive client owns).\n    Stderr,\n    /// No lifecycle markers.\n    Silent,\n}\n\nimpl PumpTrace {\n    /// The interactive-client policy: silent unless `SPT_PUMP_TRACE=1` opts\n    /// in. The env var is read by the CLIENT process only — the daemon never\n    /// exports it (an exported var would be inherited by hosted harness\n    /// subtrees and re-introduce markers into terminals spt does not own).\n    pub fn from_env() -> Self {\n        if std::env::var(\"SPT_PUMP_TRACE\").as_deref() == Ok(\"1\") {\n            PumpTrace::Stderr\n        } else {\n            PumpTrace::Silent\n        }\n    }\n}\n\n/// The SAME opt-in switch, read once, for the daemon-side diagnostic lines that\n/// attribute an investigated wait (releases#49/#267): the controller-slot\n/// closure edge and the promotion DRAINED gate's first not-drained read.\n///\n/// It is the existing gate rather than a second one on purpose — a diagnosis\n/// that needs two switches set has a way to be half-enabled, and the half that\n/// is off is the half nobody notices missing. The daemon reads its OWN\n/// environment here; it still never EXPORTS the var, so hosted harness subtrees\n/// inherit nothing (the [`PumpTrace::from_env`] rule this shares).\n///\n/// Read once: these sites sit on the control plane's hot edges, and an env\n/// lookup per closure would be a cost the diagnosis imposed on the thing it is\n/// measuring.\n// [impl->REQ-REFRESH-WAIT-ATTRIBUTION]\npub fn diag_trace_enabled() -> bool {\n    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();\n    *ENABLED.get_or_init(|| PumpTrace::from_env() == PumpTrace::Stderr)\n}\n\nimpl BrainConn {\n    /// Split a connected+handshaked stream into the pump carrier: spawn the\n    /// `pump-ipc-reader` thread (blocking [`read_frame`] → channel) and keep the\n    /// [`SendHalf`] for writes. The reader logs spawn/exit per `trace` so a\n    /// leaked (parked) reader is diagnosable in the field (KH 7.6) WITHOUT\n    /// writing into a display surface an interactive client owns\n    /// (REQ-RC-DISPLAY-SOLE-WRITER — see [`PumpTrace`]).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-RC-DISPLAY-SOLE-WRITER]\n    fn split_with_reader(stream: Stream, trace: PumpTrace) -> BrainConn {\n        let (mut recv, send) = stream.split();\n        let (tx, frames) = mpsc::channel();\n        let reader = thread::Builder::new()\n            .name(\"pump-ipc-reader\".to_string())\n            .spawn(move || {\n                if trace == PumpTrace::Stderr {\n                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: spawned\");\n                }\n                loop {\n                    let framed = read_frame(&mut recv);\n                    let was_err = framed.is_err();\n                    // Receiver gone (Brain dropped) → stop reading.\n                    if tx.send(framed).is_err() {\n                        break;\n                    }\n                    // A transport error (conn closed / hung up) is terminal —\n                    // forward it once (above), then exit so the thread never\n                    // spins on a dead half.\n                    if was_err {\n                        break;\n                    }\n                }\n                if trace == PumpTrace::Stderr {\n                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: exited\");\n                }\n            })\n            .expect(\"spawn pump-ipc-reader thread\");\n        BrainConn::Split {\n            send,\n            frames,\n            _reader: reader,\n        }\n    }\n}\n\n/// A live brain attached to a broker.\npub struct Brain {\n    conn: BrainConn,\n    generation: u64,\n    gen_start_ms: u64,\n    session_id: Option<u64>,\n    /// The next output sequence still needed (resume cursor / dedup floor).\n    next_seq: u64,\n    /// Per-net-stream resume cursors (D4b) — the stream-read siblings of\n    /// `next_seq`, set by [`Brain::net_stream_subscribe`] and advanced by\n    /// [`Brain::read_event_until`] under the same accept/dedup/gap discipline.\n    /// Durability across a handoff is the logic layer's job (a re-attaching\n    /// brain queries `net-streams` and re-subscribes from its durable cursors).\n    net_cursors: std::collections::HashMap<u64, u64>,\n    /// The presence resume cursor (D4c) — the conn-liveness sibling of\n    /// `net_cursors`, set by [`Brain::net_presence_subscribe`] and advanced by\n    /// [`Brain::read_event_until`] under the same accept/dedup/gap discipline.\n    /// `None` until subscribed (or the first event arrives).\n    presence_cursor: Option<u64>,\n    /// Per-session **resume** output cursors (restoration D4-2): the multi-session\n    /// continuity state a cold-started brain rebuilds from the broker via\n    /// [`Brain::resume_sessions`], replacing the retired single-session\n    /// `BrainState` handoff. A session present here is in **resume mode** — its\n    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n    /// boundary re-send) and accept-and-snap `seq >= cursor` (the contiguous case\n    /// *and* a post-eviction ring-floor jump — the broker's `Mutex`-held replay\n    /// cannot reorder, so a forward jump is only ever a legitimate eviction clamp,\n    /// never a real gap). A session **absent** here uses the strict single-session\n    /// `next_seq` path (reject-gap) byte-for-byte — the legacy spawn/drive seat.\n    session_cursors: std::collections::HashMap<u64, u64>,\n    /// When `Some`, every reply read is bounded by this per-call deadline budget\n…\n}\n…\n    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\n    /// OS pid (when the backend exposes one) — measurement/observability\n    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\n    ///\n    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\n    /// calls [`Brain::read_event_until`] on every interleaved frame, so on a *resume-mode*\n    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\n    /// **different** session read here is cursor-processed (its cursor snaps; the\n    /// broker already counted it delivered) and then **discarded** by `_ => continue`\n    /// — that chunk is lost to its consumer and the broker will not re-send it.\n    /// Unreachable today (the supervised brain spawns nothing; a legacy single-session\n    /// seat has no other session); the daemon-hosted live-agent event pump must make\n    /// this non-blocking (one demux owns `read_event`, routing each frame to its\n    /// session) or route the frames it reads for other sessions, not drop them.\n    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\n        self.send(\n            KIND_SPAWN,\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n        )?;\n        // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::Spawned { session_id, pid } => {\n                    self.session_id = Some(session_id);\n                    // A resume-mode brain tracks this fresh session per-session via\n                    // `read_event`'s first-sight `or_insert` seed (D4-2b) — no seed\n                    // needed here; a legacy brain (empty map) uses `next_seq`.\n                    return Ok((session_id, pid));\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => continue,\n            }\n        }\n    }\n\n    /// Spawn under the FRESH/RejectExisting policy (ADR-0041 decision 5,\n    /// [`KIND_SPAWN_FRESH`]): an endpoint that already holds a live session\n    /// answers a TYPED conflict — never `Spawned(existing)`. An N-1 broker that\n    /// predates the verb answers unknown-kind, which surfaces here as a LOUD\n    /// `Err` (never a fake Created, never a silent dedup to the old verb) —\n    /// there is deliberately NO fallback to [`KIND_SPAWN`]; the caller reports\n    /// the IPC failure truthfully. Same `Spawned`-wait caveat as\n    /// [`Self::spawn_session_pid`] (KNOWN-HAZARDS 6.9).\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n    pub fn spawn_session_fresh_pid(&mut self, req: SpawnReq) -> io::Result<FreshSpawn> {\n        self.send(\n            KIND_SPAWN_FRESH,\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n        )?;\n        // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::Spawned { session_id, pid } => {\n                    self.session_id = Some(session_id);\n                    return Ok(FreshSpawn::Created { session_id, pid });\n                }\n                BrokerEvent::SpawnConflict { session_id, .. } => {\n                    return Ok(FreshSpawn::Conflict {\n                        existing_session: session_id,\n                    });\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => continue,\n            }\n        }\n    }\n\n    /// Inject input bytes into the current session (send-keys), the **legacy\n    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\n    /// of an effect that originates from a durable source (a spool row, a\n    /// sequenced injection), use [`Brain::send_effect`].\n    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\n        let sid = self.require_session()?;\n        self.send(\n            KIND_INPUT,\n            serde_json::to_value(InputReq {\n                session_id: sid,\n                data_b64: encode_bytes(bytes),\n                op_id: None,\n                minter: None,\n                ack: true,\n            })\n            .expect(\"InputReq serializes\"),\n        )\n    }\n…\n    fn subscribe_with(\n…\n    ) -> io::Result<()> {\n…\n        // seq K arrives below the advanced cursor and is deduped → seq K never\n        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n        // legacy `next_seq` path; this only arms in resume mode.\n        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n        if !self.session_cursors.is_empty() {\n            self.session_cursors.insert(session_id, from_seq);\n        }\n        self.send(\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id,\n                from_seq,\n                intent,\n                by,\n                gen,\n                code,\n                seal_ceremony,\n            })\n            .expect(\"SubscribeReq serializes\"),\n        )\n    }\n\n    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {\n        let env = Envelope::new(kind, payload);\n        match &mut self.conn {\n            BrainConn::Whole(stream) => write_frame(stream, &env),\n            BrainConn::Split { send, .. } => write_frame(send, &env),\n        }\n    }\n\n    /// Read one framed [`Envelope`], bounded by an optional wall-clock\n    /// `deadline`. The carrier dispatch (REQ-HAZARD-PUMP-IPC-DEADLINE):\n    ///\n    /// - [`BrainConn::Whole`] — the ordinary blocking client. `None` is the\n    ///   ordinary blocking [`read_frame`]. `Some(deadline)` is REFUSED BY NAME\n    ///   (releases#190): this carrier reads the frame inline, so there is no\n    ///   thread to hand a deadline to and a timeout would have to abandon a\n    ///   half-read frame — a desynced carrier, which is exactly why the pump\n    ///   carrier was SPLIT instead of given a socket timeout. Pre-fix this arm\n    ///   dropped the parameter on the floor, so a caller that asked for a\n    ///   bounded read silently got an unbounded one; the defect was the\n    ///   silence, not the missing timeout.\n    /// - [`BrainConn::Split`] — the pump carrier: the reader thread already owns\n    ///   the blocking read, so a bounded read is just [`Receiver::recv_timeout`]\n    ///   on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]\n    ///   — the poison signal the pump escalates to a supervised restart (NEVER a\n    ///   per-peer retry). A `Disconnected` channel means the reader thread exited\n    ///   (the conn hung up); surface it as `UnexpectedEof`.\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-IPC-DEADLINE-UNHONORABLE-REFUSED]\n    fn read_frame_until(&mut self, deadline: Option<Instant>) -> io::Result<Envelope> {\n        match &mut self.conn {\n            // Absent stays absent: no fabricated default bound on the blocking\n            // read (the absent-is-not-zero shape, releases#201).\n            BrainConn::Whole(stream) if deadline.is_none() => read_frame(stream),\n            BrainConn::Whole(_) => Err(io::Error::new(\n                io::ErrorKind::Unsupported,\n                \"brain IPC read deadline cannot be honored on the blocking (Whole) \\\n                 carrier — it reads the frame inline, so a timeout would abandon a \\\n                 half-read frame and desync the conn. Build the client with \\\n                 `Brain::cold_start_pump` (the Split reader-thread carrier), which \\\n                 bounds every read; or pass no deadline to read unbounded on purpose\",\n            )),\n            BrainConn::Split { frames, .. } => match deadline {\n                Some(dl) => {\n                    // Total-wait bound: each recv is the REMAINING budget to the\n                    // call's entry deadline, so unrelated frames dripping in\n                    // cannot reset it (the per-read-timeout wedge the ruling\n                    // forbids). A non-positive remaining → recv_timeout(ZERO),\n                    // which still drains an already-queued frame before timing out.\n                    let remaining = dl.saturating_duration_since(Instant::now());\n                    match frames.recv_timeout(remaining) {\n                        Ok(framed) => framed,\n                        Err(RecvTimeoutError::Timeout) => Err(io::Error::new(\n                            io::ErrorKind::TimedOut,\n                            \"brain IPC read deadline elapsed\",\n                        )),\n                        Err(RecvTimeoutError::Disconnected) => Err(io::Error::new(\n                            io::ErrorKind::UnexpectedEof,\n                            \"brain IPC reader thread ended (conn closed)\",\n                        )),\n                    }\n                }\n                None => frames.recv().map_err(|_| {\n                    io::Error::new(\n                        io::ErrorKind::UnexpectedEof,\n                        \"brain IPC reader thread ended (conn closed)\",\n                    )\n                })?,\n            },\n        }\n    }\n\n    fn require_session(&self) -> io::Result<u64> {\n…\n    }"}],"details":{"displayContent":{"text":"pub enum PumpTrace {\n…\n    /// never a terminal an interactive client owns).\n    Stderr,\n    /// No lifecycle markers.\n    Silent,\n}\n\nimpl PumpTrace {\n    /// The interactive-client policy: silent unless `SPT_PUMP_TRACE=1` opts\n    /// in. The env var is read by the CLIENT process only — the daemon never\n    /// exports it (an exported var would be inherited by hosted harness\n    /// subtrees and re-introduce markers into terminals spt does not own).\n    pub fn from_env() -> Self {\n        if std::env::var(\"SPT_PUMP_TRACE\").as_deref() == Ok(\"1\") {\n            PumpTrace::Stderr\n        } else {\n            PumpTrace::Silent\n        }\n    }\n}\n\n/// The SAME opt-in switch, read once, for the daemon-side diagnostic lines that\n/// attribute an investigated wait (releases#49/#267): the controller-slot\n/// closure edge and the promotion DRAINED gate's first not-drained read.\n///\n/// It is the existing gate rather than a second one on purpose — a diagnosis\n/// that needs two switches set has a way to be half-enabled, and the half that\n/// is off is the half nobody notices missing. The daemon reads its OWN\n/// environment here; it still never EXPORTS the var, so hosted harness subtrees\n/// inherit nothing (the [`PumpTrace::from_env`] rule this shares).\n///\n/// Read once: these sites sit on the control plane's hot edges, and an env\n/// lookup per closure would be a cost the diagnosis imposed on the thing it is\n/// measuring.\n// [impl->REQ-REFRESH-WAIT-ATTRIBUTION]\npub fn diag_trace_enabled() -> bool {\n    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();\n    *ENABLED.get_or_init(|| PumpTrace::from_env() == PumpTrace::Stderr)\n}\n\nimpl BrainConn {\n    /// Split a connected+handshaked stream into the pump carrier: spawn the\n    /// `pump-ipc-reader` thread (blocking [`read_frame`] → channel) and keep the\n    /// [`SendHalf`] for writes. The reader logs spawn/exit per `trace` so a\n    /// leaked (parked) reader is diagnosable in the field (KH 7.6) WITHOUT\n    /// writing into a display surface an interactive client owns\n    /// (REQ-RC-DISPLAY-SOLE-WRITER — see [`PumpTrace`]).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-RC-DISPLAY-SOLE-WRITER]\n    fn split_with_reader(stream: Stream, trace: PumpTrace) -> BrainConn {\n        let (mut recv, send) = stream.split();\n        let (tx, frames) = mpsc::channel();\n        let reader = thread::Builder::new()\n            .name(\"pump-ipc-reader\".to_string())\n            .spawn(move || {\n                if trace == PumpTrace::Stderr {\n                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: spawned\");\n                }\n                loop {\n                    let framed = read_frame(&mut recv);\n                    let was_err = framed.is_err();\n                    // Receiver gone (Brain dropped) → stop reading.\n                    if tx.send(framed).is_err() {\n                        break;\n                    }\n                    // A transport error (conn closed / hung up) is terminal —\n                    // forward it once (above), then exit so the thread never\n                    // spins on a dead half.\n                    if was_err {\n                        break;\n                    }\n                }\n                if trace == PumpTrace::Stderr {\n                    spt_proto::emit_line_err!(\"PUMP_IPC_READER: exited\");\n                }\n            })\n            .expect(\"spawn pump-ipc-reader thread\");\n        BrainConn::Split {\n            send,\n            frames,\n            _reader: reader,\n        }\n    }\n}\n\n/// A live brain attached to a broker.\npub struct Brain {\n    conn: BrainConn,\n    generation: u64,\n    gen_start_ms: u64,\n    session_id: Option<u64>,\n    /// The next output sequence still needed (resume cursor / dedup floor).\n    next_seq: u64,\n    /// Per-net-stream resume cursors (D4b) — the stream-read siblings of\n    /// `next_seq`, set by [`Brain::net_stream_subscribe`] and advanced by\n    /// [`Brain::read_event_until`] under the same accept/dedup/gap discipline.\n    /// Durability across a handoff is the logic layer's job (a re-attaching\n    /// brain queries `net-streams` and re-subscribes from its durable cursors).\n    net_cursors: std::collections::HashMap<u64, u64>,\n    /// The presence resume cursor (D4c) — the conn-liveness sibling of\n    /// `net_cursors`, set by [`Brain::net_presence_subscribe`] and advanced by\n    /// [`Brain::read_event_until`] under the same accept/dedup/gap discipline.\n    /// `None` until subscribed (or the first event arrives).\n    presence_cursor: Option<u64>,\n    /// Per-session **resume** output cursors (restoration D4-2): the multi-session\n    /// continuity state a cold-started brain rebuilds from the broker via\n    /// [`Brain::resume_sessions`], replacing the retired single-session\n    /// `BrainState` handoff. A session present here is in **resume mode** — its\n    /// `KIND_OUTPUT` dedup is **dedup-below + snap-above, NO reject-gap** (seeded\n    /// at the broker's `resume_seq`): drop `seq < cursor` (the at-least-once\n    /// boundary re-send) and accept-and-snap `seq >= cursor` (the contiguous case\n    /// *and* a post-eviction ring-floor jump — the broker's `Mutex`-held replay\n    /// cannot reorder, so a forward jump is only ever a legitimate eviction clamp,\n    /// never a real gap). A session **absent** here uses the strict single-session\n    /// `next_seq` path (reject-gap) byte-for-byte — the legacy spawn/drive seat.\n    session_cursors: std::collections::HashMap<u64, u64>,\n    /// When `Some`, every reply read is bounded by this per-call deadline budget\n…\n}\n…\n    /// [`spawn_session`](Self::spawn_session) that also surfaces the child's\n    /// OS pid (when the backend exposes one) — measurement/observability\n    /// callers (the D9-3 dormancy-budget harness) sample the seat by pid.\n    ///\n    /// **Resume-mode caveat (KNOWN-HAZARDS 6.9).** The `Spawned`-wait loop below\n    /// calls [`Brain::read_event_until`] on every interleaved frame, so on a *resume-mode*\n    /// brain (multiple sessions in `session_cursors`) an OUTPUT frame for a\n    /// **different** session read here is cursor-processed (its cursor snaps; the\n    /// broker already counted it delivered) and then **discarded** by `_ => continue`\n    /// — that chunk is lost to its consumer and the broker will not re-send it.\n    /// Unreachable today (the supervised brain spawns nothing; a legacy single-session\n    /// seat has no other session); the daemon-hosted live-agent event pump must make\n    /// this non-blocking (one demux owns `read_event`, routing each frame to its\n    /// session) or route the frames it reads for other sessions, not drop them.\n    pub fn spawn_session_pid(&mut self, req: SpawnReq) -> io::Result<(u64, Option<u32>)> {\n        self.send(\n            KIND_SPAWN,\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n        )?;\n        // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::Spawned { session_id, pid } => {\n                    self.session_id = Some(session_id);\n                    // A resume-mode brain tracks this fresh session per-session via\n                    // `read_event`'s first-sight `or_insert` seed (D4-2b) — no seed\n                    // needed here; a legacy brain (empty map) uses `next_seq`.\n                    return Ok((session_id, pid));\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => continue,\n            }\n        }\n    }\n\n    /// Spawn under the FRESH/RejectExisting policy (ADR-0041 decision 5,\n    /// [`KIND_SPAWN_FRESH`]): an endpoint that already holds a live session\n    /// answers a TYPED conflict — never `Spawned(existing)`. An N-1 broker that\n    /// predates the verb answers unknown-kind, which surfaces here as a LOUD\n    /// `Err` (never a fake Created, never a silent dedup to the old verb) —\n    /// there is deliberately NO fallback to [`KIND_SPAWN`]; the caller reports\n    /// the IPC failure truthfully. Same `Spawned`-wait caveat as\n    /// [`Self::spawn_session_pid`] (KNOWN-HAZARDS 6.9).\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n    pub fn spawn_session_fresh_pid(&mut self, req: SpawnReq) -> io::Result<FreshSpawn> {\n        self.send(\n            KIND_SPAWN_FRESH,\n            serde_json::to_value(req).expect(\"SpawnReq serializes\"),\n        )?;\n        // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n        let deadline = self.call_deadline();\n        loop {\n            match self.read_event_until(deadline)? {\n                BrokerEvent::Spawned { session_id, pid } => {\n                    self.session_id = Some(session_id);\n                    return Ok(FreshSpawn::Created { session_id, pid });\n                }\n                BrokerEvent::SpawnConflict { session_id, .. } => {\n                    return Ok(FreshSpawn::Conflict {\n                        existing_session: session_id,\n                    });\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => continue,\n            }\n        }\n    }\n\n    /// Inject input bytes into the current session (send-keys), the **legacy\n    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\n    /// of an effect that originates from a durable source (a spool row, a\n    /// sequenced injection), use [`Brain::send_effect`].\n    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\n        let sid = self.require_session()?;\n        self.send(\n            KIND_INPUT,\n            serde_json::to_value(InputReq {\n                session_id: sid,\n                data_b64: encode_bytes(bytes),\n                op_id: None,\n                minter: None,\n                ack: true,\n            })\n            .expect(\"InputReq serializes\"),\n        )\n    }\n…\n    fn subscribe_with(\n…\n    ) -> io::Result<()> {\n…\n        // seq K arrives below the advanced cursor and is deduped → seq K never\n        // reaches the operator viewport → a forward gap. Resetting to `from_seq`\n        // makes the re-subscribe re-deliver everything from its floor. A cold-start\n        // brain (empty map — e.g. the production dispatch serve brain) keeps the\n        // legacy `next_seq` path; this only arms in resume mode.\n        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n        if !self.session_cursors.is_empty() {\n            self.session_cursors.insert(session_id, from_seq);\n        }\n        self.send(\n            KIND_SUBSCRIBE,\n            serde_json::to_value(SubscribeReq {\n                session_id,\n                from_seq,\n                intent,\n                by,\n                gen,\n                code,\n                seal_ceremony,\n            })\n            .expect(\"SubscribeReq serializes\"),\n        )\n    }\n\n    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {\n        let env = Envelope::new(kind, payload);\n        match &mut self.conn {\n            BrainConn::Whole(stream) => write_frame(stream, &env),\n            BrainConn::Split { send, .. } => write_frame(send, &env),\n        }\n    }\n\n    /// Read one framed [`Envelope`], bounded by an optional wall-clock\n    /// `deadline`. The carrier dispatch (REQ-HAZARD-PUMP-IPC-DEADLINE):\n    ///\n    /// - [`BrainConn::Whole`] — the ordinary blocking client. `None` is the\n    ///   ordinary blocking [`read_frame`]. `Some(deadline)` is REFUSED BY NAME\n    ///   (releases#190): this carrier reads the frame inline, so there is no\n    ///   thread to hand a deadline to and a timeout would have to abandon a\n    ///   half-read frame — a desynced carrier, which is exactly why the pump\n    ///   carrier was SPLIT instead of given a socket timeout. Pre-fix this arm\n    ///   dropped the parameter on the floor, so a caller that asked for a\n    ///   bounded read silently got an unbounded one; the defect was the\n    ///   silence, not the missing timeout.\n    /// - [`BrainConn::Split`] — the pump carrier: the reader thread already owns\n    ///   the blocking read, so a bounded read is just [`Receiver::recv_timeout`]\n    ///   on the remaining budget. A `Timeout` surfaces [`io::ErrorKind::TimedOut`]\n    ///   — the poison signal the pump escalates to a supervised restart (NEVER a\n    ///   per-peer retry). A `Disconnected` channel means the reader thread exited\n    ///   (the conn hung up); surface it as `UnexpectedEof`.\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-IPC-DEADLINE-UNHONORABLE-REFUSED]\n    fn read_frame_until(&mut self, deadline: Option<Instant>) -> io::Result<Envelope> {\n        match &mut self.conn {\n            // Absent stays absent: no fabricated default bound on the blocking\n            // read (the absent-is-not-zero shape, releases#201).\n            BrainConn::Whole(stream) if deadline.is_none() => read_frame(stream),\n            BrainConn::Whole(_) => Err(io::Error::new(\n                io::ErrorKind::Unsupported,\n                \"brain IPC read deadline cannot be honored on the blocking (Whole) \\\n                 carrier — it reads the frame inline, so a timeout would abandon a \\\n                 half-read frame and desync the conn. Build the client with \\\n                 `Brain::cold_start_pump` (the Split reader-thread carrier), which \\\n                 bounds every read; or pass no deadline to read unbounded on purpose\",\n            )),\n            BrainConn::Split { frames, .. } => match deadline {\n                Some(dl) => {\n                    // Total-wait bound: each recv is the REMAINING budget to the\n                    // call's entry deadline, so unrelated frames dripping in\n                    // cannot reset it (the per-read-timeout wedge the ruling\n                    // forbids). A non-positive remaining → recv_timeout(ZERO),\n                    // which still drains an already-queued frame before timing out.\n                    let remaining = dl.saturating_duration_since(Instant::now());\n                    match frames.recv_timeout(remaining) {\n                        Ok(framed) => framed,\n                        Err(RecvTimeoutError::Timeout) => Err(io::Error::new(\n                            io::ErrorKind::TimedOut,\n                            \"brain IPC read deadline elapsed\",\n                        )),\n                        Err(RecvTimeoutError::Disconnected) => Err(io::Error::new(\n                            io::ErrorKind::UnexpectedEof,\n                            \"brain IPC reader thread ended (conn closed)\",\n                        )),\n                    }\n                }\n                None => frames.recv().map_err(|_| {\n                    io::Error::new(\n                        io::ErrorKind::UnexpectedEof,\n                        \"brain IPC reader thread ended (conn closed)\",\n                    )\n                })?,\n            },\n        }\n    }\n\n    fn require_session(&self) -> io::Result<u64> {\n…\n    }","startLine":323,"lineNumbers":[323,null,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,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,null,460,null,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,null,2511,null,2520,null,2530,2531,2532,2533,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2544,2545,2546,2547,2548,2549,2550,2551,2552,2553,2554,2555,2556,2557,2558,2559,2560,2561,2562,2563,2564,2565,2566,2567,2568,2569,2570,2571,2572,2573,2574,2575,2576,2577,2578,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2601,2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612,2613,2614,2615,2616,2617,2618,2619,2620,2621,2622,2623,2624,2625,null,2628]},"fileSize":171079,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1789394550045}}
{"type":"message","id":"9a5fe350","parentId":"78ff191e","timestamp":"2026-09-14T14:02:30.066Z","message":{"role":"toolResult","toolCallId":"call_pxiITKlfHnKgIJrm4lHc3U3i|fc_0a63d1e8a5b46c9c016aa7fe75ec9c87d1a7010621bf331c56","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/\n## CONTEXT.md\n*1|# spt-core\n 2|\n 3|**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\n 4|\n...\n 10|\n*11|## Language\n 12|\n 13|**spt-core**:\n 14|The system. Canonical name. The Rust workspace and the umbrella project.\n...\n 35|Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\n*36|- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\n*37|- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\n 38|\n*39|Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\n 40|\n 41|**in-session relay**:\n*42|A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle de...\n 43|\n*44|### Deliverable shape\n 45|\n 46|spt-core ships **both** a library workspace and a canonical binary:\n 47|\n...\n 52|\n*53|## Runtime model\n 54|\n 55|spt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\n 56|\n...\n 109|<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\n*110|A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters ma...\n 111|<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\n 112|An optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\n 113|\n...\n 122|\n*123|### Manifest seams (outbound contract, detailed)\n 124|\n*125|Governing principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e....\n 126|\n*127|**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can s...\n 128|- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\n 129|\n 130|**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTT...\n...\n 175|- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\n*176|- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-...\n 177|- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\n*178|- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\n 179|- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs...\n 180|- **Config knobs** (pulse period, echo-commune window/gate, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override**. **An adapter may DECLARE A DEFAULT, never an override** (narrowed 2026-08-03, LOCKSMITH grill — the original \"never per-adapter\" wording is superseded): a harness has real information about its own turn shape and cost, but the operator keeps the last word. Precedence, highest first: **per-endpoint override → node/global setting → ad...\n 181|- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-...\n 182|\n*183|### Inbound `api` surface (detailed)\n 184|\n 185|All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and tele...\n 186|\n...\n 200|**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running ho...\n*201|_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`). **Read that soft/hard contrast on the RECORD axis only** (ADR-0045): `stop` is *record*-preserving (spool.db + info.json survive) where `purge` wipes them. It says nothing about processes — on the **process** axis `stop` is hard: for a broker-hosted endpoint it reaps the session and its descendant subtr...\n 202|<!-- [doc->REQ-ENDPOINT-PURGE] -->\n 203|\n 204|- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\n*205|- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\n*206|- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\n 207|\n 208|**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike. **This path is the Self's, and spt-core's echo-commune never writes i...\n 209|\n*210|### Startup flows (the two topologies)\n 211|\n 212|**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\n 213|\n...\n 228|\n*229|**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\n\n## docs/adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n*1|# Single consolidated daemon with broker/brain split; peer-propagated gated self-update\n 2|\n*3|## Status\n 4|\n 5|accepted (2026-05-29)\n 6|\n*7|## Context\n 8|\n 9|ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:\n 10|\n...\n 14|\n*15|## Decision\n 16|\n 17|**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.\n 18|\n 19|**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:\n*20|- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.\n*21|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.\n 22|\n*23|Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.\n 24|\n*25|Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).\n 26|\n 27|**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\n 28|\n...\n 30|\n*31|## Consequences\n 32|\n*33|- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n*34|- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35|- Peer-propagated updates make release signing mandatory, not optional.\n 36|- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n 37|- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n*38|- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n 39|\n*40|## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n 41|\n*42|Codex adversarial review (`docs/reviews/STAGE-A-codex-redteam.md`) put 4 FATAL findings on this ADR; Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`, `spt-spikes/spike-01-broker-handoff`) tested the hardest path. Resulting binding amendments:\n 43|\n*44|### A. Update-class taxonomy (resolves FATAL #1 — R-UPD-3 self-contradiction)\n*45|R-UPD-3's \"no endpoint terminates during update\" is **only** absolute for the *brain-only* class. Three classes, each with its own invariant:\n*46|- **brain-only** (the routine case) — endpoints MUST survive untouched. **Spike #1: PROVEN on Windows ConPTY.**\n*47|- **broker-compatible** — broker binary swap behind a versioned IPC the running broker can hot-accept; endpoints survive; no spike yet.\n*48|- **broker-breaking** — held-resource-type or OS-API change; requires a *planned endpoint-cycle*. R-UPD-3 is explicitly weakened here: this class MAY suspend endpoints, with consent + scheduling. PRD R-UPD-3 must be reworded to scope the absolute guarantee to brain-only.\n 49|\n*50|### B. Ownership table (resolves FATAL #4 — \"any live stream owned by brain breaks the invariant\")\n### 0018-broker-brain-process-isolation-restoration.md\n*1|# Restore broker/brain process isolation (correct the in-process-collapse regression)\n 2|\n*3|## Status\n 4|\n*5|Accepted (2026-06-09) — **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n 6|\n*7|## Context\n 8|\n*9|ADR-0004 decided the broker/brain split **as two processes** — a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain — specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.\n 10|\n*11|On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12|\n 13|Consequences of the drift:\n*14|- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n*15|- `spt update apply` performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — a no-op for a binary swap. `update.rs:233-234`'s \"the live daemon execs the verified new binary\" is aspirational and was never wired.\n 16|- New code does not run until an unrelated restart/logon. Observed live: `enlyzeam` ran 0.3.0 with the valid 0.3.2 binary on disk for ~a day, continuing to reproduce the `\\r`-corruption bug the update was meant to fix.\n*17|- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.\n 18|\n*19|## Decision\n 20|\n*21|**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\n 22|\n*23|1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24|2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n*25|3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n*26|4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n*27|5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28|6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n*29|7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n*30|8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31|\n*32|## Consequences\n 33|\n### 0038-broker-stream-lifecycle-truth.md\n*1|# ADR-0038: Broker stream-table lifecycle truth (redispatch eligibility)\n 2|\n 3|- **Status:** accepted (hertz RCA + doyle seam-verify 2026-07-16, dispatched same day); **amended 2026-07-16 (REDISPATCH-STALL — see Amendment below)**\n 4|- **Context:** REDISPATCH-TRUTH milestone; RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n 5|\n*6|## Context\n 7|\n*8|The broker's `NetShared.streams` table is append-only in practice: rows are\n 9|inserted once (nethost.rs ~649) and `StreamLog::finish` only marks EOF —\n 10|no removal or retirement path exists. Stream *classification* is derived at\n 11|redispatch time by peeking ring seq 0, but the `StreamLog` data ring is\n...\n 15|\n*16|After any brain cycle (`spt daemon refresh`, `spt update` apply — same\n 17|path), the fresh `dispatch::run_dispatch_loop` therefore replays EVERY\n*18|broker-held peer stream, including finished historical attach streams. A\n 19|replayed historical Attach silently steals the live controller\n 20|(same-identity `become_controller`, no `Displaced`) and its replayed EOF\n 21|clears it — the operator sees a frozen remote PTY (field: 4/5 endpoints,\n...\n 25|\n*26|## Decision\n 27|<!-- [doc->REQ-REDISPATCH-FINISHED-RETIRE] -->\n 28|\n 29|Stream rows carry explicit **lifecycle truth**, and redispatch consumes it:\n...\n 41|2. **Classification identity is restart-durable and ring-independent.**\n*42|   Each broker-held inbound stream pins an immutable bounded opener/\n 43|   classification fact (the complete first NDJSON record or its derived\n 44|   family+cursor facts) OUTSIDE the evictable data ring, held until stream\n 45|   close. Recovery classifies from that metadata, never from ring seq 0.\n...\n 50|4. **Live reconstruction semantics are unchanged.** The legitimate\n*51|   same-operator successor re-take after a brain restart (controller\n 52|   identity keyed on `by`) still silently re-takes; the discriminator for\n 53|   replay suppression is stream lifecycle, never origin identity.\n 54|\n*55|## Consequences\n 56|\n*57|- Brain cycles (refresh/update) stop being a controller-integrity hazard:\n 58|  historical streams can neither take nor clear a live controller, and\n 59|  active streams survive opener eviction — verified by production-path\n 60|  regressions (the prior e2e manually re-served a stream and bypassed\n...\n 64|  pinned opener record per stream, ring untouched).\n*65|- Rejected alternatives: clearing the whole stream table on brain restart\n 66|  (destroys live streams' reconstruction facts along with the stale ones);\n 67|  enlarging the data ring to preserve openers (unbounded memory for a\n 68|  correctness fact); filtering replay by origin identity (breaks the\n...\n 72|\n*73|## Amendment — REDISPATCH-STALL (2026-07-16)\n 74|<!-- [doc->REQ-DISPATCH-FALLBACK-CIRCUIT] -->\n 75|<!-- [doc->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] -->\n 76|<!-- [doc->REQ-SERVE-OWNERSHIP-GENERATION] -->\n...\n 87|ring-peek fallback, whose guard arm is a broad `Err(_)`\n*88|(`dispatch.rs:414` — intended old-broker-only, catches transport\n 89|timeout/EOF/poison too), each pass installing a throwaway peek subscriber\n 90|whose `StreamLog::attach` replays the whole retained ring UNDER the per-stream\n 91|mutex with discarded write errors — one wedged subscriber conn = serial 15s\n...\n 97|1. **(amends Decision 2)** The ring-peek fallback fires ONLY on the explicit\n*98|   unsupported-verb/old-broker answer. Every other opener-query error\n 99|   (transport timeout, EOF, poison) is `Failed` → the bounded claim requeue —\n 100|   never a second replay subscriber.\n 101|2. **(new — subscriber write discipline)** Replay and live fan-out stop at\n...\n 108|3. **(new — bounded forwarding)** Attach worker completion and forwarding are\n*109|   BOUNDED and cancelable: a poisoned brain subscriber cancels its paired\n 110|   serve/wire worker (no orphan forwarding legs). Coordinate at build with\n 111|   the DAEMON-LIFECYCLE C2 stream-lease work — same neighborhood, build once.\n 112|4. **(amends Decision 3)** Retries are CLASSIFIED: a pre-setup transient may\n*113|   retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff), and\n 114|   a replacement subscriber is never installed until the prior subscriber is\n 115|   fully gone. This is an upgrade of the bounded backoff, NOT a revert to the\n 116|   v0.33 burn-the-claim abandonment (explicitly rejected).\n...\n 129|Rejected here: reverting retryable claims (restores permanent abandonment);\n*130|widening any write deadline (hides the composition instead of removing it);\n 131|tearing down all subscribers on any poison (punishes healthy streams for a\n 132|wedged conn).\n### 0009-endpoint-access-control-model.md\n*1|# Endpoint access-control model (visibility / whitelist / grants)\n 2|\n*3|## Status\n 4|\n 5|accepted (2026-06-02)\n 6|\n...\n 41|\n*42|## Context\n 43|\n 44|Three access/discovery concepts accreted around the multi-subnet model and risked overlapping into a muddle: **endpoint visibility** (ADR-0006 — is an endpoint routable in a subnet at all?), a new **per-endpoint access whitelist** (who may *remotely reach* an endpoint), and the existing **consent grant store** (may *this agent* do *this high-risk thing*?). A new **resource advertisement** (free-text service blurb) also needed to know which access layer gates it. Folding the whitelist into the grant sto...\n 45|\n*46|## Decision\n 47|\n 48|Model access as **three orthogonal, nested gates**, evaluated in order:\n 49|\n...\n 55|\n*56|### Access whitelist semantics\n 57|\n 58|- **Origin-node gate.** It checks the **node the inbound interaction originates from** (where the operator is acting), **not** the sender endpoint's identity or home — *\"the operator must be on a whitelisted machine; the sender endpoint's location is irrelevant.\"* — **superseded in part, see the 2026-07-29 W2b amendment above: a daemon-stamped proven sender endpoint IS now a subject (MSG family), ahead of this node check and abstaining when absent.**\n 59|- **Stateful-firewall model**, not a blanket inbound block:\n...\n 67|\n*68|## Consequences\n 69|\n 70|- New v1 requirements registered: `REQ-SEC-1` (access whitelist, M4 node-tier), `REQ-INST-14` (resource advertisement, M4), `REQ-PAIR-7` (subnet icon, M4 data / frontend render) — all `required_stages = []` until M4.\n 71|- The routing/messaging path (M4) gains a whitelist check on inbound: classify reply-vs-new (via `__REPLY_TO__` correlation) before applying the origin-node gate.\n### 0044-controller-lease-truth.md\n*1|# ADR-0044: Controller lease truth — unique lease identity, loud takeover, broker input fencing\n 2|\n 3|- Status: accepted (doyle triage 2026-07-18, RC-RENDER-TRUTH milestone, W2)\n*4|- Source: hertz same-machine `--take` split-brain RCA (2026-07-16 field repro, filed\n 5|  2026-07-18; doyle seam-verified same day: `resolve_subscribe` same-identity branch,\n 6|  `dispatch_input` unfenced). KNOWN-HAZARDS 7.48. Kin: ADR-0038 Amendment fix-6\n 7|  (ownership/generation validation — the generation half of this contract), ADR-0040\n...\n 9|\n*10|## Context\n 11|\n 12|Field reproduction: endpoint controlled in terminal A; terminal B on the SAME machine ran\n 13|`spt rc <id> --take`. A stayed attached and kept sending input — but stopped receiving\n*14|output. Confirmed source-level split-brain:\n 15|\n*16|1. Local rc attaches over broker loopback, whose origin is the NODE identity — two\n 17|   windows on one node are indistinguishable at the current `by` granularity.\n 18|2. `OutputLog::resolve_subscribe` computes `same_identity = controller_by() == by` and\n 19|   takes the silent same-identity successor path even for a distinct `--take` — intent is\n...\n 28|are missing lease identity, missing input fencing, and the same-node silent-retake\n*29|policy. `brain.rs` already documents the general `None==None` shape of this defect.\n 30|\n*31|## Invariant\n 32|\n 33|At most ONE input-capable controller lease exists per PTY session. Replacing that lease\n 34|atomically revokes the old lease, delivers/closes its attach stream, and rejects every\n...\n 37|\n*38|## Decisions\n 39|\n 40|<!-- [doc->REQ-CONTROLLER-LEASE-IDENTITY] -->\n 41|1. **Distinct viewport/lease identity.** Each rc invocation/attach stream mints a unique\n...\n 83|   <!-- [doc->REQ-INPUT-CONTROLLER-FENCE] -->\n*84|3. **Broker-enforced input fencing (required defense, not optional hardening) — scoped\n 85|   to RC-origin input.** RC-origin Input/Resize are bound to the active controller lease\n*86|   (or originating broker connection); commands from a displaced/stale lease are\n 87|   rejected/dropped after replacement. This is what makes the one-controller invariant\n 88|   TRUE even when a displacement notification is delayed or lost. The fence does NOT\n 89|   globally gate generic `KIND_INPUT`: shell/system injection legitimately sends\n...\n 98|\n*99|## Consequences\n 100|\n 101|- Two same-machine windows behave exactly like two operators: `--take` displaces loudly,\n 102|  the loser's rc exits through its existing `PumpEnd::Displaced` path (already\n*103|  implemented client-side — the broker simply never emitted the event on this path).\n 104|- A displaced viewport can never keep typing into the PTY, regardless of notification\n 105|  timing.\n 106|- Genuine dispatcher recovery (same lease, equal/newer generation) remains silent — the\n...\n 109|<!-- [doc->REQ-DRIVEN-BY-OWN-NODE-NORMALIZE] -->\n*110|### v0.38.1 consequence note — `driven_by` own-node truth (ruling v3)\n 111|\n 112|W1's canonical-wire-id work routes a same-node rc attach through `serve_attach`, so the\n 113|controller's `by` legitimately carries the daemon's OWN node hex. Per CONTEXT.md:386,\n*114|`driven_by` is the broker-single-writer \"controller node\" datum — own node INCLUDED; the\n 115|old stamp comment's remote-only claim was an artifact of the legacy local path (`by =\n 116|None`), not the model. The own-hex latch is therefore TRUTHFUL and stays. Its client-side\n 117|consequence is also the documented design: plain `Control` on a CONTROLLED endpoint is\n...\n 122|where it belongs: the recovery seams (the rc reconnect re-drive and dispatcher re-serve\n*123|enter at the broker, below the client gate), which is what keeps post-restart recovery\n 124|order-independent without ever letting an ordinary second `spt rc` steal control.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/297-enforcement-shape/CONTEXT.md, .worktrees/297-enforcement-shape/docs/adr/*brain*, .worktrees/297-enforcement-shape/docs/adr/*broker*, .worktrees/297-enforcement-shape/docs/adr/*process*, .worktrees/297-enforcement-shape/docs/adr/*control*","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":98,"fileCount":6,"files":[".worktrees/297-enforcement-shape/CONTEXT.md",".worktrees/297-enforcement-shape/docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md",".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md",".worktrees/297-enforcement-shape/docs/adr/0038-broker-stream-lifecycle-truth.md",".worktrees/297-enforcement-shape/docs/adr/0009-endpoint-access-control-model.md",".worktrees/297-enforcement-shape/docs/adr/0044-controller-lease-truth.md"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/CONTEXT.md","count":20},{"path":".worktrees/297-enforcement-shape/docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":20},{"path":".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md","count":20},{"path":".worktrees/297-enforcement-shape/docs/adr/0038-broker-stream-lifecycle-truth.md","count":17},{"path":".worktrees/297-enforcement-shape/docs/adr/0009-endpoint-access-control-model.md","count":6},{"path":".worktrees/297-enforcement-shape/docs/adr/0044-controller-lease-truth.md","count":15}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/297-enforcement-shape/\n## CONTEXT.md\n  *1│# spt-core\n   2│\n   3│**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.\n   4│\n    │...\n  10│\n *11│## Language\n  12│\n  13│**spt-core**:\n  14│The system. Canonical name. The Rust workspace and the umbrella project.\n    │...\n  35│Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\n *36│- **broker** (stable \"kernel\") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.\n *37│- **daemon brain** (\"userspace\") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.\n  38│\n *39│Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable. There is exactly **one broker per machine** (per `SPT_HOME`) — *not* one per endpoint: a single broker holds every hosted endpoint's resources, and it is present whenever the daemon runs, even with zero endpoints online (the bare-daemon case). It is therefore the always-present per-machine layer, which is why the single-daemon lock + liveness anchor belong to it.\n  40│\n  41│**in-session relay**:\n *42│A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle de...\n  43│\n *44│### Deliverable shape\n  45│\n  46│spt-core ships **both** a library workspace and a canonical binary:\n  47│\n    │...\n  52│\n *53│## Runtime model\n  54│\n  55│spt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.\n  56│\n    │...\n 109│<!-- [doc->REQ-ADAPTER-GH-TRANSPORT] -->\n*110│A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters ma...\n 111│<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->\n 112│An optional **`[update].message`** (avenue-agnostic) is a plain multi-line operator notice surfaced to stdout, markdown-rendered (the helpfmt prose path), **only when an update is actually applied** (the version changed) — never on a no-op. It is read from the newly-installed manifest with no `{key}` substitution; its use is to announce a post-update action (e.g. \"run `/reload-plugins` in any ongoing sessions\").\n 113│\n    │...\n 122│\n*123│### Manifest seams (outbound contract, detailed)\n 124│\n*125│Governing principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e....\n 126│\n*127│**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can s...\n 128│- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.\n 129│\n 130│**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTT...\n    │...\n 175│- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.\n*176│- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-...\n 177│- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.\n*178│- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.\n 179│- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs...\n 180│- **Config knobs** (pulse period, echo-commune window/gate, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override**. **An adapter may DECLARE A DEFAULT, never an override** (narrowed 2026-08-03, LOCKSMITH grill — the original \"never per-adapter\" wording is superseded): a harness has real information about its own turn shape and cost, but the operator keeps the last word. Precedence, highest first: **per-endpoint override → node/global setting → ad...\n 181│- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable. <!-- [doc->REQ-RESUME-CONTEXT-PULL] --> This includes the **checkpoint sentinel `!!checkpoint!!`** — the agent-checkpoint trigger an adapter embeds in a commune/signoff drop body (one bare token = checkpoint with default wake; a `!!checkpoint!! <text> !!checkpoint!!` pair makes the inter-marker text a custom wake directive). It is spt-core control metadata: spt-...\n 182│\n*183│### Inbound `api` surface (detailed)\n 184│\n 185│All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and tele...\n 186│\n    │...\n 200│**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running ho...\n*201│_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`). **Read that soft/hard contrast on the RECORD axis only** (ADR-0045): `stop` is *record*-preserving (spool.db + info.json survive) where `purge` wipes them. It says nothing about processes — on the **process** axis `stop` is hard: for a broker-hosted endpoint it reaps the session and its descendant subtr...\n 202│<!-- [doc->REQ-ENDPOINT-PURGE] -->\n 203│\n 204│- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\n*205│- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\n*206│- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\n 207│\n 208│**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike. **This path is the Self's, and spt-core's echo-commune never writes i...\n 209│\n*210│### Startup flows (the two topologies)\n 211│\n 212│**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.\n 213│\n    │...\n 228│\n*229│**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**\n\n## docs/adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n *1│# Single consolidated daemon with broker/brain split; peer-propagated gated self-update\n  2│\n *3│## Status\n  4│\n  5│accepted (2026-05-29)\n  6│\n *7│## Context\n  8│\n  9│ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:\n 10│\n   │...\n 14│\n*15│## Decision\n 16│\n 17│**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.\n 18│\n 19│**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:\n*20│- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.\n*21│- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.\n 22│\n*23│Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.\n 24│\n*25│Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).\n 26│\n 27│**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.\n 28│\n   │...\n 30│\n*31│## Consequences\n 32│\n*33│- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n*34│- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35│- Peer-propagated updates make release signing mandatory, not optional.\n 36│- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n 37│- The \"deliver to most-recently-active session\" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.\n*38│- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.\n 39│\n*40│## Validation & amendments (2026-05-31 — Stage A red-team + Spike #1)\n 41│\n*42│Codex adversarial review (`docs/reviews/STAGE-A-codex-redteam.md`) put 4 FATAL findings on this ADR; Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`, `spt-spikes/spike-01-broker-handoff`) tested the hardest path. Resulting binding amendments:\n 43│\n*44│### A. Update-class taxonomy (resolves FATAL #1 — R-UPD-3 self-contradiction)\n*45│R-UPD-3's \"no endpoint terminates during update\" is **only** absolute for the *brain-only* class. Three classes, each with its own invariant:\n*46│- **brain-only** (the routine case) — endpoints MUST survive untouched. **Spike #1: PROVEN on Windows ConPTY.**\n*47│- **broker-compatible** — broker binary swap behind a versioned IPC the running broker can hot-accept; endpoints survive; no spike yet.\n*48│- **broker-breaking** — held-resource-type or OS-API change; requires a *planned endpoint-cycle*. R-UPD-3 is explicitly weakened here: this class MAY suspend endpoints, with consent + scheduling. PRD R-UPD-3 must be reworded to scope the absolute guarantee to brain-only.\n 49│\n*50│### B. Ownership table (resolves FATAL #4 — \"any live stream owned by brain breaks the invariant\")\n### 0018-broker-brain-process-isolation-restoration.md\n *1│# Restore broker/brain process isolation (correct the in-process-collapse regression)\n  2│\n *3│## Status\n  4│\n *5│Accepted (2026-06-09) — **extends and amends ADR-0004.** The design was independently verified (agent `doyle`, verified-with-amendments); implementation is a dedicated future milestone, sequenced next (before `spt-claude-code`). Full rationale, the `file:line` audit, and the per-decision alternatives live in `docs/BROKER-BRAIN-SPLIT-RESTORATION.md`.\n  6│\n *7│## Context\n  8│\n *9│ADR-0004 decided the broker/brain split **as two processes** — a stable broker kernel (PTY masters, harness children, sockets, the Iroh/QUIC endpoint) beneath a freely-restartable brain — specifically to satisfy the no-endpoint-drop self-update invariant (REQ-UPD-3): a routine (brain-only) update restarts the brain while the broker process survives, so no hosted endpoint terminates. The spikes (01/03/04/05/06) proved this with two separate binaries.\n 10│\n*11│On 2026-06-09, while verifying the v0.3.2 cross-OS update fix on the live fleet, a regression was discovered: **the production daemon runs the broker as a background thread inside the single `spt daemon` process** (`daemon.rs:165-170`: `Broker::bind_in_with_net` → `Arc<Broker>` served on `thread::spawn`; no broker child-spawn exists anywhere in the repo). This is **unintended drift** — no doc, plan, commit, or comment marks the collapse as a deliberate interim (the operator confirmed no such decision...\n 12│\n 13│Consequences of the drift:\n*14│- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n*15│- `spt update apply` performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — a no-op for a binary swap. `update.rs:233-234`'s \"the live daemon execs the verified new binary\" is aspirational and was never wired.\n 16│- New code does not run until an unrelated restart/logon. Observed live: `enlyzeam` ran 0.3.0 with the valid 0.3.2 binary on disk for ~a day, continuing to reproduce the `\\r`-corruption bug the update was meant to fix.\n*17│- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.\n 18│\n*19│## Decision\n 20│\n*21│**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\n 22│\n*23│1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n*24│2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-sp...\n*25│3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`...\n*26│4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update...\n*27│5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n*28│6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable ...\n*29│7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n*30│8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n 31│\n*32│## Consequences\n 33│\n### 0038-broker-stream-lifecycle-truth.md\n  *1│# ADR-0038: Broker stream-table lifecycle truth (redispatch eligibility)\n   2│\n   3│- **Status:** accepted (hertz RCA + doyle seam-verify 2026-07-16, dispatched same day); **amended 2026-07-16 (REDISPATCH-STALL — see Amendment below)**\n   4│- **Context:** REDISPATCH-TRUTH milestone; RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n   5│\n  *6│## Context\n   7│\n  *8│The broker's `NetShared.streams` table is append-only in practice: rows are\n   9│inserted once (nethost.rs ~649) and `StreamLog::finish` only marks EOF —\n  10│no removal or retirement path exists. Stream *classification* is derived at\n  11│redispatch time by peeking ring seq 0, but the `StreamLog` data ring is\n    │...\n  15│\n *16│After any brain cycle (`spt daemon refresh`, `spt update` apply — same\n  17│path), the fresh `dispatch::run_dispatch_loop` therefore replays EVERY\n *18│broker-held peer stream, including finished historical attach streams. A\n  19│replayed historical Attach silently steals the live controller\n  20│(same-identity `become_controller`, no `Displaced`) and its replayed EOF\n  21│clears it — the operator sees a frozen remote PTY (field: 4/5 endpoints,\n    │...\n  25│\n *26│## Decision\n  27│<!-- [doc->REQ-REDISPATCH-FINISHED-RETIRE] -->\n  28│\n  29│Stream rows carry explicit **lifecycle truth**, and redispatch consumes it:\n    │...\n  41│2. **Classification identity is restart-durable and ring-independent.**\n *42│   Each broker-held inbound stream pins an immutable bounded opener/\n  43│   classification fact (the complete first NDJSON record or its derived\n  44│   family+cursor facts) OUTSIDE the evictable data ring, held until stream\n  45│   close. Recovery classifies from that metadata, never from ring seq 0.\n    │...\n  50│4. **Live reconstruction semantics are unchanged.** The legitimate\n *51│   same-operator successor re-take after a brain restart (controller\n  52│   identity keyed on `by`) still silently re-takes; the discriminator for\n  53│   replay suppression is stream lifecycle, never origin identity.\n  54│\n *55│## Consequences\n  56│\n *57│- Brain cycles (refresh/update) stop being a controller-integrity hazard:\n  58│  historical streams can neither take nor clear a live controller, and\n  59│  active streams survive opener eviction — verified by production-path\n  60│  regressions (the prior e2e manually re-served a stream and bypassed\n    │...\n  64│  pinned opener record per stream, ring untouched).\n *65│- Rejected alternatives: clearing the whole stream table on brain restart\n  66│  (destroys live streams' reconstruction facts along with the stale ones);\n  67│  enlarging the data ring to preserve openers (unbounded memory for a\n  68│  correctness fact); filtering replay by origin identity (breaks the\n    │...\n  72│\n *73│## Amendment — REDISPATCH-STALL (2026-07-16)\n  74│<!-- [doc->REQ-DISPATCH-FALLBACK-CIRCUIT] -->\n  75│<!-- [doc->REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE] -->\n  76│<!-- [doc->REQ-SERVE-OWNERSHIP-GENERATION] -->\n    │...\n  87│ring-peek fallback, whose guard arm is a broad `Err(_)`\n *88│(`dispatch.rs:414` — intended old-broker-only, catches transport\n  89│timeout/EOF/poison too), each pass installing a throwaway peek subscriber\n  90│whose `StreamLog::attach` replays the whole retained ring UNDER the per-stream\n  91│mutex with discarded write errors — one wedged subscriber conn = serial 15s\n    │...\n  97│1. **(amends Decision 2)** The ring-peek fallback fires ONLY on the explicit\n *98│   unsupported-verb/old-broker answer. Every other opener-query error\n  99│   (transport timeout, EOF, poison) is `Failed` → the bounded claim requeue —\n 100│   never a second replay subscriber.\n 101│2. **(new — subscriber write discipline)** Replay and live fan-out stop at\n    │...\n 108│3. **(new — bounded forwarding)** Attach worker completion and forwarding are\n*109│   BOUNDED and cancelable: a poisoned brain subscriber cancels its paired\n 110│   serve/wire worker (no orphan forwarding legs). Coordinate at build with\n 111│   the DAEMON-LIFECYCLE C2 stream-lease work — same neighborhood, build once.\n 112│4. **(amends Decision 3)** Retries are CLASSIFIED: a pre-setup transient may\n*113│   retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff), and\n 114│   a replacement subscriber is never installed until the prior subscriber is\n 115│   fully gone. This is an upgrade of the bounded backoff, NOT a revert to the\n 116│   v0.33 burn-the-claim abandonment (explicitly rejected).\n    │...\n 129│Rejected here: reverting retryable claims (restores permanent abandonment);\n*130│widening any write deadline (hides the composition instead of removing it);\n 131│tearing down all subscribers on any poison (punishes healthy streams for a\n 132│wedged conn).\n### 0009-endpoint-access-control-model.md\n *1│# Endpoint access-control model (visibility / whitelist / grants)\n  2│\n *3│## Status\n  4│\n  5│accepted (2026-06-02)\n  6│\n   │...\n 41│\n*42│## Context\n 43│\n 44│Three access/discovery concepts accreted around the multi-subnet model and risked overlapping into a muddle: **endpoint visibility** (ADR-0006 — is an endpoint routable in a subnet at all?), a new **per-endpoint access whitelist** (who may *remotely reach* an endpoint), and the existing **consent grant store** (may *this agent* do *this high-risk thing*?). A new **resource advertisement** (free-text service blurb) also needed to know which access layer gates it. Folding the whitelist into the grant sto...\n 45│\n*46│## Decision\n 47│\n 48│Model access as **three orthogonal, nested gates**, evaluated in order:\n 49│\n   │...\n 55│\n*56│### Access whitelist semantics\n 57│\n 58│- **Origin-node gate.** It checks the **node the inbound interaction originates from** (where the operator is acting), **not** the sender endpoint's identity or home — *\"the operator must be on a whitelisted machine; the sender endpoint's location is irrelevant.\"* — **superseded in part, see the 2026-07-29 W2b amendment above: a daemon-stamped proven sender endpoint IS now a subject (MSG family), ahead of this node check and abstaining when absent.**\n 59│- **Stateful-firewall model**, not a blanket inbound block:\n   │...\n 67│\n*68│## Consequences\n 69│\n 70│- New v1 requirements registered: `REQ-SEC-1` (access whitelist, M4 node-tier), `REQ-INST-14` (resource advertisement, M4), `REQ-PAIR-7` (subnet icon, M4 data / frontend render) — all `required_stages = []` until M4.\n 71│- The routing/messaging path (M4) gains a whitelist check on inbound: classify reply-vs-new (via `__REPLY_TO__` correlation) before applying the origin-node gate.\n### 0044-controller-lease-truth.md\n  *1│# ADR-0044: Controller lease truth — unique lease identity, loud takeover, broker input fencing\n   2│\n   3│- Status: accepted (doyle triage 2026-07-18, RC-RENDER-TRUTH milestone, W2)\n  *4│- Source: hertz same-machine `--take` split-brain RCA (2026-07-16 field repro, filed\n   5│  2026-07-18; doyle seam-verified same day: `resolve_subscribe` same-identity branch,\n   6│  `dispatch_input` unfenced). KNOWN-HAZARDS 7.48. Kin: ADR-0038 Amendment fix-6\n   7│  (ownership/generation validation — the generation half of this contract), ADR-0040\n    │...\n   9│\n *10│## Context\n  11│\n  12│Field reproduction: endpoint controlled in terminal A; terminal B on the SAME machine ran\n  13│`spt rc <id> --take`. A stayed attached and kept sending input — but stopped receiving\n *14│output. Confirmed source-level split-brain:\n  15│\n *16│1. Local rc attaches over broker loopback, whose origin is the NODE identity — two\n  17│   windows on one node are indistinguishable at the current `by` granularity.\n  18│2. `OutputLog::resolve_subscribe` computes `same_identity = controller_by() == by` and\n  19│   takes the silent same-identity successor path even for a distinct `--take` — intent is\n    │...\n  28│are missing lease identity, missing input fencing, and the same-node silent-retake\n *29│policy. `brain.rs` already documents the general `None==None` shape of this defect.\n  30│\n *31│## Invariant\n  32│\n  33│At most ONE input-capable controller lease exists per PTY session. Replacing that lease\n  34│atomically revokes the old lease, delivers/closes its attach stream, and rejects every\n    │...\n  37│\n *38│## Decisions\n  39│\n  40│<!-- [doc->REQ-CONTROLLER-LEASE-IDENTITY] -->\n  41│1. **Distinct viewport/lease identity.** Each rc invocation/attach stream mints a unique\n    │...\n  83│   <!-- [doc->REQ-INPUT-CONTROLLER-FENCE] -->\n *84│3. **Broker-enforced input fencing (required defense, not optional hardening) — scoped\n  85│   to RC-origin input.** RC-origin Input/Resize are bound to the active controller lease\n *86│   (or originating broker connection); commands from a displaced/stale lease are\n  87│   rejected/dropped after replacement. This is what makes the one-controller invariant\n  88│   TRUE even when a displacement notification is delayed or lost. The fence does NOT\n  89│   globally gate generic `KIND_INPUT`: shell/system injection legitimately sends\n    │...\n  98│\n *99│## Consequences\n 100│\n 101│- Two same-machine windows behave exactly like two operators: `--take` displaces loudly,\n 102│  the loser's rc exits through its existing `PumpEnd::Displaced` path (already\n*103│  implemented client-side — the broker simply never emitted the event on this path).\n 104│- A displaced viewport can never keep typing into the PTY, regardless of notification\n 105│  timing.\n 106│- Genuine dispatcher recovery (same lease, equal/newer generation) remains silent — the\n    │...\n 109│<!-- [doc->REQ-DRIVEN-BY-OWN-NODE-NORMALIZE] -->\n*110│### v0.38.1 consequence note — `driven_by` own-node truth (ruling v3)\n 111│\n 112│W1's canonical-wire-id work routes a same-node rc attach through `serve_attach`, so the\n 113│controller's `by` legitimately carries the daemon's OWN node hex. Per CONTEXT.md:386,\n*114│`driven_by` is the broker-single-writer \"controller node\" datum — own node INCLUDED; the\n 115│old stamp comment's remote-only claim was an artifact of the legacy local path (`by =\n 116│None`), not the model. The own-hex latch is therefore TRUTHFUL and stays. Its client-side\n 117│consequence is also the documented design: plain `Control` on a CONTROLLED endpoint is\n    │...\n 122│where it belongs: the recovery seams (the rc reconnect re-drive and dispatcher re-serve\n*123│enter at the broker, below the client gate), which is what keeps post-restart recovery\n 124│order-independent without ever letting an ordinary second `spt rc` steal control.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789394550066}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PA0HgD2NSF1UJfNDHZ1ERWF4|fc_0a63d1e8a5b46c9c016aa7fe7e1a9887d1bd5fdb0f6a6196c1","toolName":"read","startedAt":"2026-09-14T14:02:38.320Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:8660-8920"},"intent":"Reading spawn terminal path"},"id":"8c0a6707","parentId":"9a5fe350","timestamp":"2026-09-14T14:02:38.320Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FPKdpqK1xH7yVYR05Xowduz4|fc_0a63d1e8a5b46c9c016aa7fe7e1aa887d1a442357e9961b5bb","toolName":"read","startedAt":"2026-09-14T14:02:38.320Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:8030-8280"},"intent":"Reading spawn admission path"},"id":"2f19c455","parentId":"8c0a6707","timestamp":"2026-09-14T14:02:38.320Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nXDZzDHzWYnXoP8XSWP6zbeX|fc_0a63d1e8a5b46c9c016aa7fe7e1ab487d1994a79c56073789d","toolName":"grep","startedAt":"2026-09-14T14:02:38.320Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"},"intent":"Locating brain deadline construction"},"id":"526788f9","parentId":"2f19c455","timestamp":"2026-09-14T14:02:38.320Z"}
{"type":"message","id":"1d20e35f","parentId":"526788f9","timestamp":"2026-09-14T14:02:38.320Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_PA0HgD2NSF1UJfNDHZ1ERWF4|fc_0a63d1e8a5b46c9c016aa7fe7e1a9887d1bd5fdb0f6a6196c1","name":"read","arguments":{"i":"Reading spawn terminal path","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:8660-8920"},"intent":"Reading spawn terminal path"},{"type":"toolCall","id":"call_FPKdpqK1xH7yVYR05Xowduz4|fc_0a63d1e8a5b46c9c016aa7fe7e1aa887d1a442357e9961b5bb","name":"read","arguments":{"i":"Reading spawn admission path","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:8030-8280"},"intent":"Reading spawn admission path"},{"type":"toolCall","id":"call_nXDZzDHzWYnXoP8XSWP6zbeX|fc_0a63d1e8a5b46c9c016aa7fe7e1ab487d1994a79c56073789d","name":"grep","arguments":{"i":"Locating brain deadline construction","pattern":"fn call_deadline|fn cold_start_pump|KIND_SPAWNED|fn dispatch_spawn|fn send_error|fn send_spawned","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","case":true,"gitignore":false,"skip":null},"intent":"Locating brain deadline construction"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":12812,"output":213,"cacheRead":20864,"cacheWrite":0,"totalTokens":33889,"cost":{"input":0.12812,"output":0.01065,"cacheRead":0.020864,"cacheWrite":0,"total":0.159634}},"stopReason":"toolUse","timestamp":1789394550093,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe76547087d1a843336ac5d8f745","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe7e1a9887d1bd5fdb0f6a6196c1","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading spawn terminal path\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:8660-8920\"}","call_id":"call_PA0HgD2NSF1UJfNDHZ1ERWF4","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe7e1aa887d1a442357e9961b5bb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading spawn admission path\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:8030-8280\"}","call_id":"call_FPKdpqK1xH7yVYR05Xowduz4","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe7e1ab487d1994a79c56073789d","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating brain deadline construction\",\"pattern\":\"fn call_deadline|fn cold_start_pump|KIND_SPAWNED|fn dispatch_spawn|fn send_error|fn send_spawned\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_nXDZzDHzWYnXoP8XSWP6zbeX","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":8225.488700002432,"ttft":8040.110600009561,"completedAt":1789394558319,"contextSnapshot":{"promptTokens":33676,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"cb6720a1","parentId":"1d20e35f","timestamp":"2026-09-14T14:02:38.387Z","message":{"role":"toolResult","toolCallId":"call_FPKdpqK1xH7yVYR05Xowduz4|fc_0a63d1e8a5b46c9c016aa7fe7e1aa887d1a442357e9961b5bb","toolName":"read","content":[{"type":"text","text":"    fn redeem_bringup_admit_at(\n…\n    ) -> AdmitTicket {\n…\n        match claimed {\n…\n            _ => AdmitTicket::Unredeemed,\n        }\n    }\n\n    /// Record that this broker has STARTED a bring-up for `endpoint`, good for\n    /// `ttl` — the bring-up's own ready-wait (see [`Self::bringups_in_flight`]).\n    ///\n    /// Written before the launch fires, so the fact exists for the whole window\n    /// in which the spawned harness may ask about it. Expired entries are swept\n    /// on every write, the way [`Self::mint_bringup_admit_at`] sweeps tickets, so\n    /// the map holds only what is still live and a broker that never sees another\n    /// bring-up does not accumulate.\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\n    fn note_bringup_in_flight(&self, endpoint: &str, ttl: Duration) {\n        self.note_bringup_in_flight_at(endpoint, ttl, Instant::now());\n    }\n\n    /// [`note_bringup_in_flight`](Self::note_bringup_in_flight) with the clock\n    /// passed in — the expiry seam, for the same reason\n    /// [`mint_bringup_admit_at`](Self::mint_bringup_admit_at) has one: `Instant`\n    /// cannot be fabricated, so a test that must see an entry EXPIRE has no other\n    /// way across the TTL than to sleep through it, and a sleeping test proves\n    /// the TTL only for the value it slept for.\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\n    fn note_bringup_in_flight_at(&self, endpoint: &str, ttl: Duration, now: Instant) {\n        let mut flight = recover(&self.bringups_in_flight);\n        flight.retain(|_, expires| *expires > now);\n        flight.insert(endpoint.to_string(), now + ttl);\n    }\n\n    /// Drop `endpoint`'s in-flight entry: its session row now exists, so the\n    /// ledger has nothing left to say about it.\n    ///\n    /// A no-op for the endpoints that never had an entry, which is every\n    /// ordinary spawn — only a bring-up writes one.\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\n    fn clear_bringup_in_flight(&self, endpoint: &str) {\n        recover(&self.bringups_in_flight).remove(endpoint);\n    }\n\n    /// The endpoints whose bring-up is in flight RIGHT NOW, expired entries\n    /// excluded.\n    ///\n    /// Reads without pruning: this answers a query, and a query that mutates the\n    /// ledger would make the reply depend on who asked last.\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\n    fn bringups_in_flight_at(&self, now: Instant) -> Vec<String> {\n        recover(&self.bringups_in_flight)\n            .iter()\n            .filter(|(_, expires)| **expires > now)\n            .map(|(endpoint, _)| endpoint.clone())\n            .collect()\n    }\n\n    /// The shared spawn choke behind BOTH policies (`fresh=false` =\n    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\n    fn dispatch_spawn_policy(\n        &self,\n        req: SpawnReq,\n        send: &SharedSend,\n        fresh: bool,\n    ) -> Result<Option<u64>, String> {\n        // releases#199 instrument, BROKER SIDE. The launch-path stamps say when the\n        // client asked; these say what the broker did with the ask. Two spans are\n        // separately suspect and were previously indistinguishable from each other\n        // and from the client's own wait: the single-flight gate below (bounded by the\n        // holder's PROGRESS since #235, plus an unbounded-looking zombie-reap re-evaluation)\n        // and the ConPTY create (candidate C). Stamped in the SHARED choke, not\n        // only on the bring-up path, because the choke is where the time goes.\n        // [impl->REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE]\n        let spawn_t0 = Instant::now();\n        // The label is CLONED, not borrowed from `req`: the request's fields are\n        // moved into the spawn below, and a closure holding `&req` would keep the\n        // whole request borrowed past those moves.\n        let spawn_label = req.endpoint.clone();\n        // #235: the breadcrumb sites are ALSO the progress sites. The gate decides\n        // \"still working\" vs \"wedged\" from the holder's last phase stamp, so every\n        // place that already announces a phase records it into the claim too —\n        // rather than a second set of call sites that can drift from these.\n        // `None` until this thread wins the claim; a stamp before then would be\n        // writing progress into somebody else's claim.\n        let claim_gen: std::cell::Cell<Option<u64>> = std::cell::Cell::new(None);\n        let spawn_phase = |phase: &str, detail: &str| {\n            spt_proto::emit_line_err!(\n                \"SPAWN_PHASE:{spawn_label} phase={phase} t_ms={}{}{detail}\",\n                spawn_t0.elapsed().as_millis(),\n                if detail.is_empty() { \"\" } else { \" \" },\n            );\n            if let Some(generation) = claim_gen.get() {\n                let mut inflight = recover(&self.wake_inflight);\n                if let Some(claim) = inflight.get_mut(&spawn_label) {\n                    // Only OUR generation: a holder taken over as wedged must not\n                    // keep refreshing the successor's claim from behind.\n                    if claim.generation == generation {\n                        claim.phase = phase.to_string();\n                        claim.phase_at = Instant::now();\n                    }\n                }\n            }\n        };\n        spawn_phase(\"received\", &format!(\"fresh={fresh}\"));\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\n        // then stomped info.json, routing injects to a contended record). The claim +\n        // the live-session check are read together under one atomic critical section so\n        // a concurrent spawn either sees our claim or its registered session — never a\n        // gap; NO I/O under the lock (process_id reads a cached pid; the ZOMBIE probe\n        // runs OFF the lock in the AlreadyLive arm). An empty endpoint is an\n        // ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim` holds\n        // the claim to fn end, releasing (RAII) after the session is table-visible\n        // or on any early error.\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\n            None\n        } else {\n            loop {\n                enum Gate<'a> {\n                    Claimed(WakeClaimGuard<'a>),\n                    AlreadyLive {\n                        sid: u64,\n                        spid: Option<u32>,\n                        process_started_at: Option<u64>,\n                        adapter_labeled: bool,\n                        spawned_ms_ago: u64,\n                    },\n                    Racing,\n                }\n                let gate = {\n                    let sessions = recover(&self.sessions);\n                    let mut inflight = recover(&self.wake_inflight);\n                    let live = sessions.iter().find(|(_, h)| h.endpoint == req.endpoint).map(\n                        |(sid, h)| {\n                            (\n                                *sid,\n                                h.session.process_id(),\n                                h.process_started_at,\n                                !h.adapter.is_empty(),\n                                h.spawned_at.elapsed().as_millis() as u64,\n                            )\n                        },\n                    );\n                    // The holder's own phase stamp, read under the SAME lock as the\n                    // claim — an elapsed against a recorded Instant, no I/O, per this\n                    // block's no-I/O-under-the-lock contract.\n                    let held = inflight.get(&req.endpoint).cloned();\n                    let holder_stalled = held\n                        .as_ref()\n                        .is_some_and(|c| c.phase_at.elapsed() >= phase_wedge_ceiling(&c.phase));\n                    match wake_gate_decision(live.is_some(), held.is_some(), holder_stalled) {\n                        WakeGate::AlreadyLive => {\n                            let (sid, spid, process_started_at, adapter_labeled, spawned_ms_ago) =\n                                live.expect(\"live is Some on AlreadyLive\");\n                            Gate::AlreadyLive {\n                                sid, spid, process_started_at, adapter_labeled, spawned_ms_ago,\n                            }\n                        }\n                        WakeGate::Racing => Gate::Racing,\n                        WakeGate::Claim => {\n                            // A takeover is the availability arm firing. LOUD and\n                            // always, naming the phase and the elapsed that decided\n                            // it — a silent takeover is indistinguishable from a\n                            // free endpoint in every log we would read afterwards.\n                            if let Some(prev) = &held {\n                                WEDGE_TAKEOVER_COUNT.fetch_add(1, Ordering::Relaxed);\n                                eprintln!(\n                                    \"SPAWN_WEDGE_TAKEOVER:{}: claim gen {} wedged in phase {} for {}ms \\\n                                     (ceiling {}ms) — taking over \\\n                                     [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\n                                    req.endpoint,\n                                    prev.generation,\n                                    prev.phase,\n                                    prev.phase_at.elapsed().as_millis(),\n                                    phase_wedge_ceiling(&prev.phase).as_millis(),\n                                );\n                            }\n                            let generation =\n                                self.wake_generation.fetch_add(1, Ordering::Relaxed) + 1;\n                            inflight.insert(\n                                req.endpoint.clone(),\n                                WakeClaim {\n                                    generation,\n                                    phase: \"gate_claimed\".to_string(),\n                                    phase_at: Instant::now(),\n                                },\n                            );\n                            claim_gen.set(Some(generation));\n                            Gate::Claimed(WakeClaimGuard {\n                                map: &self.wake_inflight,\n                                endpoint: req.endpoint.clone(),\n                                generation,\n                            })\n                        }\n                    }\n                };\n                match gate {\n                    Gate::Claimed(guard) => {\n                        spawn_phase(\"gate_claimed\", \"\");\n                        break Some(guard);\n                    }\n                    Gate::AlreadyLive {\n                        sid, spid, process_started_at, adapter_labeled, spawned_ms_ago,\n                    } => {\n                        // ONE liveness authority (ADR-0041 decision 6,\n                        // REQ-ENDPOINT-CYCLE-HONEST): before refusing/deduping by\n                        // citing the claimed session, PROBE its client tree — off\n                        // every lock. A zombie (dead root, or an adapter wrapper\n                        // whose harness clients died past boot grace) is reaped\n                        // honestly — identity-checked tree-kill only if the root\n                        // is still ours, then de-table — and the gate re-evaluates:\n                        // the next pass claims and spawns for real. Never a\n                        // refusal citing a corpse.\n                        // [impl->REQ-ENDPOINT-CYCLE-HONEST]\n                        if session_is_zombie(spid, adapter_labeled, spawned_ms_ago) {\n                            // Re-observe immediately before any kill, off-lock.\n                            // The existing owned-process death oracle preserves\n                            // snapshot-less Unix cleanup and Linux zombie demotion.\n                            // [impl->REQ-BROKER-ZOMBIE-IDENTITY]\n                            match reap_zombie_root(\n                                spid,\n                                process_started_at,\n                                |pid| {\n                                    use spt_store::proc::{self, ProcIdentity};\n                                    if proc::provably_gone(pid) {\n                                        ProcIdentity::Absent\n                                    } else {\n                                        proc::process_started_at(pid)\n                                            .map(ProcIdentity::Present)\n                                            .unwrap_or(ProcIdentity::Unproven)\n                                    }\n                                },\n                                spt_store::proc::kill_pid_tree,\n                            ) {\n                                Ok(ZombieReap::Recycled { recorded, observed }) => {\n                                    spt_proto::emit_line_err!(\n                                        \"SPAWN_ZOMBIE_REAP_RECYCLED:{}: session {sid} pid={spid:?} \\\n                                         recorded_birth={recorded} observed_birth={observed} — \\\n                                         remembered root gone; no signal, clearing stale session\",\n                                        req.endpoint\n                                    );\n                                }\n                                Ok(ZombieReap::Absent | ZombieReap::Killed) => {\n                                    spt_proto::emit_line_err!(\n                                        \"SPAWN_ZOMBIE_REAP:{}: claimed session {sid} has a dead client \\\n                                         tree — reaping before honest respawn [REQ-ENDPOINT-CYCLE-HONEST]\",\n                                        req.endpoint\n                                    );\n                                }\n                                Err(reason) => {\n                                    spt_proto::emit_line_err!(\n                                        \"SPAWN_ZOMBIE_REAP_DECLINE:{}: session {sid} pid={spid:?} \\\n                                         reason={reason} — no kill or forced de-table\",\n                                        req.endpoint\n                                    );\n…\n                                }\n                            }\n…\n                        }\n…\n                    }\n…\n                }\n            }\n        };\n…\n    }\n\n[Showing lines 8029-8283 of 16852. Use :8284 to continue]"}],"details":{"truncation":{"content":"            _ => AdmitTicket::Unredeemed,\r\n        }\r\n    }\r\n\r\n    /// Record that this broker has STARTED a bring-up for `endpoint`, good for\r\n    /// `ttl` — the bring-up's own ready-wait (see [`Self::bringups_in_flight`]).\r\n    ///\r\n    /// Written before the launch fires, so the fact exists for the whole window\r\n    /// in which the spawned harness may ask about it. Expired entries are swept\r\n    /// on every write, the way [`Self::mint_bringup_admit_at`] sweeps tickets, so\r\n    /// the map holds only what is still live and a broker that never sees another\r\n    /// bring-up does not accumulate.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn note_bringup_in_flight(&self, endpoint: &str, ttl: Duration) {\r\n        self.note_bringup_in_flight_at(endpoint, ttl, Instant::now());\r\n    }\r\n\r\n    /// [`note_bringup_in_flight`](Self::note_bringup_in_flight) with the clock\r\n    /// passed in — the expiry seam, for the same reason\r\n    /// [`mint_bringup_admit_at`](Self::mint_bringup_admit_at) has one: `Instant`\r\n    /// cannot be fabricated, so a test that must see an entry EXPIRE has no other\r\n    /// way across the TTL than to sleep through it, and a sleeping test proves\r\n    /// the TTL only for the value it slept for.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn note_bringup_in_flight_at(&self, endpoint: &str, ttl: Duration, now: Instant) {\r\n        let mut flight = recover(&self.bringups_in_flight);\r\n        flight.retain(|_, expires| *expires > now);\r\n        flight.insert(endpoint.to_string(), now + ttl);\r\n    }\r\n\r\n    /// Drop `endpoint`'s in-flight entry: its session row now exists, so the\r\n    /// ledger has nothing left to say about it.\r\n    ///\r\n    /// A no-op for the endpoints that never had an entry, which is every\r\n    /// ordinary spawn — only a bring-up writes one.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn clear_bringup_in_flight(&self, endpoint: &str) {\r\n        recover(&self.bringups_in_flight).remove(endpoint);\r\n    }\r\n\r\n    /// The endpoints whose bring-up is in flight RIGHT NOW, expired entries\r\n    /// excluded.\r\n    ///\r\n    /// Reads without pruning: this answers a query, and a query that mutates the\r\n    /// ledger would make the reply depend on who asked last.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn bringups_in_flight_at(&self, now: Instant) -> Vec<String> {\r\n        recover(&self.bringups_in_flight)\r\n            .iter()\r\n            .filter(|(_, expires)| **expires > now)\r\n            .map(|(endpoint, _)| endpoint.clone())\r\n            .collect()\r\n    }\r\n\r\n    /// The shared spawn choke behind BOTH policies (`fresh=false` =\r\n    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\r\n    fn dispatch_spawn_policy(\r\n        &self,\r\n        req: SpawnReq,\r\n        send: &SharedSend,\r\n        fresh: bool,\r\n    ) -> Result<Option<u64>, String> {\r\n        // releases#199 instrument, BROKER SIDE. The launch-path stamps say when the\r\n        // client asked; these say what the broker did with the ask. Two spans are\r\n        // separately suspect and were previously indistinguishable from each other\r\n        // and from the client's own wait: the single-flight gate below (bounded by the\r\n        // holder's PROGRESS since #235, plus an unbounded-looking zombie-reap re-evaluation)\r\n        // and the ConPTY create (candidate C). Stamped in the SHARED choke, not\r\n        // only on the bring-up path, because the choke is where the time goes.\r\n        // [impl->REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE]\r\n        let spawn_t0 = Instant::now();\r\n        // The label is CLONED, not borrowed from `req`: the request's fields are\r\n        // moved into the spawn below, and a closure holding `&req` would keep the\r\n        // whole request borrowed past those moves.\r\n        let spawn_label = req.endpoint.clone();\r\n        // #235: the breadcrumb sites are ALSO the progress sites. The gate decides\r\n        // \"still working\" vs \"wedged\" from the holder's last phase stamp, so every\r\n        // place that already announces a phase records it into the claim too —\r\n        // rather than a second set of call sites that can drift from these.\r\n        // `None` until this thread wins the claim; a stamp before then would be\r\n        // writing progress into somebody else's claim.\r\n        let claim_gen: std::cell::Cell<Option<u64>> = std::cell::Cell::new(None);\r\n        let spawn_phase = |phase: &str, detail: &str| {\r\n            spt_proto::emit_line_err!(\r\n                \"SPAWN_PHASE:{spawn_label} phase={phase} t_ms={}{}{detail}\",\r\n                spawn_t0.elapsed().as_millis(),\r\n                if detail.is_empty() { \"\" } else { \" \" },\r\n            );\r\n            if let Some(generation) = claim_gen.get() {\r\n                let mut inflight = recover(&self.wake_inflight);\r\n                if let Some(claim) = inflight.get_mut(&spawn_label) {\r\n                    // Only OUR generation: a holder taken over as wedged must not\r\n                    // keep refreshing the successor's claim from behind.\r\n                    if claim.generation == generation {\r\n                        claim.phase = phase.to_string();\r\n                        claim.phase_at = Instant::now();\r\n                    }\r\n                }\r\n            }\r\n        };\r\n        spawn_phase(\"received\", &format!(\"fresh={fresh}\"));\r\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\r\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\r\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\r\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\r\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\r\n        // then stomped info.json, routing injects to a contended record). The claim +\r\n        // the live-session check are read together under one atomic critical section so\r\n        // a concurrent spawn either sees our claim or its registered session — never a\r\n        // gap; NO I/O under the lock (process_id reads a cached pid; the ZOMBIE probe\r\n        // runs OFF the lock in the AlreadyLive arm). An empty endpoint is an\r\n        // ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim` holds\r\n        // the claim to fn end, releasing (RAII) after the session is table-visible\r\n        // or on any early error.\r\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\r\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\r\n            None\r\n        } else {\r\n            loop {\r\n                enum Gate<'a> {\r\n                    Claimed(WakeClaimGuard<'a>),\r\n                    AlreadyLive {\r\n                        sid: u64,\r\n                        spid: Option<u32>,\r\n                        process_started_at: Option<u64>,\r\n                        adapter_labeled: bool,\r\n                        spawned_ms_ago: u64,\r\n                    },\r\n                    Racing,\r\n                }\r\n                let gate = {\r\n                    let sessions = recover(&self.sessions);\r\n                    let mut inflight = recover(&self.wake_inflight);\r\n                    let live = sessions.iter().find(|(_, h)| h.endpoint == req.endpoint).map(\r\n                        |(sid, h)| {\r\n                            (\r\n                                *sid,\r\n                                h.session.process_id(),\r\n                                h.process_started_at,\r\n                                !h.adapter.is_empty(),\r\n                                h.spawned_at.elapsed().as_millis() as u64,\r\n                            )\r\n                        },\r\n                    );\r\n                    // The holder's own phase stamp, read under the SAME lock as the\r\n                    // claim — an elapsed against a recorded Instant, no I/O, per this\r\n                    // block's no-I/O-under-the-lock contract.\r\n                    let held = inflight.get(&req.endpoint).cloned();\r\n                    let holder_stalled = held\r\n                        .as_ref()\r\n                        .is_some_and(|c| c.phase_at.elapsed() >= phase_wedge_ceiling(&c.phase));\r\n                    match wake_gate_decision(live.is_some(), held.is_some(), holder_stalled) {\r\n                        WakeGate::AlreadyLive => {\r\n                            let (sid, spid, process_started_at, adapter_labeled, spawned_ms_ago) =\r\n                                live.expect(\"live is Some on AlreadyLive\");\r\n                            Gate::AlreadyLive {\r\n                                sid, spid, process_started_at, adapter_labeled, spawned_ms_ago,\r\n                            }\r\n                        }\r\n                        WakeGate::Racing => Gate::Racing,\r\n                        WakeGate::Claim => {\r\n                            // A takeover is the availability arm firing. LOUD and\r\n                            // always, naming the phase and the elapsed that decided\r\n                            // it — a silent takeover is indistinguishable from a\r\n                            // free endpoint in every log we would read afterwards.\r\n                            if let Some(prev) = &held {\r\n                                WEDGE_TAKEOVER_COUNT.fetch_add(1, Ordering::Relaxed);\r\n                                eprintln!(\r\n                                    \"SPAWN_WEDGE_TAKEOVER:{}: claim gen {} wedged in phase {} for {}ms \\\r\n                                     (ceiling {}ms) — taking over \\\r\n                                     [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\r\n                                    req.endpoint,\r\n                                    prev.generation,\r\n                                    prev.phase,\r\n                                    prev.phase_at.elapsed().as_millis(),\r\n                                    phase_wedge_ceiling(&prev.phase).as_millis(),\r\n                                );\r\n                            }\r\n                            let generation =\r\n                                self.wake_generation.fetch_add(1, Ordering::Relaxed) + 1;\r\n                            inflight.insert(\r\n                                req.endpoint.clone(),\r\n                                WakeClaim {\r\n                                    generation,\r\n                                    phase: \"gate_claimed\".to_string(),\r\n                                    phase_at: Instant::now(),\r\n                                },\r\n                            );\r\n                            claim_gen.set(Some(generation));\r\n                            Gate::Claimed(WakeClaimGuard {\r\n                                map: &self.wake_inflight,\r\n                                endpoint: req.endpoint.clone(),\r\n                                generation,\r\n                            })\r\n                        }\r\n                    }\r\n                };\r\n                match gate {\r\n                    Gate::Claimed(guard) => {\r\n                        spawn_phase(\"gate_claimed\", \"\");\r\n                        break Some(guard);\r\n                    }\r\n                    Gate::AlreadyLive {\r\n                        sid, spid, process_started_at, adapter_labeled, spawned_ms_ago,\r\n                    } => {\r\n                        // ONE liveness authority (ADR-0041 decision 6,\r\n                        // REQ-ENDPOINT-CYCLE-HONEST): before refusing/deduping by\r\n                        // citing the claimed session, PROBE its client tree — off\r\n                        // every lock. A zombie (dead root, or an adapter wrapper\r\n                        // whose harness clients died past boot grace) is reaped\r\n                        // honestly — identity-checked tree-kill only if the root\r\n                        // is still ours, then de-table — and the gate re-evaluates:\r\n                        // the next pass claims and spawns for real. Never a\r\n                        // refusal citing a corpse.\r\n                        // [impl->REQ-ENDPOINT-CYCLE-HONEST]\r\n                        if session_is_zombie(spid, adapter_labeled, spawned_ms_ago) {\r\n                            // Re-observe immediately before any kill, off-lock.\r\n                            // The existing owned-process death oracle preserves\r\n                            // snapshot-less Unix cleanup and Linux zombie demotion.\r\n                            // [impl->REQ-BROKER-ZOMBIE-IDENTITY]\r\n                            match reap_zombie_root(\r\n                                spid,\r\n                                process_started_at,\r\n                                |pid| {\r\n                                    use spt_store::proc::{self, ProcIdentity};\r\n                                    if proc::provably_gone(pid) {\r\n                                        ProcIdentity::Absent\r\n                                    } else {\r\n                                        proc::process_started_at(pid)\r\n                                            .map(ProcIdentity::Present)\r\n                                            .unwrap_or(ProcIdentity::Unproven)\r\n                                    }\r\n                                },\r\n                                spt_store::proc::kill_pid_tree,\r\n                            ) {\r\n                                Ok(ZombieReap::Recycled { recorded, observed }) => {\r\n                                    spt_proto::emit_line_err!(\r\n                                        \"SPAWN_ZOMBIE_REAP_RECYCLED:{}: session {sid} pid={spid:?} \\\r\n                                         recorded_birth={recorded} observed_birth={observed} — \\\r\n                                         remembered root gone; no signal, clearing stale session\",\r\n                                        req.endpoint\r\n                                    );\r\n                                }\r\n                                Ok(ZombieReap::Absent | ZombieReap::Killed) => {\r\n                                    spt_proto::emit_line_err!(\r\n                                        \"SPAWN_ZOMBIE_REAP:{}: claimed session {sid} has a dead client \\\r\n                                         tree — reaping before honest respawn [REQ-ENDPOINT-CYCLE-HONEST]\",\r\n                                        req.endpoint\r\n                                    );\r\n                                }\r\n                                Err(reason) => {\r\n                                    spt_proto::emit_line_err!(\r\n                                        \"SPAWN_ZOMBIE_REAP_DECLINE:{}: session {sid} pid={spid:?} \\\r\n                                         reason={reason} — no kill or forced de-table\",\r\n                                        req.endpoint\r","truncated":true,"truncatedBy":"lines","totalLines":8824,"totalBytes":14763,"outputLines":255,"outputBytes":14763,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":16852,"displayContent":{"text":"    fn redeem_bringup_admit_at(\n…\n    ) -> AdmitTicket {\n…\n        match claimed {\n…\n            _ => AdmitTicket::Unredeemed,\r\n        }\r\n    }\r\n\r\n    /// Record that this broker has STARTED a bring-up for `endpoint`, good for\r\n    /// `ttl` — the bring-up's own ready-wait (see [`Self::bringups_in_flight`]).\r\n    ///\r\n    /// Written before the launch fires, so the fact exists for the whole window\r\n    /// in which the spawned harness may ask about it. Expired entries are swept\r\n    /// on every write, the way [`Self::mint_bringup_admit_at`] sweeps tickets, so\r\n    /// the map holds only what is still live and a broker that never sees another\r\n    /// bring-up does not accumulate.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn note_bringup_in_flight(&self, endpoint: &str, ttl: Duration) {\r\n        self.note_bringup_in_flight_at(endpoint, ttl, Instant::now());\r\n    }\r\n\r\n    /// [`note_bringup_in_flight`](Self::note_bringup_in_flight) with the clock\r\n    /// passed in — the expiry seam, for the same reason\r\n    /// [`mint_bringup_admit_at`](Self::mint_bringup_admit_at) has one: `Instant`\r\n    /// cannot be fabricated, so a test that must see an entry EXPIRE has no other\r\n    /// way across the TTL than to sleep through it, and a sleeping test proves\r\n    /// the TTL only for the value it slept for.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn note_bringup_in_flight_at(&self, endpoint: &str, ttl: Duration, now: Instant) {\r\n        let mut flight = recover(&self.bringups_in_flight);\r\n        flight.retain(|_, expires| *expires > now);\r\n        flight.insert(endpoint.to_string(), now + ttl);\r\n    }\r\n\r\n    /// Drop `endpoint`'s in-flight entry: its session row now exists, so the\r\n    /// ledger has nothing left to say about it.\r\n    ///\r\n    /// A no-op for the endpoints that never had an entry, which is every\r\n    /// ordinary spawn — only a bring-up writes one.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn clear_bringup_in_flight(&self, endpoint: &str) {\r\n        recover(&self.bringups_in_flight).remove(endpoint);\r\n    }\r\n\r\n    /// The endpoints whose bring-up is in flight RIGHT NOW, expired entries\r\n    /// excluded.\r\n    ///\r\n    /// Reads without pruning: this answers a query, and a query that mutates the\r\n    /// ledger would make the reply depend on who asked last.\r\n    // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n    fn bringups_in_flight_at(&self, now: Instant) -> Vec<String> {\r\n        recover(&self.bringups_in_flight)\r\n            .iter()\r\n            .filter(|(_, expires)| **expires > now)\r\n            .map(|(endpoint, _)| endpoint.clone())\r\n            .collect()\r\n    }\r\n\r\n    /// The shared spawn choke behind BOTH policies (`fresh=false` =\r\n    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\r\n    fn dispatch_spawn_policy(\r\n        &self,\r\n        req: SpawnReq,\r\n        send: &SharedSend,\r\n        fresh: bool,\r\n    ) -> Result<Option<u64>, String> {\r\n        // releases#199 instrument, BROKER SIDE. The launch-path stamps say when the\r\n        // client asked; these say what the broker did with the ask. Two spans are\r\n        // separately suspect and were previously indistinguishable from each other\r\n        // and from the client's own wait: the single-flight gate below (bounded by the\r\n        // holder's PROGRESS since #235, plus an unbounded-looking zombie-reap re-evaluation)\r\n        // and the ConPTY create (candidate C). Stamped in the SHARED choke, not\r\n        // only on the bring-up path, because the choke is where the time goes.\r\n        // [impl->REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE]\r\n        let spawn_t0 = Instant::now();\r\n        // The label is CLONED, not borrowed from `req`: the request's fields are\r\n        // moved into the spawn below, and a closure holding `&req` would keep the\r\n        // whole request borrowed past those moves.\r\n        let spawn_label = req.endpoint.clone();\r\n        // #235: the breadcrumb sites are ALSO the progress sites. The gate decides\r\n        // \"still working\" vs \"wedged\" from the holder's last phase stamp, so every\r\n        // place that already announces a phase records it into the claim too —\r\n        // rather than a second set of call sites that can drift from these.\r\n        // `None` until this thread wins the claim; a stamp before then would be\r\n        // writing progress into somebody else's claim.\r\n        let claim_gen: std::cell::Cell<Option<u64>> = std::cell::Cell::new(None);\r\n        let spawn_phase = |phase: &str, detail: &str| {\r\n            spt_proto::emit_line_err!(\r\n                \"SPAWN_PHASE:{spawn_label} phase={phase} t_ms={}{}{detail}\",\r\n                spawn_t0.elapsed().as_millis(),\r\n                if detail.is_empty() { \"\" } else { \" \" },\r\n            );\r\n            if let Some(generation) = claim_gen.get() {\r\n                let mut inflight = recover(&self.wake_inflight);\r\n                if let Some(claim) = inflight.get_mut(&spawn_label) {\r\n                    // Only OUR generation: a holder taken over as wedged must not\r\n                    // keep refreshing the successor's claim from behind.\r\n                    if claim.generation == generation {\r\n                        claim.phase = phase.to_string();\r\n                        claim.phase_at = Instant::now();\r\n                    }\r\n                }\r\n            }\r\n        };\r\n        spawn_phase(\"received\", &format!(\"fresh={fresh}\"));\r\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\r\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\r\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\r\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\r\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\r\n        // then stomped info.json, routing injects to a contended record). The claim +\r\n        // the live-session check are read together under one atomic critical section so\r\n        // a concurrent spawn either sees our claim or its registered session — never a\r\n        // gap; NO I/O under the lock (process_id reads a cached pid; the ZOMBIE probe\r\n        // runs OFF the lock in the AlreadyLive arm). An empty endpoint is an\r\n        // ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim` holds\r\n        // the claim to fn end, releasing (RAII) after the session is table-visible\r\n        // or on any early error.\r\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\r\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\r\n            None\r\n        } else {\r\n            loop {\r\n                enum Gate<'a> {\r\n                    Claimed(WakeClaimGuard<'a>),\r\n                    AlreadyLive {\r\n                        sid: u64,\r\n                        spid: Option<u32>,\r\n                        process_started_at: Option<u64>,\r\n                        adapter_labeled: bool,\r\n                        spawned_ms_ago: u64,\r\n                    },\r\n                    Racing,\r\n                }\r\n                let gate = {\r\n                    let sessions = recover(&self.sessions);\r\n                    let mut inflight = recover(&self.wake_inflight);\r\n                    let live = sessions.iter().find(|(_, h)| h.endpoint == req.endpoint).map(\r\n                        |(sid, h)| {\r\n                            (\r\n                                *sid,\r\n                                h.session.process_id(),\r\n                                h.process_started_at,\r\n                                !h.adapter.is_empty(),\r\n                                h.spawned_at.elapsed().as_millis() as u64,\r\n                            )\r\n                        },\r\n                    );\r\n                    // The holder's own phase stamp, read under the SAME lock as the\r\n                    // claim — an elapsed against a recorded Instant, no I/O, per this\r\n                    // block's no-I/O-under-the-lock contract.\r\n                    let held = inflight.get(&req.endpoint).cloned();\r\n                    let holder_stalled = held\r\n                        .as_ref()\r\n                        .is_some_and(|c| c.phase_at.elapsed() >= phase_wedge_ceiling(&c.phase));\r\n                    match wake_gate_decision(live.is_some(), held.is_some(), holder_stalled) {\r\n                        WakeGate::AlreadyLive => {\r\n                            let (sid, spid, process_started_at, adapter_labeled, spawned_ms_ago) =\r\n                                live.expect(\"live is Some on AlreadyLive\");\r\n                            Gate::AlreadyLive {\r\n                                sid, spid, process_started_at, adapter_labeled, spawned_ms_ago,\r\n                            }\r\n                        }\r\n                        WakeGate::Racing => Gate::Racing,\r\n                        WakeGate::Claim => {\r\n                            // A takeover is the availability arm firing. LOUD and\r\n                            // always, naming the phase and the elapsed that decided\r\n                            // it — a silent takeover is indistinguishable from a\r\n                            // free endpoint in every log we would read afterwards.\r\n                            if let Some(prev) = &held {\r\n                                WEDGE_TAKEOVER_COUNT.fetch_add(1, Ordering::Relaxed);\r\n                                eprintln!(\r\n                                    \"SPAWN_WEDGE_TAKEOVER:{}: claim gen {} wedged in phase {} for {}ms \\\r\n                                     (ceiling {}ms) — taking over \\\r\n                                     [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\r\n                                    req.endpoint,\r\n                                    prev.generation,\r\n                                    prev.phase,\r\n                                    prev.phase_at.elapsed().as_millis(),\r\n                                    phase_wedge_ceiling(&prev.phase).as_millis(),\r\n                                );\r\n                            }\r\n                            let generation =\r\n                                self.wake_generation.fetch_add(1, Ordering::Relaxed) + 1;\r\n                            inflight.insert(\r\n                                req.endpoint.clone(),\r\n                                WakeClaim {\r\n                                    generation,\r\n                                    phase: \"gate_claimed\".to_string(),\r\n                                    phase_at: Instant::now(),\r\n                                },\r\n                            );\r\n                            claim_gen.set(Some(generation));\r\n                            Gate::Claimed(WakeClaimGuard {\r\n                                map: &self.wake_inflight,\r\n                                endpoint: req.endpoint.clone(),\r\n                                generation,\r\n                            })\r\n                        }\r\n                    }\r\n                };\r\n                match gate {\r\n                    Gate::Claimed(guard) => {\r\n                        spawn_phase(\"gate_claimed\", \"\");\r\n                        break Some(guard);\r\n                    }\r\n                    Gate::AlreadyLive {\r\n                        sid, spid, process_started_at, adapter_labeled, spawned_ms_ago,\r\n                    } => {\r\n                        // ONE liveness authority (ADR-0041 decision 6,\r\n                        // REQ-ENDPOINT-CYCLE-HONEST): before refusing/deduping by\r\n                        // citing the claimed session, PROBE its client tree — off\r\n                        // every lock. A zombie (dead root, or an adapter wrapper\r\n                        // whose harness clients died past boot grace) is reaped\r\n                        // honestly — identity-checked tree-kill only if the root\r\n                        // is still ours, then de-table — and the gate re-evaluates:\r\n                        // the next pass claims and spawns for real. Never a\r\n                        // refusal citing a corpse.\r\n                        // [impl->REQ-ENDPOINT-CYCLE-HONEST]\r\n                        if session_is_zombie(spid, adapter_labeled, spawned_ms_ago) {\r\n                            // Re-observe immediately before any kill, off-lock.\r\n                            // The existing owned-process death oracle preserves\r\n                            // snapshot-less Unix cleanup and Linux zombie demotion.\r\n                            // [impl->REQ-BROKER-ZOMBIE-IDENTITY]\r\n                            match reap_zombie_root(\r\n                                spid,\r\n                                process_started_at,\r\n                                |pid| {\r\n                                    use spt_store::proc::{self, ProcIdentity};\r\n                                    if proc::provably_gone(pid) {\r\n                                        ProcIdentity::Absent\r\n                                    } else {\r\n                                        proc::process_started_at(pid)\r\n                                            .map(ProcIdentity::Present)\r\n                                            .unwrap_or(ProcIdentity::Unproven)\r\n                                    }\r\n                                },\r\n                                spt_store::proc::kill_pid_tree,\r\n                            ) {\r\n                                Ok(ZombieReap::Recycled { recorded, observed }) => {\r\n                                    spt_proto::emit_line_err!(\r\n                                        \"SPAWN_ZOMBIE_REAP_RECYCLED:{}: session {sid} pid={spid:?} \\\r\n                                         recorded_birth={recorded} observed_birth={observed} — \\\r\n                                         remembered root gone; no signal, clearing stale session\",\r\n                                        req.endpoint\r\n                                    );\r\n                                }\r\n                                Ok(ZombieReap::Absent | ZombieReap::Killed) => {\r\n                                    spt_proto::emit_line_err!(\r\n                                        \"SPAWN_ZOMBIE_REAP:{}: claimed session {sid} has a dead client \\\r\n                                         tree — reaping before honest respawn [REQ-ENDPOINT-CYCLE-HONEST]\",\r\n                                        req.endpoint\r\n                                    );\r\n                                }\r\n                                Err(reason) => {\r\n                                    spt_proto::emit_line_err!(\r\n                                        \"SPAWN_ZOMBIE_REAP_DECLINE:{}: session {sid} pid={spid:?} \\\r\n                                         reason={reason} — no kill or forced de-table\",\r\n                                        req.endpoint\r\n                                    );\n…\n                                }\n                            }\n…\n                        }\n…\n                    }\n…\n                }\n            }\n        };\n…\n    }","startLine":8012,"lineNumbers":[8012,null,8017,null,8022,null,8029,8030,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8062,8063,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8117,8118,8119,8120,8121,8122,8123,8124,8125,8126,8127,8128,8129,8130,8131,8132,8133,8134,8135,8136,8137,8138,8139,8140,8141,8142,8143,8144,8145,8146,8147,8148,8149,8150,8151,8152,8153,8154,8155,8156,8157,8158,8159,8160,8161,8162,8163,8164,8165,8166,8167,8168,8169,8170,8171,8172,8173,8174,8175,8176,8177,8178,8179,8180,8181,8182,8183,8184,8185,8186,8187,8188,8189,8190,8191,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8204,8205,8206,8207,8208,8209,8210,8211,8212,8213,8214,8215,8216,8217,8218,8219,8220,8221,8222,8223,8224,8225,8226,8227,8228,8229,8230,8231,8232,8233,8234,8235,8236,8237,8238,8239,8240,8241,8242,8243,8244,8245,8246,8247,8248,8249,8250,8251,8252,8253,8254,8255,8256,8257,8258,8259,8260,8261,8262,8263,8264,8265,8266,8267,8268,8269,8270,8271,8272,8273,8274,8275,8276,8277,8278,8279,8280,8281,8282,8283,8284,null,8292,8293,null,8317,null,8352,null,8354,8355,8356,null,8740]},"fileSize":877537,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":16852,"totalBytes":14763,"outputLines":255,"outputBytes":14763,"shownRange":{"start":8029,"end":8283},"nextOffset":8284}}},"isError":false,"timestamp":1789394558386}}
{"type":"message","id":"ca306e88","parentId":"cb6720a1","timestamp":"2026-09-14T14:02:38.392Z","message":{"role":"toolResult","toolCallId":"call_PA0HgD2NSF1UJfNDHZ1ERWF4|fc_0a63d1e8a5b46c9c016aa7fe7e1a9887d1bd5fdb0f6a6196c1","toolName":"read","content":[{"type":"text","text":"    fn dispatch_spawn_policy(\n…\n    ) -> Result<Option<u64>, String> {\n…\n        thread::spawn(move || {\n…\n            {\n…\n                for (tx, sink) in fanout.viewers {\n…\n                }\n            }\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n            // died, not the controller conn, so no detach path clears the perch\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n            // outlive the session in info.json (or gossip a stale controller).\n            // Bumps the stamp generation under the per-endpoint write serial,\n            // so any snapshot taken BEFORE the removal is refused at write time.\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\n            recover_log(&log_exit).stamp_reaped();\n        });\n\n        spawn_phase(\"waiter_armed\", \"\");\n\n        // Layer C: if the spawn carried a translation binary, bring it up with the\n        // session — spawn the pure stdin→stdout filter + the inject worker that\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\n        // aborting the session — a missing binary costs idle delivery, never the\n        // operator their agent.\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\n        // resets it on a healthy commit, the dispatch respawn path increments it).\n        let translation_respawns = Arc::new(AtomicU32::new(0));\n        let translation = req\n            .translation_binary\n            .as_deref()\n            .and_then(|argv| {\n                build_translation(\n                    argv,\n                    &req.endpoint,\n                    &input,\n                    Arc::clone(&translation_respawns),\n                    &log,\n                )\n            });\n\n        spawn_phase(\"translation_ready\", \"\");\n\n        recover(&self.sessions).insert(\n            id,\n            HostedSession {\n                session,\n                input,\n                input_reports: Arc::new(Mutex::new(crate::inputreceipt::ReceiptBook::default())),\n                drain,\n                log,\n                endpoint: req.endpoint.clone(),\n                translation,\n                translation_argv: req.translation_binary.clone(),\n                translation_respawns,\n                adapter: req.adapter.clone(),\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\n                spawned_at: Instant::now(),\n                process_started_at,\n            },\n        );\n        // The row exists, so the in-flight ledger has nothing left to say about\n        // this endpoint (releases#199). Cleared HERE, under the same moment that\n        // inserts the row, so the two facts are never both true and never both\n        // false: the window the ledger covers closes exactly where the sessions\n        // table opens. A no-op for every ordinary spawn — only a bring-up writes\n        // an entry.\n        // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\n        self.clear_bringup_in_flight(&req.endpoint);\n        // releases#199 instrument: the row is the fact the bring-up's poll reads.\n        // This stamp and `ER_BRINGUP_WAIT_EXPIRED` are the two sides of that\n        // question — which of them the sink carries decides whether the launch was\n        // late or absent.\n        spawn_phase(\"row_inserted\", &format!(\"session={id}\"));\n\n        let resp = Spawned {\n            session_id: id,\n            pid,\n        };\n        let frame = Envelope::new(\n            KIND_SPAWNED,\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(Some(id))\n    }\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        // A bring-up this broker just admitted seats its controller without\n        // re-presenting the code, ONCE, for the session it spawned. The reason\n        // is a race, not a discount: the bounded wait for the harness can\n        // outlive the ±1 TOTP window, so the same gate that admitted a code\n        // moments ago can refuse it here — gate-admits-then-attach-refuses,\n        // which is the defect this wave closes, rebuilt one seam later.\n        //\n        // Read off the presented SECRET, not off this connection: the subscribe\n        // that seats an rc attach is sent by the dispatcher's own conn, never\n        // the one that asked for the bring-up (dispatch.rs `connect` →\n        // attach.rs `attach_as_coded`), so a conn-keyed admit could never be\n        // redeemed. The ticket rides the code field the operator's code already\n        // rides, which is the one pipe that does cross this seam.\n        // [impl->REQ-ER-BRINGUP-SPAWNS-SESSION]\n        let ticket = self.redeem_bringup_admit(req.code.as_deref(), req.session_id);\n        let (outcome, seat_note) = recover_log(&log).resolve_subscribe_gated(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n            req.gen,\n            // The bring-up code the operator presented, for the ONE endpoint whose\n            // seat resolver demands one (ADR-0052 decision 2).\n            // [impl->REQ-ER-BRINGUP-TOTP-GATE]\n            req.code,\n            ticket,\n            // The W2 seal-ceremony capability this client declared.\n            // [impl->REQ-SEAL-NO-CEREMONY-SURFACE]\n            req.seal_ceremony,\n        );\n        send_frame(\n            send,\n            &subscribed_envelope(req.session_id, outcome, seat_note),\n        );\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    spt_proto::emit_line_err!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if_gen(send, req.gen);\n        // A controller that detaches cleanly mid-ceremony aborts it — the\n        // requester is answered cancelled instead of waiting on an overlay\n        // nobody hosts (REQ-SEAL-CEREMONY-ESC-CANCEL's involuntary arm).\n        self.abort_ceremonies_for_conn(send.id());\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///\n    /// With an `op_id` the write is routed through the broker-owned\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\n    /// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy\n    /// direct write (no dedup, no ack).\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: InputReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\n        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!(\"bad input base64: {e}\"))?;\n        // Clone the input-writer Arc (and the translation handle, if any) out from\n        // under the sessions lock so the ENQUEUE happens without holding the table\n        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\n        // dispatch thread is never parked here even when the harness has stalled\n        // (P0 / REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\n        let (input, translation, log) = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            (Arc::clone(&h.input), h.translation.clone(), Arc::clone(&h.log))\n        };\n        // RC-ORIGIN INPUT FENCE (ADR-0044 decision 3, the required defense):\n        // an rc-tagged input must come from the ACTIVE controller lease's\n        // originating conn — the same conn-identity gate resize has always had\n        // — so a displaced viewport can never keep typing into the PTY, no\n        // matter whether its Displaced notice was delivered, delayed, or lost.\n        // SCOPED to `Minter::Rc`: shell/system injection (`Minter::Shell`,\n        // legacy/untagged senders) legitimately writes from non-controller\n        // conns and is untouched. The error frame this surfaces terminates the\n        // stale serve worker's loop — defense in depth on the same terminal\n        // signal. In-memory check only (no I/O under the log lock).\n        // [impl->REQ-INPUT-CONTROLLER-FENCE]\n        if rc_input_rejected(req.minter, recover_log(&log).is_controller(send)) {\n            return Err(format!(\n                \"input is controller-exclusive: this rc lease was displaced on \\\n                 session {} (a newer controller holds the slot)\",\n                req.session_id\n            ));\n        }\n        // Layer C: when a (non-faulted) translation binary is attached, fire a\n        // CONTENT-FREE `{type:input}` ping so the binary can track operator idleness\n        // — best-effort, never carries the keystroke bytes (doyle gate #1, privacy).\n        if let Some(tr) = &translation {\n            if !tr.faulted.load(Ordering::Relaxed) {\n                let _ = tr.child.send(&ToBinary::Input);\n            }\n        }\n        // Layer C: if an injected sequence is mid-flight, the operator's keystrokes\n        // BUFFER in the inject floor and are flushed to the PTY after the sequence\n        // commits — so a stash/restore choreography can't be clobbered. Returns\n        // true when buffered (the caller must NOT write the PTY now).\n        let buffered = |bytes: &[u8]| -> bool {\n            translation\n                .as_ref()\n                .is_some_and(|tr| lock_floor(&tr.floor).buffer_if_held(bytes))\n        };\n        match req.op_id {\n            Some(op) => {\n                // Materialize the minter at the decode seam: an untagged (older)\n                // sender ⇒ Legacy; a live producer stamped its real tag (ADR-0034).\n                let minter = req.minter.unwrap_or(Minter::Legacy);\n                let key = EffectKey {\n                    class: req.session_id,\n                    minter,\n                    op,\n                };\n                let outcome = self\n                    .journal\n                    .apply_once(key, EffectKind::PtyWrite, || {\n                        // Buffer-or-ENQUEUE INSIDE the journal effect so the op is\n                        // still recorded applied + acked (dedup-safe) whether the byte\n                        // buffers (inject floor held), enqueues, or is dropped under\n                        // backpressure. The effect is now the non-blocking enqueue —\n                        // PtyWrite is ephemeral, so Applied-at-enqueue is sound\n                        // (V0.13.0-P0 design); the ack means \"accepted + ordered + will\n                        // land,\" and rc does not gate on landing.\n                        if !buffered(&bytes) {\n                            input.enqueue(bytes.clone());\n                        }\n                        Ok(())\n                    })\n                    .map_err(|e| format!(\"input enqueue failed: {e}\"))?;\n                // Ack exactly-once — but ONLY when the caller requested it\n…\n            }\n…\n        }\n    }\n\n[Showing lines 8659-8923 of 16852. Use :8924 to continue]"}],"details":{"truncation":{"content":"                }\r\n            }\r\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\r\n            // died, not the controller conn, so no detach path clears the perch\r\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\r\n            // outlive the session in info.json (or gossip a stale controller).\r\n            // Bumps the stamp generation under the per-endpoint write serial,\r\n            // so any snapshot taken BEFORE the removal is refused at write time.\r\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n            recover_log(&log_exit).stamp_reaped();\r\n        });\r\n\r\n        spawn_phase(\"waiter_armed\", \"\");\r\n\r\n        // Layer C: if the spawn carried a translation binary, bring it up with the\r\n        // session — spawn the pure stdin→stdout filter + the inject worker that\r\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\r\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\r\n        // aborting the session — a missing binary costs idle delivery, never the\r\n        // operator their agent.\r\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\r\n        // resets it on a healthy commit, the dispatch respawn path increments it).\r\n        let translation_respawns = Arc::new(AtomicU32::new(0));\r\n        let translation = req\r\n            .translation_binary\r\n            .as_deref()\r\n            .and_then(|argv| {\r\n                build_translation(\r\n                    argv,\r\n                    &req.endpoint,\r\n                    &input,\r\n                    Arc::clone(&translation_respawns),\r\n                    &log,\r\n                )\r\n            });\r\n\r\n        spawn_phase(\"translation_ready\", \"\");\r\n\r\n        recover(&self.sessions).insert(\r\n            id,\r\n            HostedSession {\r\n                session,\r\n                input,\r\n                input_reports: Arc::new(Mutex::new(crate::inputreceipt::ReceiptBook::default())),\r\n                drain,\r\n                log,\r\n                endpoint: req.endpoint.clone(),\r\n                translation,\r\n                translation_argv: req.translation_binary.clone(),\r\n                translation_respawns,\r\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n                spawned_at: Instant::now(),\r\n                process_started_at,\r\n            },\r\n        );\r\n        // The row exists, so the in-flight ledger has nothing left to say about\r\n        // this endpoint (releases#199). Cleared HERE, under the same moment that\r\n        // inserts the row, so the two facts are never both true and never both\r\n        // false: the window the ledger covers closes exactly where the sessions\r\n        // table opens. A no-op for every ordinary spawn — only a bring-up writes\r\n        // an entry.\r\n        // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n        self.clear_bringup_in_flight(&req.endpoint);\r\n        // releases#199 instrument: the row is the fact the bring-up's poll reads.\r\n        // This stamp and `ER_BRINGUP_WAIT_EXPIRED` are the two sides of that\r\n        // question — which of them the sink carries decides whether the launch was\r\n        // late or absent.\r\n        spawn_phase(\"row_inserted\", &format!(\"session={id}\"));\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(Some(id))\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\r\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\r\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\r\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\r\n    /// self-kick guard.\r\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\r\n        // Clone the log Arc out from under the sessions lock, then resolve under\r\n        // the log lock — never hold both locks at once.\r\n        //\r\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\r\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\r\n        // the per-session exit-waiter holds for the WHOLE life of a live child\r\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\r\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\r\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\r\n        // post-removal subscribe gets a prompt \"no such session\" error — both\r\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\r\n        // the rest.\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            Arc::clone(&h.log)\r\n        };\r\n        // A bring-up this broker just admitted seats its controller without\r\n        // re-presenting the code, ONCE, for the session it spawned. The reason\r\n        // is a race, not a discount: the bounded wait for the harness can\r\n        // outlive the ±1 TOTP window, so the same gate that admitted a code\r\n        // moments ago can refuse it here — gate-admits-then-attach-refuses,\r\n        // which is the defect this wave closes, rebuilt one seam later.\r\n        //\r\n        // Read off the presented SECRET, not off this connection: the subscribe\r\n        // that seats an rc attach is sent by the dispatcher's own conn, never\r\n        // the one that asked for the bring-up (dispatch.rs `connect` →\r\n        // attach.rs `attach_as_coded`), so a conn-keyed admit could never be\r\n        // redeemed. The ticket rides the code field the operator's code already\r\n        // rides, which is the one pipe that does cross this seam.\r\n        // [impl->REQ-ER-BRINGUP-SPAWNS-SESSION]\r\n        let ticket = self.redeem_bringup_admit(req.code.as_deref(), req.session_id);\r\n        let (outcome, seat_note) = recover_log(&log).resolve_subscribe_gated(\r\n            Arc::clone(send),\r\n            req.from_seq,\r\n            req.intent,\r\n            req.by,\r\n            req.gen,\r\n            // The bring-up code the operator presented, for the ONE endpoint whose\r\n            // seat resolver demands one (ADR-0052 decision 2).\r\n            // [impl->REQ-ER-BRINGUP-TOTP-GATE]\r\n            req.code,\r\n            ticket,\r\n            // The W2 seal-ceremony capability this client declared.\r\n            // [impl->REQ-SEAL-NO-CEREMONY-SURFACE]\r\n            req.seal_ceremony,\r\n        );\r\n        send_frame(\r\n            send,\r\n            &subscribed_envelope(req.session_id, outcome, seat_note),\r\n        );\r\n        Ok(req.session_id)\r\n    }\r\n\r\n    /// Release this connection's controller/viewer role on a session\r\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\r\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\r\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\r\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\r\n    /// a no-op if a newer controller already took over, or the session is gone.\r\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&req.session_id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => {\r\n                    spt_proto::emit_line_err!(\"DBG unsubscribe: no session {}\", req.session_id);\r\n                    return Ok(());\r\n                }\r\n            }\r\n        };\r\n        recover_log(&log).detach_if_gen(send, req.gen);\r\n        // A controller that detaches cleanly mid-ceremony aborts it — the\r\n        // requester is answered cancelled instead of waiting on an overlay\r\n        // nobody hosts (REQ-SEAL-CEREMONY-ESC-CANCEL's involuntary arm).\r\n        self.abort_ceremonies_for_conn(send.id());\r\n        Ok(())\r\n    }\r\n\r\n    /// Inject input bytes into a session's PTY.\r\n    ///\r\n    /// With an `op_id` the write is routed through the broker-owned\r\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\r\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\r\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\r\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\r\n    /// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy\r\n    /// direct write (no dedup, no ack).\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: InputReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\r\n        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!(\"bad input base64: {e}\"))?;\r\n        // Clone the input-writer Arc (and the translation handle, if any) out from\r\n        // under the sessions lock so the ENQUEUE happens without holding the table\r\n        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\r\n        // dispatch thread is never parked here even when the harness has stalled\r\n        // (P0 / REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\r\n        let (input, translation, log) = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            (Arc::clone(&h.input), h.translation.clone(), Arc::clone(&h.log))\r\n        };\r\n        // RC-ORIGIN INPUT FENCE (ADR-0044 decision 3, the required defense):\r\n        // an rc-tagged input must come from the ACTIVE controller lease's\r\n        // originating conn — the same conn-identity gate resize has always had\r\n        // — so a displaced viewport can never keep typing into the PTY, no\r\n        // matter whether its Displaced notice was delivered, delayed, or lost.\r\n        // SCOPED to `Minter::Rc`: shell/system injection (`Minter::Shell`,\r\n        // legacy/untagged senders) legitimately writes from non-controller\r\n        // conns and is untouched. The error frame this surfaces terminates the\r\n        // stale serve worker's loop — defense in depth on the same terminal\r\n        // signal. In-memory check only (no I/O under the log lock).\r\n        // [impl->REQ-INPUT-CONTROLLER-FENCE]\r\n        if rc_input_rejected(req.minter, recover_log(&log).is_controller(send)) {\r\n            return Err(format!(\r\n                \"input is controller-exclusive: this rc lease was displaced on \\\r\n                 session {} (a newer controller holds the slot)\",\r\n                req.session_id\r\n            ));\r\n        }\r\n        // Layer C: when a (non-faulted) translation binary is attached, fire a\r\n        // CONTENT-FREE `{type:input}` ping so the binary can track operator idleness\r\n        // — best-effort, never carries the keystroke bytes (doyle gate #1, privacy).\r\n        if let Some(tr) = &translation {\r\n            if !tr.faulted.load(Ordering::Relaxed) {\r\n                let _ = tr.child.send(&ToBinary::Input);\r\n            }\r\n        }\r\n        // Layer C: if an injected sequence is mid-flight, the operator's keystrokes\r\n        // BUFFER in the inject floor and are flushed to the PTY after the sequence\r\n        // commits — so a stash/restore choreography can't be clobbered. Returns\r\n        // true when buffered (the caller must NOT write the PTY now).\r\n        let buffered = |bytes: &[u8]| -> bool {\r\n            translation\r\n                .as_ref()\r\n                .is_some_and(|tr| lock_floor(&tr.floor).buffer_if_held(bytes))\r\n        };\r\n        match req.op_id {\r\n            Some(op) => {\r\n                // Materialize the minter at the decode seam: an untagged (older)\r\n                // sender ⇒ Legacy; a live producer stamped its real tag (ADR-0034).\r\n                let minter = req.minter.unwrap_or(Minter::Legacy);\r\n                let key = EffectKey {\r\n                    class: req.session_id,\r\n                    minter,\r\n                    op,\r\n                };\r\n                let outcome = self\r\n                    .journal\r\n                    .apply_once(key, EffectKind::PtyWrite, || {\r\n                        // Buffer-or-ENQUEUE INSIDE the journal effect so the op is\r\n                        // still recorded applied + acked (dedup-safe) whether the byte\r\n                        // buffers (inject floor held), enqueues, or is dropped under\r\n                        // backpressure. The effect is now the non-blocking enqueue —\r\n                        // PtyWrite is ephemeral, so Applied-at-enqueue is sound\r\n                        // (V0.13.0-P0 design); the ack means \"accepted + ordered + will\r\n                        // land,\" and rc does not gate on landing.\r\n                        if !buffered(&bytes) {\r\n                            input.enqueue(bytes.clone());\r\n                        }\r\n                        Ok(())\r\n                    })\r\n                    .map_err(|e| format!(\"input enqueue failed: {e}\"))?;\r\n                // Ack exactly-once — but ONLY when the caller requested it\r","truncated":true,"truncatedBy":"lines","totalLines":8194,"totalBytes":14263,"outputLines":265,"outputBytes":14263,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":16852,"displayContent":{"text":"    fn dispatch_spawn_policy(\n…\n    ) -> Result<Option<u64>, String> {\n…\n        thread::spawn(move || {\n…\n            {\n…\n                for (tx, sink) in fanout.viewers {\n…\n                }\r\n            }\r\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\r\n            // died, not the controller conn, so no detach path clears the perch\r\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\r\n            // outlive the session in info.json (or gossip a stale controller).\r\n            // Bumps the stamp generation under the per-endpoint write serial,\r\n            // so any snapshot taken BEFORE the removal is refused at write time.\r\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n            recover_log(&log_exit).stamp_reaped();\r\n        });\r\n\r\n        spawn_phase(\"waiter_armed\", \"\");\r\n\r\n        // Layer C: if the spawn carried a translation binary, bring it up with the\r\n        // session — spawn the pure stdin→stdout filter + the inject worker that\r\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\r\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\r\n        // aborting the session — a missing binary costs idle delivery, never the\r\n        // operator their agent.\r\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\r\n        // resets it on a healthy commit, the dispatch respawn path increments it).\r\n        let translation_respawns = Arc::new(AtomicU32::new(0));\r\n        let translation = req\r\n            .translation_binary\r\n            .as_deref()\r\n            .and_then(|argv| {\r\n                build_translation(\r\n                    argv,\r\n                    &req.endpoint,\r\n                    &input,\r\n                    Arc::clone(&translation_respawns),\r\n                    &log,\r\n                )\r\n            });\r\n\r\n        spawn_phase(\"translation_ready\", \"\");\r\n\r\n        recover(&self.sessions).insert(\r\n            id,\r\n            HostedSession {\r\n                session,\r\n                input,\r\n                input_reports: Arc::new(Mutex::new(crate::inputreceipt::ReceiptBook::default())),\r\n                drain,\r\n                log,\r\n                endpoint: req.endpoint.clone(),\r\n                translation,\r\n                translation_argv: req.translation_binary.clone(),\r\n                translation_respawns,\r\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n                spawned_at: Instant::now(),\r\n                process_started_at,\r\n            },\r\n        );\r\n        // The row exists, so the in-flight ledger has nothing left to say about\r\n        // this endpoint (releases#199). Cleared HERE, under the same moment that\r\n        // inserts the row, so the two facts are never both true and never both\r\n        // false: the window the ledger covers closes exactly where the sessions\r\n        // table opens. A no-op for every ordinary spawn — only a bring-up writes\r\n        // an entry.\r\n        // [impl->REQ-ER-BRINGUP-INFLIGHT-LEDGER]\r\n        self.clear_bringup_in_flight(&req.endpoint);\r\n        // releases#199 instrument: the row is the fact the bring-up's poll reads.\r\n        // This stamp and `ER_BRINGUP_WAIT_EXPIRED` are the two sides of that\r\n        // question — which of them the sink carries decides whether the launch was\r\n        // late or absent.\r\n        spawn_phase(\"row_inserted\", &format!(\"session={id}\"));\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(Some(id))\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\r\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\r\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\r\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\r\n    /// self-kick guard.\r\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\r\n        // Clone the log Arc out from under the sessions lock, then resolve under\r\n        // the log lock — never hold both locks at once.\r\n        //\r\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\r\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\r\n        // the per-session exit-waiter holds for the WHOLE life of a live child\r\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\r\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\r\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\r\n        // post-removal subscribe gets a prompt \"no such session\" error — both\r\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\r\n        // the rest.\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            Arc::clone(&h.log)\r\n        };\r\n        // A bring-up this broker just admitted seats its controller without\r\n        // re-presenting the code, ONCE, for the session it spawned. The reason\r\n        // is a race, not a discount: the bounded wait for the harness can\r\n        // outlive the ±1 TOTP window, so the same gate that admitted a code\r\n        // moments ago can refuse it here — gate-admits-then-attach-refuses,\r\n        // which is the defect this wave closes, rebuilt one seam later.\r\n        //\r\n        // Read off the presented SECRET, not off this connection: the subscribe\r\n        // that seats an rc attach is sent by the dispatcher's own conn, never\r\n        // the one that asked for the bring-up (dispatch.rs `connect` →\r\n        // attach.rs `attach_as_coded`), so a conn-keyed admit could never be\r\n        // redeemed. The ticket rides the code field the operator's code already\r\n        // rides, which is the one pipe that does cross this seam.\r\n        // [impl->REQ-ER-BRINGUP-SPAWNS-SESSION]\r\n        let ticket = self.redeem_bringup_admit(req.code.as_deref(), req.session_id);\r\n        let (outcome, seat_note) = recover_log(&log).resolve_subscribe_gated(\r\n            Arc::clone(send),\r\n            req.from_seq,\r\n            req.intent,\r\n            req.by,\r\n            req.gen,\r\n            // The bring-up code the operator presented, for the ONE endpoint whose\r\n            // seat resolver demands one (ADR-0052 decision 2).\r\n            // [impl->REQ-ER-BRINGUP-TOTP-GATE]\r\n            req.code,\r\n            ticket,\r\n            // The W2 seal-ceremony capability this client declared.\r\n            // [impl->REQ-SEAL-NO-CEREMONY-SURFACE]\r\n            req.seal_ceremony,\r\n        );\r\n        send_frame(\r\n            send,\r\n            &subscribed_envelope(req.session_id, outcome, seat_note),\r\n        );\r\n        Ok(req.session_id)\r\n    }\r\n\r\n    /// Release this connection's controller/viewer role on a session\r\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\r\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\r\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\r\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\r\n    /// a no-op if a newer controller already took over, or the session is gone.\r\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&req.session_id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => {\r\n                    spt_proto::emit_line_err!(\"DBG unsubscribe: no session {}\", req.session_id);\r\n                    return Ok(());\r\n                }\r\n            }\r\n        };\r\n        recover_log(&log).detach_if_gen(send, req.gen);\r\n        // A controller that detaches cleanly mid-ceremony aborts it — the\r\n        // requester is answered cancelled instead of waiting on an overlay\r\n        // nobody hosts (REQ-SEAL-CEREMONY-ESC-CANCEL's involuntary arm).\r\n        self.abort_ceremonies_for_conn(send.id());\r\n        Ok(())\r\n    }\r\n\r\n    /// Inject input bytes into a session's PTY.\r\n    ///\r\n    /// With an `op_id` the write is routed through the broker-owned\r\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\r\n    /// `(session_id, op_id)` after a brain crash is deduped (the PTY is not\r\n    /// written twice), and either outcome — fresh apply or dedup — is acked back\r\n    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the\r\n    /// effect (B5 / KNOWN-HAZARDS 7.2). Without an `op_id` it is the legacy\r\n    /// direct write (no dedup, no ack).\r\n    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\r\n    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: InputReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\r\n        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!(\"bad input base64: {e}\"))?;\r\n        // Clone the input-writer Arc (and the translation handle, if any) out from\r\n        // under the sessions lock so the ENQUEUE happens without holding the table\r\n        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\r\n        // dispatch thread is never parked here even when the harness has stalled\r\n        // (P0 / REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\r\n        let (input, translation, log) = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            (Arc::clone(&h.input), h.translation.clone(), Arc::clone(&h.log))\r\n        };\r\n        // RC-ORIGIN INPUT FENCE (ADR-0044 decision 3, the required defense):\r\n        // an rc-tagged input must come from the ACTIVE controller lease's\r\n        // originating conn — the same conn-identity gate resize has always had\r\n        // — so a displaced viewport can never keep typing into the PTY, no\r\n        // matter whether its Displaced notice was delivered, delayed, or lost.\r\n        // SCOPED to `Minter::Rc`: shell/system injection (`Minter::Shell`,\r\n        // legacy/untagged senders) legitimately writes from non-controller\r\n        // conns and is untouched. The error frame this surfaces terminates the\r\n        // stale serve worker's loop — defense in depth on the same terminal\r\n        // signal. In-memory check only (no I/O under the log lock).\r\n        // [impl->REQ-INPUT-CONTROLLER-FENCE]\r\n        if rc_input_rejected(req.minter, recover_log(&log).is_controller(send)) {\r\n            return Err(format!(\r\n                \"input is controller-exclusive: this rc lease was displaced on \\\r\n                 session {} (a newer controller holds the slot)\",\r\n                req.session_id\r\n            ));\r\n        }\r\n        // Layer C: when a (non-faulted) translation binary is attached, fire a\r\n        // CONTENT-FREE `{type:input}` ping so the binary can track operator idleness\r\n        // — best-effort, never carries the keystroke bytes (doyle gate #1, privacy).\r\n        if let Some(tr) = &translation {\r\n            if !tr.faulted.load(Ordering::Relaxed) {\r\n                let _ = tr.child.send(&ToBinary::Input);\r\n            }\r\n        }\r\n        // Layer C: if an injected sequence is mid-flight, the operator's keystrokes\r\n        // BUFFER in the inject floor and are flushed to the PTY after the sequence\r\n        // commits — so a stash/restore choreography can't be clobbered. Returns\r\n        // true when buffered (the caller must NOT write the PTY now).\r\n        let buffered = |bytes: &[u8]| -> bool {\r\n            translation\r\n                .as_ref()\r\n                .is_some_and(|tr| lock_floor(&tr.floor).buffer_if_held(bytes))\r\n        };\r\n        match req.op_id {\r\n            Some(op) => {\r\n                // Materialize the minter at the decode seam: an untagged (older)\r\n                // sender ⇒ Legacy; a live producer stamped its real tag (ADR-0034).\r\n                let minter = req.minter.unwrap_or(Minter::Legacy);\r\n                let key = EffectKey {\r\n                    class: req.session_id,\r\n                    minter,\r\n                    op,\r\n                };\r\n                let outcome = self\r\n                    .journal\r\n                    .apply_once(key, EffectKind::PtyWrite, || {\r\n                        // Buffer-or-ENQUEUE INSIDE the journal effect so the op is\r\n                        // still recorded applied + acked (dedup-safe) whether the byte\r\n                        // buffers (inject floor held), enqueues, or is dropped under\r\n                        // backpressure. The effect is now the non-blocking enqueue —\r\n                        // PtyWrite is ephemeral, so Applied-at-enqueue is sound\r\n                        // (V0.13.0-P0 design); the ack means \"accepted + ordered + will\r\n                        // land,\" and rc does not gate on landing.\r\n                        if !buffered(&bytes) {\r\n                            input.enqueue(bytes.clone());\r\n                        }\r\n                        Ok(())\r\n                    })\r\n                    .map_err(|e| format!(\"input enqueue failed: {e}\"))?;\r\n                // Ack exactly-once — but ONLY when the caller requested it\r\n…\n            }\n…\n        }\n    }","startLine":8085,"lineNumbers":[8085,null,8090,null,8532,null,8633,null,8657,null,8659,8660,8661,8662,8663,8664,8665,8666,8667,8668,8669,8670,8671,8672,8673,8674,8675,8676,8677,8678,8679,8680,8681,8682,8683,8684,8685,8686,8687,8688,8689,8690,8691,8692,8693,8694,8695,8696,8697,8698,8699,8700,8701,8702,8703,8704,8705,8706,8707,8708,8709,8710,8711,8712,8713,8714,8715,8716,8717,8718,8719,8720,8721,8722,8723,8724,8725,8726,8727,8728,8729,8730,8731,8732,8733,8734,8735,8736,8737,8738,8739,8740,8741,8742,8743,8744,8745,8746,8747,8748,8749,8750,8751,8752,8753,8754,8755,8756,8757,8758,8759,8760,8761,8762,8763,8764,8765,8766,8767,8768,8769,8770,8771,8772,8773,8774,8775,8776,8777,8778,8779,8780,8781,8782,8783,8784,8785,8786,8787,8788,8789,8790,8791,8792,8793,8794,8795,8796,8797,8798,8799,8800,8801,8802,8803,8804,8805,8806,8807,8808,8809,8810,8811,8812,8813,8814,8815,8816,8817,8818,8819,8820,8821,8822,8823,8824,8825,8826,8827,8828,8829,8830,8831,8832,8833,8834,8835,8836,8837,8838,8839,8840,8841,8842,8843,8844,8845,8846,8847,8848,8849,8850,8851,8852,8853,8854,8855,8856,8857,8858,8859,8860,8861,8862,8863,8864,8865,8866,8867,8868,8869,8870,8871,8872,8873,8874,8875,8876,8877,8878,8879,8880,8881,8882,8883,8884,8885,8886,8887,8888,8889,8890,8891,8892,8893,8894,8895,8896,8897,8898,8899,8900,8901,8902,8903,8904,8905,8906,8907,8908,8909,8910,8911,8912,8913,8914,8915,8916,8917,8918,8919,8920,8921,8922,8923,null,8940,null,8947,8948]},"fileSize":877537,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":16852,"totalBytes":14263,"outputLines":265,"outputBytes":14263,"shownRange":{"start":8659,"end":8923},"nextOffset":8924}}},"isError":false,"timestamp":1789394558392}}
{"type":"message","id":"b44e5654","parentId":"ca306e88","timestamp":"2026-09-14T14:02:38.402Z","message":{"role":"toolResult","toolCallId":"call_nXDZzDHzWYnXoP8XSWP6zbeX|fc_0a63d1e8a5b46c9c016aa7fe7e1ab487d1994a79c56073789d","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## brain.rs\n 57|    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*58|    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n 59|    KIND_SPAWN_CONFLICT, KIND_SPAWN_FRESH,\n 60|    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n 61|    BringUpReq, BroughtUpReply, KIND_BRING_UP, KIND_BROUGHT_UP,\n...\n 498|    // [impl->REQ-RC-DISPLAY-SOLE-WRITER]\n*499|    pub fn cold_start_pump(\n 500|        name: &str,\n 501|        now_ms: u64,\n 502|        io_timeout: Duration,\n...\n 1040|    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n*1041|    pub fn call_deadline(&self) -> Option<Instant> {\n 1042|        self.io_timeout.map(|d| Instant::now() + d)\n 1043|    }\n 1044|\n...\n 1207|                }\n*1208|                KIND_SPAWNED => {\n 1209|                    let ev: Spawned = serde_json::from_value(env.payload)\n 1210|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1211|                    return Ok(BrokerEvent::Spawned {\n## broker.rs\n 79|    SEAL_FIDO2_PROOF_REFUSED, encode_bytes,\n*80|    KIND_SPAWNED, KIND_SPAWN_CONFLICT, KIND_SPAWN_FRESH, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n 81|};\n 82|use crate::nethost::{NetHost, NET_EFFECT_SESSION};\n 83|use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\n...\n 6486|    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n*6487|    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 6488|        let req: SpawnReq =\n 6489|            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n 6490|        self.dispatch_spawn_policy(req, send, false)\n...\n 6499|    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n*6500|    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n 6501|        let req: SpawnReq =\n 6502|            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n 6503|        self.dispatch_spawn_policy(req, send, true)\n...\n 8084|    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\n*8085|    fn dispatch_spawn_policy(\n 8086|        &self,\n 8087|        req: SpawnReq,\n 8088|        send: &SharedSend,\n...\n 8345|                        let frame = Envelope::new(\n*8346|                            KIND_SPAWNED,\n 8347|                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\n 8348|                                .expect(\"Spawned serializes\"),\n 8349|                        );\n...\n 8734|        let frame = Envelope::new(\n*8735|            KIND_SPAWNED,\n 8736|            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n 8737|        );\n 8738|        send_frame(send, &frame);\n...\n 10261|/// Send an `error` event to the brain (best-effort).\n*10262|fn send_error(send: &SharedSend, message: &str) {\n 10263|    let frame = Envelope::new(\n 10264|        KIND_ERROR,\n 10265|        serde_json::to_value(ErrorEvent {"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs, .worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":11,"fileCount":2,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs","count":4},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","count":7}],"truncated":false,"displayContent":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## brain.rs\n   57│    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  *58│    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,\n   59│    KIND_SPAWN_CONFLICT, KIND_SPAWN_FRESH,\n   60│    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,\n   61│    BringUpReq, BroughtUpReply, KIND_BRING_UP, KIND_BROUGHT_UP,\n     │...\n  498│    // [impl->REQ-RC-DISPLAY-SOLE-WRITER]\n *499│    pub fn cold_start_pump(\n  500│        name: &str,\n  501│        now_ms: u64,\n  502│        io_timeout: Duration,\n     │...\n 1040│    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n*1041│    pub fn call_deadline(&self) -> Option<Instant> {\n 1042│        self.io_timeout.map(|d| Instant::now() + d)\n 1043│    }\n 1044│\n     │...\n 1207│                }\n*1208│                KIND_SPAWNED => {\n 1209│                    let ev: Spawned = serde_json::from_value(env.payload)\n 1210│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 1211│                    return Ok(BrokerEvent::Spawned {\n## broker.rs\n    79│    SEAL_FIDO2_PROOF_REFUSED, encode_bytes,\n   *80│    KIND_SPAWNED, KIND_SPAWN_CONFLICT, KIND_SPAWN_FRESH, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,\n    81│};\n    82│use crate::nethost::{NetHost, NET_EFFECT_SESSION};\n    83│use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};\n      │...\n  6486│    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n *6487│    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n  6488│        let req: SpawnReq =\n  6489│            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n  6490│        self.dispatch_spawn_policy(req, send, false)\n      │...\n  6499│    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n *6500│    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n  6501│        let req: SpawnReq =\n  6502│            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n  6503│        self.dispatch_spawn_policy(req, send, true)\n      │...\n  8084│    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\n *8085│    fn dispatch_spawn_policy(\n  8086│        &self,\n  8087│        req: SpawnReq,\n  8088│        send: &SharedSend,\n      │...\n  8345│                        let frame = Envelope::new(\n *8346│                            KIND_SPAWNED,\n  8347│                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\n  8348│                                .expect(\"Spawned serializes\"),\n  8349│                        );\n      │...\n  8734│        let frame = Envelope::new(\n *8735│            KIND_SPAWNED,\n  8736│            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n  8737│        );\n  8738│        send_frame(send, &frame);\n      │...\n 10261│/// Send an `error` event to the brain (best-effort).\n*10262│fn send_error(send: &SharedSend, message: &str) {\n 10263│    let frame = Envelope::new(\n 10264│        KIND_ERROR,\n 10265│        serde_json::to_value(ErrorEvent {"},"isError":false,"timestamp":1789394558402}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jawV6fpADdfzNoe6mCGQ5gut|fc_0a63d1e8a5b46c9c016aa7fe80312c87d1943c3881d875ed9a","toolName":"hub","startedAt":"2026-09-14T14:02:43.754Z","intent":"Sharing spawn boundary findings"},"id":"e36a54f6","parentId":"b44e5654","timestamp":"2026-09-14T14:02:43.754Z"}
{"type":"message","id":"c8bfc1aa","parentId":"e36a54f6","timestamp":"2026-09-14T14:02:43.755Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_jawV6fpADdfzNoe6mCGQ5gut|fc_0a63d1e8a5b46c9c016aa7fe80312c87d1943c3881d875ed9a","name":"hub","arguments":{"i":"Sharing spawn boundary findings","op":"send","to":"NetRuntimeTrace","message":"Exact broker:8672 waiter_armed then build_translation 8682-8694; translation_ready8696; sessions insert8698; clear_bringup8723; row_inserted8728; Spawned send8738. Brain spawn sends831 then takes call_deadline836 once, read_event_until loop; Split recv_timeout remaining2600. Please own build_translation direct dependencies; report every startup blocking op/budgets, no parent runtime presumed."},"intent":"Sharing spawn boundary findings"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":7501,"output":126,"cacheRead":33536,"cacheWrite":0,"totalTokens":41163,"cost":{"input":0.07501000000000001,"output":0.0063,"cacheRead":0.033535999999999996,"cacheWrite":0,"total":0.114846}},"stopReason":"toolUse","timestamp":1789394558433,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe7ea4f087d1bf9817d8d3601a14","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe80312c87d1943c3881d875ed9a","type":"function_call","status":"completed","arguments":"{\"i\":\"Sharing spawn boundary findings\",\"op\":\"send\",\"to\":\"NetRuntimeTrace\",\"message\":\"Exact broker:8672 waiter_armed then build_translation 8682-8694; translation_ready8696; sessions insert8698; clear_bringup8723; row_inserted8728; Spawned send8738. Brain spawn sends831 then takes call_deadline836 once, read_event_until loop; Split recv_timeout remaining2600. Please own build_translation direct dependencies; report every startup blocking op/budgets, no parent runtime presumed.\"}","call_id":"call_jawV6fpADdfzNoe6mCGQ5gut","internal_chat_message_metadata_passthrough":{"create_time":1789394558.945327,"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"hub","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":5319.404200002551,"ttft":1785.31740000844,"completedAt":1789394563753,"contextSnapshot":{"promptTokens":41037,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"70b10179","parentId":"c8bfc1aa","timestamp":"2026-09-14T14:02:43.757Z","message":{"role":"toolResult","toolCallId":"call_jawV6fpADdfzNoe6mCGQ5gut|fc_0a63d1e8a5b46c9c016aa7fe80312c87d1943c3881d875ed9a","toolName":"hub","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- NetRuntimeTrace: injected"}],"details":{"op":"send","from":"SpawnReplyTrace","to":"NetRuntimeTrace","receipts":[{"to":"NetRuntimeTrace","outcome":"injected"}]},"isError":false,"timestamp":1789394563756}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_b05CDK8OeHkTXfvzkNu7uHIO|fc_0a63d1e8a5b46c9c016aa7fe8d1d8887d199fbe7d578f6d628","toolName":"read","startedAt":"2026-09-14T14:02:53.176Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:6050-6110,6470-6508,8360-8560,10245-10277"},"intent":"Reading dispatch and spawn resource setup"},"id":"0238a4cd","parentId":"70b10179","timestamp":"2026-09-14T14:02:53.176Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DOGQfSmAl8VFjqioVfdODuCo|fc_0a63d1e8a5b46c9c016aa7fe8d1d9c87d191270a7b4c3d0f4a","toolName":"read","startedAt":"2026-09-14T14:02:53.176Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs:475-530,1026-1095,1200-1228"},"intent":"Reading brain framing event handling"},"id":"7243dc8f","parentId":"0238a4cd","timestamp":"2026-09-14T14:02:53.176Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IwrX1VBbxGfIHTaQb5IleM64|fc_0a63d1e8a5b46c9c016aa7fe8d1da087d19ff9ebe27e8f0eeb","toolName":"grep","startedAt":"2026-09-14T14:02:53.176Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"},"intent":"Locating connection write bounds"},"id":"49b489ec","parentId":"7243dc8f","timestamp":"2026-09-14T14:02:53.176Z"}
{"type":"message","id":"04823ab8","parentId":"49b489ec","timestamp":"2026-09-14T14:02:53.176Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_b05CDK8OeHkTXfvzkNu7uHIO|fc_0a63d1e8a5b46c9c016aa7fe8d1d8887d199fbe7d578f6d628","name":"read","arguments":{"i":"Reading dispatch and spawn resource setup","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:6050-6110,6470-6508,8360-8560,10245-10277"},"intent":"Reading dispatch and spawn resource setup"},{"type":"toolCall","id":"call_DOGQfSmAl8VFjqioVfdODuCo|fc_0a63d1e8a5b46c9c016aa7fe8d1d9c87d191270a7b4c3d0f4a","name":"read","arguments":{"i":"Reading brain framing event handling","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs:475-530,1026-1095,1200-1228"},"intent":"Reading brain framing event handling"},{"type":"toolCall","id":"call_IwrX1VBbxGfIHTaQb5IleM64|fc_0a63d1e8a5b46c9c016aa7fe8d1da087d19ff9ebe27e8f0eeb","name":"grep","arguments":{"i":"Locating connection write bounds","pattern":"fn send_frame|fn write\\(|WRITE_TIMEOUT|WRITE_DEADLINE|from_secs|fn drop|impl Drop|phase_wedge_ceiling","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","case":true,"gitignore":false,"skip":null},"intent":"Locating connection write bounds"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":355,"output":263,"cacheRead":40832,"cacheWrite":0,"totalTokens":41450,"cost":{"input":0.00355,"output":0.01315,"cacheRead":0.040832,"cacheWrite":0,"total":0.057532}},"stopReason":"toolUse","timestamp":1789394563766,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe840cbc87d1bb2e6fa64656817b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fe8d1d8887d199fbe7d578f6d628","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading dispatch and spawn resource setup\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:6050-6110,6470-6508,8360-8560,10245-10277\"}","call_id":"call_b05CDK8OeHkTXfvzkNu7uHIO","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe8d1d9c87d191270a7b4c3d0f4a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain framing event handling\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs:475-530,1026-1095,1200-1228\"}","call_id":"call_DOGQfSmAl8VFjqioVfdODuCo","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe8d1da087d19ff9ebe27e8f0eeb","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating connection write bounds\",\"pattern\":\"fn send_frame|fn write\\\\(|WRITE_TIMEOUT|WRITE_DEADLINE|from_secs|fn drop|impl Drop|phase_wedge_ceiling\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_IwrX1VBbxGfIHTaQb5IleM64","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":9408.418300002813,"ttft":9376.183500006795,"completedAt":1789394573175,"contextSnapshot":{"promptTokens":41187,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"7124d135","parentId":"04823ab8","timestamp":"2026-09-14T14:02:53.179Z","message":{"role":"toolResult","toolCallId":"call_DOGQfSmAl8VFjqioVfdODuCo|fc_0a63d1e8a5b46c9c016aa7fe8d1d9c87d191270a7b4c3d0f4a","toolName":"read","content":[{"type":"text","text":"    pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {\n…\n        Ok(Brain {\n…\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: None,\n            baseline_next_output: false,\n        })\n    }\n\n    /// Cold start in **PUMP mode**: connect, handshake, then SPLIT the stream\n    /// into the reader-thread carrier ([`BrainConn::Split`]) and arm a per-IPC-\n    /// call deadline of `io_timeout`. This is the peer pump's dedicated client —\n    /// every reply read is bounded ([`Brain::read_event_until`] via\n    /// [`Brain::call_deadline`]) so a black-holed peer can never wedge the\n    /// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a\n    /// live `Whole` after the fact — moving the `Stream` out by value to split it\n    /// is impossible behind `&mut self`), so a pump brain is split-and-bounded\n    /// for its whole life and a non-pump brain never pays the reader thread.\n    /// `trace` picks the reader's lifecycle-marker sink (REQ-RC-DISPLAY-SOLE-\n    /// WRITER): the daemon's peer pump passes [`PumpTrace::Stderr`] (its stderr\n    /// is the log), interactive clients pass [`PumpTrace::from_env`] (silent\n    /// unless a rig opts in — a client's stderr is a terminal it must not\n    /// write to from a background thread).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-RC-DISPLAY-SOLE-WRITER]\n    pub fn cold_start_pump(\n        name: &str,\n        now_ms: u64,\n        io_timeout: Duration,\n        trace: PumpTrace,\n    ) -> io::Result<Self> {\n        let mut stream = LocalSocketTransport::connect(name)?;\n        send_hello(&mut stream, Role::Brain)?;\n        Ok(Brain {\n            conn: BrainConn::split_with_reader(stream, trace),\n            generation: 0,\n            gen_start_ms: now_ms,\n            session_id: None,\n            next_seq: 0,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: Some(io_timeout),\n            baseline_next_output: false,\n        })\n    }\n\n    /// Handoff: connect, handshake, rehydrate the resume cursor + generation\n    /// from `prior`, but stamp `gen_start = now()` (NOT `prior.gen_start_ms` —\n    /// the 2.4 invariant), and re-subscribe to the session from the cursor for a\n    /// gapless resume.\n    ///\n    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n    /// integration tests) replays output through it with no later `attach`. So it\n    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n    /// ALSO subscribes from 0 over this same socket → two racing\n…\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n…\n        loop {\n…\n            match self.read_event_until(deadline)? {\n…\n                BrokerEvent::Exit { .. } => {\n…\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => {}\n            }\n        }\n    }\n\n    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n    /// entry (a whole reply-wait loop shares it, so unrelated events\n    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n    /// pump mode (blocking, unbounded).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n    pub fn call_deadline(&self) -> Option<Instant> {\n        self.io_timeout.map(|d| Instant::now() + d)\n    }\n\n    /// Deadline for a peer-reply read: `now + min(io_timeout, PEER_REPLY_READ_\n    /// BUDGET)` — decoupled from and shorter than [`Self::call_deadline`] so a\n    /// silent peer drops before the carrier deadline. `None` outside pump mode\n    /// (unbounded), same as `call_deadline`.\n    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n    pub fn reply_read_deadline(&self) -> Option<Instant> {\n        peer_reply_deadline(self.io_timeout, Instant::now())\n    }\n\n    /// Read and demux one broker event, enforcing output cursor continuity.\n    /// Each frame read is bounded by an optional wall-clock `deadline`\n    /// (pump mode passes one; everyone else passes\n    /// `None`). A frame that does not arrive before `deadline` surfaces\n    /// [`io::ErrorKind::TimedOut`] — the pump escalates that to a SUPERVISED\n    /// RESTART (a timed-out IPC read may have desynced the stream; the client\n    /// is poisoned, never retried per-peer).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n        loop {\n            let env = self.read_frame_until(deadline)?;\n            match env.kind.as_str() {\n                KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // A **resume-mode** brain (the per-session map is populated by\n                    // `resume_sessions`) routes EVERY session through the map —\n                    // including a freshly-spawned or first-seen one, which seeds\n                    // itself at its first `seq` via `entry().or_insert` (D4-2b,\n                    // doyle amendment 5: this closes the mixed-mode gap where N≥2\n                    // fresh spawns would otherwise share the single `next_seq` and\n                    // silently drop the later sessions' seq-0 output). The broker's\n                    // `Mutex<OutputLog>`-held wire order guarantees first-sight of a\n                    // new session IS seq 0, so the seed == 0 for a spawn; a resumed\n                    // session already holds its `resume_seq` seed (the `entry`\n                    // returns it, so the boundary-dedup still applies). Discipline:\n                    // **dedup-below + snap-above, NO reject-gap** — drop an\n                    // already-delivered seq (the at-least-once boundary re-send),\n                    // accept-and-snap everything `>= cursor` (the contiguous case\n                    // AND a post-eviction ring-floor jump, which the broker's\n                    // non-reordering replay makes the only legitimate forward jump).\n                    if !self.session_cursors.is_empty() {\n                        let cursor = self.session_cursors.entry(ev.session_id).or_insert(ev.seq);\n                        if ev.seq < *cursor {\n                            // Replay duplicate / boundary re-send — dedup. A FLAGGED\n                            // backward frame (a replayed old sync frame) dedups the\n                            // same way: NEVER a backward baseline — a rewind would\n                            // re-accept replayed frames (the wire-flag ruling's\n                            // forward-only pin, REQ-RC-RESIZE-PRESENTATION-BARRIER).\n                            continue;\n                        }\n…\n                    }\n…\n                }\n…\n                KIND_SEAL_CEREMONY_RESULT => {\n                    let ev: SealCeremonyResultEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::SealCeremonyResult {\n                        session_id: ev.session_id,\n                        ceremony_id: ev.ceremony_id,\n                        outcome: ev.outcome,\n                        detail: ev.detail,\n                    });\n                }\n                KIND_SPAWNED => {\n                    let ev: Spawned = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Spawned {\n                        session_id: ev.session_id,\n                        pid: ev.pid,\n                    });\n                }\n                KIND_EXIT => {\n                    let ev: ExitEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Exit {\n                        session_id: ev.session_id,\n                        code: ev.code,\n                    });\n                }\n                KIND_SUBSCRIBED => {\n                    let ev: SubscribedReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Subscribed {\n                        session_id: ev.session_id,\n…\n                    });\n                }\n…\n            }\n        }\n    }"}],"details":{"displayContent":{"text":"    pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {\n…\n        Ok(Brain {\n…\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: None,\n            baseline_next_output: false,\n        })\n    }\n\n    /// Cold start in **PUMP mode**: connect, handshake, then SPLIT the stream\n    /// into the reader-thread carrier ([`BrainConn::Split`]) and arm a per-IPC-\n    /// call deadline of `io_timeout`. This is the peer pump's dedicated client —\n    /// every reply read is bounded ([`Brain::read_event_until`] via\n    /// [`Brain::call_deadline`]) so a black-holed peer can never wedge the\n    /// single-threaded pump. The split is done at CONSTRUCTION (not flipped on a\n    /// live `Whole` after the fact — moving the `Stream` out by value to split it\n    /// is impossible behind `&mut self`), so a pump brain is split-and-bounded\n    /// for its whole life and a non-pump brain never pays the reader thread.\n    /// `trace` picks the reader's lifecycle-marker sink (REQ-RC-DISPLAY-SOLE-\n    /// WRITER): the daemon's peer pump passes [`PumpTrace::Stderr`] (its stderr\n    /// is the log), interactive clients pass [`PumpTrace::from_env`] (silent\n    /// unless a rig opts in — a client's stderr is a terminal it must not\n    /// write to from a background thread).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-RC-DISPLAY-SOLE-WRITER]\n    pub fn cold_start_pump(\n        name: &str,\n        now_ms: u64,\n        io_timeout: Duration,\n        trace: PumpTrace,\n    ) -> io::Result<Self> {\n        let mut stream = LocalSocketTransport::connect(name)?;\n        send_hello(&mut stream, Role::Brain)?;\n        Ok(Brain {\n            conn: BrainConn::split_with_reader(stream, trace),\n            generation: 0,\n            gen_start_ms: now_ms,\n            session_id: None,\n            next_seq: 0,\n            net_cursors: std::collections::HashMap::new(),\n            presence_cursor: None,\n            session_cursors: std::collections::HashMap::new(),\n            io_timeout: Some(io_timeout),\n            baseline_next_output: false,\n        })\n    }\n\n    /// Handoff: connect, handshake, rehydrate the resume cursor + generation\n    /// from `prior`, but stamp `gen_start = now()` (NOT `prior.gen_start_ms` —\n    /// the 2.4 invariant), and re-subscribe to the session from the cursor for a\n    /// gapless resume.\n    ///\n    /// The eager `subscribe` IS the resume mechanism — every standalone handoff\n    /// caller (the brain-only update engine, `idempotent`/`daemon_e2e`/`handoff`\n    /// integration tests) replays output through it with no later `attach`. So it\n    /// stays. The P1c controller-writer reorder (a `serve_attach` re-serve that\n    /// ALSO subscribes from 0 over this same socket → two racing\n…\n    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {\n…\n        loop {\n…\n            match self.read_event_until(deadline)? {\n…\n                BrokerEvent::Exit { .. } => {\n…\n                }\n                BrokerEvent::Error { message } => {\n                    return Err(io::Error::other(message));\n                }\n                _ => {}\n            }\n        }\n    }\n\n    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call\n    /// entry (a whole reply-wait loop shares it, so unrelated events\n    /// dripping in cannot reset it — the total-wait bound). `None` when not in\n    /// pump mode (blocking, unbounded).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n    pub fn call_deadline(&self) -> Option<Instant> {\n        self.io_timeout.map(|d| Instant::now() + d)\n    }\n\n    /// Deadline for a peer-reply read: `now + min(io_timeout, PEER_REPLY_READ_\n    /// BUDGET)` — decoupled from and shorter than [`Self::call_deadline`] so a\n    /// silent peer drops before the carrier deadline. `None` outside pump mode\n    /// (unbounded), same as `call_deadline`.\n    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n    pub fn reply_read_deadline(&self) -> Option<Instant> {\n        peer_reply_deadline(self.io_timeout, Instant::now())\n    }\n\n    /// Read and demux one broker event, enforcing output cursor continuity.\n    /// Each frame read is bounded by an optional wall-clock `deadline`\n    /// (pump mode passes one; everyone else passes\n    /// `None`). A frame that does not arrive before `deadline` surfaces\n    /// [`io::ErrorKind::TimedOut`] — the pump escalates that to a SUPERVISED\n    /// RESTART (a timed-out IPC read may have desynced the stream; the client\n    /// is poisoned, never retried per-peer).\n    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]\n    // [impl->REQ-BRAIN-READ-BOUNDED-PER-CALL]\n    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n        loop {\n            let env = self.read_frame_until(deadline)?;\n            match env.kind.as_str() {\n                KIND_OUTPUT => {\n                    let ev: OutputEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    // A **resume-mode** brain (the per-session map is populated by\n                    // `resume_sessions`) routes EVERY session through the map —\n                    // including a freshly-spawned or first-seen one, which seeds\n                    // itself at its first `seq` via `entry().or_insert` (D4-2b,\n                    // doyle amendment 5: this closes the mixed-mode gap where N≥2\n                    // fresh spawns would otherwise share the single `next_seq` and\n                    // silently drop the later sessions' seq-0 output). The broker's\n                    // `Mutex<OutputLog>`-held wire order guarantees first-sight of a\n                    // new session IS seq 0, so the seed == 0 for a spawn; a resumed\n                    // session already holds its `resume_seq` seed (the `entry`\n                    // returns it, so the boundary-dedup still applies). Discipline:\n                    // **dedup-below + snap-above, NO reject-gap** — drop an\n                    // already-delivered seq (the at-least-once boundary re-send),\n                    // accept-and-snap everything `>= cursor` (the contiguous case\n                    // AND a post-eviction ring-floor jump, which the broker's\n                    // non-reordering replay makes the only legitimate forward jump).\n                    if !self.session_cursors.is_empty() {\n                        let cursor = self.session_cursors.entry(ev.session_id).or_insert(ev.seq);\n                        if ev.seq < *cursor {\n                            // Replay duplicate / boundary re-send — dedup. A FLAGGED\n                            // backward frame (a replayed old sync frame) dedups the\n                            // same way: NEVER a backward baseline — a rewind would\n                            // re-accept replayed frames (the wire-flag ruling's\n                            // forward-only pin, REQ-RC-RESIZE-PRESENTATION-BARRIER).\n                            continue;\n                        }\n…\n                    }\n…\n                }\n…\n                KIND_SEAL_CEREMONY_RESULT => {\n                    let ev: SealCeremonyResultEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::SealCeremonyResult {\n                        session_id: ev.session_id,\n                        ceremony_id: ev.ceremony_id,\n                        outcome: ev.outcome,\n                        detail: ev.detail,\n                    });\n                }\n                KIND_SPAWNED => {\n                    let ev: Spawned = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Spawned {\n                        session_id: ev.session_id,\n                        pid: ev.pid,\n                    });\n                }\n                KIND_EXIT => {\n                    let ev: ExitEvent = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Exit {\n                        session_id: ev.session_id,\n                        code: ev.code,\n                    });\n                }\n                KIND_SUBSCRIBED => {\n                    let ev: SubscribedReply = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::Subscribed {\n                        session_id: ev.session_id,\n…\n                    });\n                }\n…\n            }\n        }\n    }","startLine":466,"lineNumbers":[466,null,469,null,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,null,1012,null,1015,null,1019,null,1021,null,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,null,1104,null,1153,null,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,null,1231,1232,null,1376,1377,1378]},"fileSize":171079,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1789394573179}}
{"type":"message","id":"7ebb0307","parentId":"7124d135","timestamp":"2026-09-14T14:02:53.184Z","message":{"role":"toolResult","toolCallId":"call_b05CDK8OeHkTXfvzkNu7uHIO|fc_0a63d1e8a5b46c9c016aa7fe8d1d8887d199fbe7d578f6d628","toolName":"read","content":[{"type":"text","text":"impl Broker {\n…\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        // Streams THIS conn opened as ConnectionBound (ADR-0040 decision 6):\n        // they die with the conn — the exit sweep FINs each toward its target\n        // and terminal-retires the local row. Durable rows never enter it.\n        // [impl->REQ-STREAM-LEASE-CLASSES]\n        let mut my_cb_streams: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                // Fresh/RejectExisting spawn (ADR-0041 decision 5): a typed\n                // conflict reply carries no session — only a real Created\n                // auto-subscribes. TEST-RIG-ONLY valve (the N-1 loud-fail leg):\n                // with SPT_TEST_SPAWN_FRESH_UNSUPPORTED set this broker answers\n                // EXACTLY like an N-1 broker that predates the verb — the\n                // forward-compat unknown-kind reply, byte-identical. Never set\n                // in production.\n                // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n                KIND_SPAWN_FRESH => {\n                    if std::env::var(\"SPT_TEST_SPAWN_FRESH_UNSUPPORTED\").is_ok() {\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\n                    } else {\n                        match self.dispatch_spawn_fresh(env, &send) {\n                            Ok(Some(id)) => my_subs.push(id),\n                            Ok(None) => {} // typed conflict already sent\n…\n                        }\n                    }\n                }\n…\n            }\n        }\n…\n        if let Some(host) = self.net.get() {\n…\n            for id in &my_cb_streams {\n…\n                spt_proto::emit_line_err!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n            if my_presence_sub {\n                host.detach_presence_sub(&send);\n            }\n        }\n        Ok(())\n    }\n\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\n    /// exactly this isolation (one owner's hung shell binary stalls neither\n    /// another owner's delivery nor the broker's control dispatch).\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        self.dispatch_spawn_policy(req, send, false)\n            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\n    }\n\n    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\n    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\n    /// but an endpoint that already holds a live session answers the TYPED\n    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\n    /// conflict was sent (no session to auto-subscribe).\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        self.dispatch_spawn_policy(req, send, true)\n    }\n\n    /// `bring-up` — the engine room's entrance (releases#71).\n    ///\n    /// The gate this runs is the SAME one the seat resolver runs; what is new is\n…\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\n        spawn_phase(\"pty_creating\", &format!(\"program={}\", req.program));\n        // INT-RIG SEAM (#235): hold the winner inside `pty_creating` for a set time,\n        // reproducing the measured shape — a ConPTY create that overruns while the\n        // holder is perfectly alive. One long phase with no new stamp is exactly what\n        // the production slow case looks like to the gate, so the rig drives the real\n        // decision path rather than a simulation of it.\n        // Compiled out of release entirely (see `phase_wedge_ceiling`'s seam): a stray\n        // value must never be able to stall a production spawn, and a seam that can only\n        // be reached in a debug build cannot. Parsed once, clamped, and LOUD every time\n        // it fires — a silent delay is the failure mode worth engineering against.\n        #[cfg(debug_assertions)]\n        {\n            static STRETCH: std::sync::OnceLock<Option<Duration>> = std::sync::OnceLock::new();\n            if let Some(d) = STRETCH.get_or_init(|| {\n                let raw = std::env::var(\"SPT_SPAWN_STRETCH_MS\")\n                    .ok()\n                    .and_then(|v| v.parse::<u64>().ok())?;\n                Some(Duration::from_millis(raw.min(30_000)))\n            }) {\n                eprintln!(\n                    \"SPAWN_STRETCH_SEAM:{}: holding pty_creating for {}ms — test seam, \\\n                     debug builds only\",\n                    req.endpoint,\n                    d.as_millis()\n                );\n                thread::sleep(*d);\n            }\n        }\n        let session = match PtySession::spawn_program_in_env(\n            &req.program,\n            &req.args,\n            SurfaceSize::new(req.rows, req.cols),\n            req.cwd.as_deref(),\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\n        ) {\n            Ok(session) => session,\n            Err(e) => {\n                spawn_phase(\"pty_failed\", &format!(\"err={e}\"));\n                return Err(format!(\"spawn failed: {e}\"));\n            }\n        };\n        let session = Arc::new(session);\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\n        let pid = session.process_id();\n        // Birth identity is trustworthy here: Windows retains the child handle;\n        // Unix cannot recycle our child's pid until it is reaped, and no waiter\n        // has been started yet. Exec may change its image, not its birth stamp.\n        // [impl->REQ-BROKER-ZOMBIE-IDENTITY]\n        let process_started_at = pid.and_then(spt_store::proc::process_started_at);\n        // The ConPTY-create span releases#199 candidate C names, closed.\n        spawn_phase(\"pty_created\", &format!(\"pid={pid:?}\"));\n\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\n        // paste burst parks only this thread — never the broker dispatch thread\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\n        // too, so the lone writer enforces the W2 inject-floor order.\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\n        // releases#199: the span from here to `row_inserted` is the one a 10-way\n        // contention run put 1427ms into while the ConPTY create took 14ms — the\n        // child is ALREADY RUNNING through all of it and the row the bring-up poll\n        // reads does not exist yet, which is this issue's face in miniature. So it\n        // is stamped per step rather than as one block.\n        spawn_phase(\"input_writer\", \"\");\n\n        // The output log, with the spawning connection pre-attached as the\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\n        // is missed before an explicit subscribe — and so a remote attach to a\n        // spawned-but-undriven session takes control silently (no loud displace\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\n        let log = Arc::new(Mutex::new(OutputLog::new(\n            id,\n            self.log_cap_chunks,\n            req.endpoint.clone(),\n            (req.rows, req.cols),\n        )));\n        log.lock()\n            .unwrap()\n            .become_controller(Arc::clone(send), None, 0, 0);\n        spawn_phase(\"log_ready\", \"\");\n\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n        // then deliver to the controller OFF the lock with a bounded deadline\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n        // longer inline under the log lock, so a backed-up controller can never\n        // wedge the session; a fully-stalled controller is detached after the\n        // deadline (bounded wedge, never permanent).\n        let log_drain = Arc::clone(&log);\n        // DSR cursor authority (REQ-DSR-SINGLE-CPR): the drain answers each DSR\n        // with the grid's real display-column cursor. The provider runs on the\n        // drain thread BETWEEN sink handoffs (the append lock is released), and\n        // the grid is advanced by that same thread, so the reported position is\n        // exact at the query point.\n        let log_cursor = Arc::clone(&log);\n        let drain = session\n            .drain_with_cursor(\n                move |chunk| {\n                    // OBS RIDER Tap A (debug-gated, default OFF): the raw pre-parse\n                    // child output, captured before append()/grid.advance. No-op in a\n                    // release build and when SPT_OBS_BYTE_TAP is unset.\n                    spt_store::obstap::tap(id, \"raw\", chunk);\n                    // append() does the controller handoff inline now (one non-blocking\n                    // try_send — b4) and returns Some(epoch) ONLY when the controller is\n                    // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n                    // the first lock (a fresh lock, no self-deadlock). The drain never\n                    // sleeps here, so a slow controller can't throttle the viewer fan-out.\n                    let evict = recover_log(&log_drain).append(chunk);\n                    if let Some(epoch) = evict {\n                        recover_log(&log_drain).mark_controller_gone(epoch);\n                    }\n                },\n                Box::new(move || recover_log(&log_cursor).grid_cursor()),\n            )\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\n        spawn_phase(\"drain_armed\", \"\");\n\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n        // session is removed from the table. Runs on its own thread; `wait`\n        // holds the child lock but `kill` uses a split killer.\n        let sessions_exit = Arc::clone(&self.sessions);\n        let log_exit = Arc::clone(&log);\n        let waiter = Arc::clone(&session);\n        // Carried for the Err arm's diagnostic below: a failed wait must name\n        // the session it belonged to, not just the error.\n        let exit_endpoint = req.endpoint.clone();\n        // TEST-RIG-ONLY FAULT INJECTION. Never set in production.\n        //\n        // NOT the same kind of valve as this file's two siblings\n        // (SPT_TEST_SPAWN_FRESH_UNSUPPORTED / SPT_TEST_STREAM_OPENER_UNSUPPORTED):\n        // those make this broker answer like an OLDER broker — a state that\n        // genuinely exists in the fleet — whereas this one makes it a BROKEN\n        // one. It SUPPRESSES a correctness mechanism, so tripping it in the\n        // field would leak sessions, not merely degrade a feature.\n        //\n        // THE FIELD CONDITION IT REPRODUCES: dead root + SURVIVING record —\n        // the wedged/missed exit-wait behind hertz's zombie rows, and the state\n        // `zombie_verdict`'s PRIMARY arm (\"dead root, surviving record\") exists\n        // to classify. A healthy waiter removes the row within milliseconds, so\n        // that arm is otherwise unreachable from any rig — which is how it went\n        // this long fed by an unsound oracle with nothing able to catch it.\n        //\n        // It SUPPRESSES ONLY: no row is fabricated, no verdict faked. The test\n        // observes the genuine state or it observes nothing.\n        // [impl->REQ-LIVENESS-ORACLE-SOUND]\n        let waiter_suppressed = std::env::var(\"SPT_TEST_EXIT_WAITER_SUPPRESSED\").is_ok();\n        // TEST-RIG-ONLY FAULT INJECTION. Never set in production.\n        //\n        // The NARROW sibling of the valve above, and it exists because that one\n        // is too WIDE for the face it would be borrowed for (releases#201): a\n        // suppressed waiter never removes the row, never quiesces the drain and\n        // never unlatches the perch, which is a DIFFERENT field state — dead\n        // root plus SURVIVING record. #201 is the opposite shape: the reap\n        // COMPLETES, the session is gone from the table and the perch is\n        // unlatched, and the attached terminal is simply never told.\n        //\n        // THE FIELD CONDITION IT REPRODUCES: `PtySession::wait` is\n        // `child.wait().map_err(SurfaceError::Io)`, so ANY Io error on the child\n        // handle takes the Err arm below — the kill-versus-natural reap race is\n        // the shape it takes in the field, our own best-effort kill contending\n        // with the child exiting on its own. That arm gates the whole Exit\n        // emission, so the terminal frame is dropped while every other teardown\n        // step still runs.\n        //\n        // It FALSIFIES ONE OUTCOME, IT SKIPS NOTHING: the wait below is still\n        // performed and still blocks until the child is genuinely dead, so a rig\n        // observes the real teardown with a real reap — only the status it\n        // yielded is replaced. No row is fabricated and no step is bypassed.\n        //\n        // Instrument for REQ-BROKER-EXIT-EVERY-REAP; the requirement is carried\n        // by the emission below, not by this valve.\n        let wait_err_injected = std::env::var(\"SPT_TEST_EXIT_WAIT_ERR\").is_ok();\n        thread::spawn(move || {\n            if waiter_suppressed {\n                // Never wait, never remove the row: the session stays tabled\n                // over a root that is about to die. The rig that set this owns\n                // the reap this thread would have performed.\n                return;\n            }\n            let status = waiter.wait();\n            // Replace the outcome AFTER the real wait returns: the child is\n            // reaped either way, so the injected Err costs the rig nothing but\n            // the status itself.\n            let status = if wait_err_injected {\n                Err(spt_term::SurfaceError::Io(io::Error::other(\n                    \"SPT_TEST_EXIT_WAIT_ERR\",\n                )))\n            } else {\n                status\n            };\n            // Reap the dead session from the table FIRST (ADR-0041 decision 4\n            // ordering): once removed, no NEW KIND_SESSIONS snapshot can see\n            // this session at all. Clearing before removal left a window where\n            // a poll snapshotted AFTER the generation bump — same generation,\n            // controller slot still occupied in the log — and legitimately\n            // re-wrote `controlled=true` onto the perch of a session that was\n            // about to vanish (the relatch the W2 int leg caught live). The\n            // removed entry hands back the DRAIN handle the ordering below needs.\n            let removed = recover(&sessions_exit).remove(&id);\n            // releases#199 instrument, THE FOURTH CANDIDATE the A/B/C set did not\n            // name: a harness that spawns and dies immediately produces a row that\n…\n        });\n…\n    fn dispatch_adapter_apply(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n    }\n}\n\n/// The default broker-owned effect-journal path: `<spt_home>/daemon-effects.log`.\npub(crate) fn default_journal_path() -> std::path::PathBuf {\n    spt_store::perch::spt_home().join(\"daemon-effects.log\")\n}\n\n/// Write a frame to the brain, ignoring failures (brain gone / conn poisoned).\n/// Bounded + cancelable (round-4): a dispatch reply behind a wedged conn fails\n/// at the deadline instead of parking the dispatch thread.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\nfn send_frame(send: &SharedSend, frame: &Envelope) {\n    let _ = send.write(frame);\n}\n\n/// Send an `error` event to the brain (best-effort).\nfn send_error(send: &SharedSend, message: &str) {\n    let frame = Envelope::new(\n        KIND_ERROR,\n        serde_json::to_value(ErrorEvent {\n            message: message.to_string(),\n        })\n        .expect(\"ErrorEvent serializes\"),\n    );\n    send_frame(send, &frame);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::msg::KIND_VIEWER_EVICTED;\n\n…\n}"}],"details":{"displayContent":{"text":"impl Broker {\n…\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\n        struct HandlerTally<'a>(&'a AtomicUsize);\n        impl Drop for HandlerTally<'_> {\n            fn drop(&mut self) {\n                self.0.fetch_sub(1, Ordering::AcqRel);\n            }\n        }\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\n        let _tally = HandlerTally(&self.conn_handlers);\n\n        let (mut recv, send) = conn.split();\n        // Round-4: the send half is owned by the bounded/cancelable conn\n        // primitive from birth — every write on this physical connection is\n        // deadline-bounded, and a wedge poisons + retires the conn.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\n        send.describe(\"role=brain\");\n\n        // Version handshake: this connection must be a brain (2.3).\n        recv_hello(&mut recv, Role::Brain)?;\n\n        let mut my_subs: Vec<u64> = Vec::new();\n        let mut my_stream_subs: Vec<u64> = Vec::new();\n        // Streams THIS conn opened as ConnectionBound (ADR-0040 decision 6):\n        // they die with the conn — the exit sweep FINs each toward its target\n        // and terminal-retires the local row. Durable rows never enter it.\n        // [impl->REQ-STREAM-LEASE-CLASSES]\n        let mut my_cb_streams: Vec<u64> = Vec::new();\n        let mut my_presence_sub = false;\n\n        loop {\n            let env = match read_frame(&mut recv) {\n                Ok(e) => e,\n                // Any read failure (EOF included) ends the connection.\n                Err(_) => break,\n            };\n            match env.kind.as_str() {\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                // Fresh/RejectExisting spawn (ADR-0041 decision 5): a typed\n                // conflict reply carries no session — only a real Created\n                // auto-subscribes. TEST-RIG-ONLY valve (the N-1 loud-fail leg):\n                // with SPT_TEST_SPAWN_FRESH_UNSUPPORTED set this broker answers\n                // EXACTLY like an N-1 broker that predates the verb — the\n                // forward-compat unknown-kind reply, byte-identical. Never set\n                // in production.\n                // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n                KIND_SPAWN_FRESH => {\n                    if std::env::var(\"SPT_TEST_SPAWN_FRESH_UNSUPPORTED\").is_ok() {\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\n                    } else {\n                        match self.dispatch_spawn_fresh(env, &send) {\n                            Ok(Some(id)) => my_subs.push(id),\n                            Ok(None) => {} // typed conflict already sent\n…\n                        }\n                    }\n                }\n…\n            }\n        }\n…\n        if let Some(host) = self.net.get() {\n…\n            for id in &my_cb_streams {\n…\n                spt_proto::emit_line_err!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\n            }\n            // And presence: the liveness log + its ring persist (D4c).\n            if my_presence_sub {\n                host.detach_presence_sub(&send);\n            }\n        }\n        Ok(())\n    }\n\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\n    /// exactly this isolation (one owner's hung shell binary stalls neither\n    /// another owner's delivery nor the broker's control dispatch).\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        self.dispatch_spawn_policy(req, send, false)\n            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\n    }\n\n    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\n    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\n    /// but an endpoint that already holds a live session answers the TYPED\n    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\n    /// conflict was sent (no session to auto-subscribe).\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        self.dispatch_spawn_policy(req, send, true)\n    }\n\n    /// `bring-up` — the engine room's entrance (releases#71).\n    ///\n    /// The gate this runs is the SAME one the seat resolver runs; what is new is\n…\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\n        spawn_phase(\"pty_creating\", &format!(\"program={}\", req.program));\n        // INT-RIG SEAM (#235): hold the winner inside `pty_creating` for a set time,\n        // reproducing the measured shape — a ConPTY create that overruns while the\n        // holder is perfectly alive. One long phase with no new stamp is exactly what\n        // the production slow case looks like to the gate, so the rig drives the real\n        // decision path rather than a simulation of it.\n        // Compiled out of release entirely (see `phase_wedge_ceiling`'s seam): a stray\n        // value must never be able to stall a production spawn, and a seam that can only\n        // be reached in a debug build cannot. Parsed once, clamped, and LOUD every time\n        // it fires — a silent delay is the failure mode worth engineering against.\n        #[cfg(debug_assertions)]\n        {\n            static STRETCH: std::sync::OnceLock<Option<Duration>> = std::sync::OnceLock::new();\n            if let Some(d) = STRETCH.get_or_init(|| {\n                let raw = std::env::var(\"SPT_SPAWN_STRETCH_MS\")\n                    .ok()\n                    .and_then(|v| v.parse::<u64>().ok())?;\n                Some(Duration::from_millis(raw.min(30_000)))\n            }) {\n                eprintln!(\n                    \"SPAWN_STRETCH_SEAM:{}: holding pty_creating for {}ms — test seam, \\\n                     debug builds only\",\n                    req.endpoint,\n                    d.as_millis()\n                );\n                thread::sleep(*d);\n            }\n        }\n        let session = match PtySession::spawn_program_in_env(\n            &req.program,\n            &req.args,\n            SurfaceSize::new(req.rows, req.cols),\n            req.cwd.as_deref(),\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\n        ) {\n            Ok(session) => session,\n            Err(e) => {\n                spawn_phase(\"pty_failed\", &format!(\"err={e}\"));\n                return Err(format!(\"spawn failed: {e}\"));\n            }\n        };\n        let session = Arc::new(session);\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\n        let pid = session.process_id();\n        // Birth identity is trustworthy here: Windows retains the child handle;\n        // Unix cannot recycle our child's pid until it is reaped, and no waiter\n        // has been started yet. Exec may change its image, not its birth stamp.\n        // [impl->REQ-BROKER-ZOMBIE-IDENTITY]\n        let process_started_at = pid.and_then(spt_store::proc::process_started_at);\n        // The ConPTY-create span releases#199 candidate C names, closed.\n        spawn_phase(\"pty_created\", &format!(\"pid={pid:?}\"));\n\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\n        // paste burst parks only this thread — never the broker dispatch thread\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\n        // too, so the lone writer enforces the W2 inject-floor order.\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\n        // releases#199: the span from here to `row_inserted` is the one a 10-way\n        // contention run put 1427ms into while the ConPTY create took 14ms — the\n        // child is ALREADY RUNNING through all of it and the row the bring-up poll\n        // reads does not exist yet, which is this issue's face in miniature. So it\n        // is stamped per step rather than as one block.\n        spawn_phase(\"input_writer\", \"\");\n\n        // The output log, with the spawning connection pre-attached as the\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\n        // is missed before an explicit subscribe — and so a remote attach to a\n        // spawned-but-undriven session takes control silently (no loud displace\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\n        let log = Arc::new(Mutex::new(OutputLog::new(\n            id,\n            self.log_cap_chunks,\n            req.endpoint.clone(),\n            (req.rows, req.cols),\n        )));\n        log.lock()\n            .unwrap()\n            .become_controller(Arc::clone(send), None, 0, 0);\n        spawn_phase(\"log_ready\", \"\");\n\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n        // then deliver to the controller OFF the lock with a bounded deadline\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n        // longer inline under the log lock, so a backed-up controller can never\n        // wedge the session; a fully-stalled controller is detached after the\n        // deadline (bounded wedge, never permanent).\n        let log_drain = Arc::clone(&log);\n        // DSR cursor authority (REQ-DSR-SINGLE-CPR): the drain answers each DSR\n        // with the grid's real display-column cursor. The provider runs on the\n        // drain thread BETWEEN sink handoffs (the append lock is released), and\n        // the grid is advanced by that same thread, so the reported position is\n        // exact at the query point.\n        let log_cursor = Arc::clone(&log);\n        let drain = session\n            .drain_with_cursor(\n                move |chunk| {\n                    // OBS RIDER Tap A (debug-gated, default OFF): the raw pre-parse\n                    // child output, captured before append()/grid.advance. No-op in a\n                    // release build and when SPT_OBS_BYTE_TAP is unset.\n                    spt_store::obstap::tap(id, \"raw\", chunk);\n                    // append() does the controller handoff inline now (one non-blocking\n                    // try_send — b4) and returns Some(epoch) ONLY when the controller is\n                    // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n                    // the first lock (a fresh lock, no self-deadlock). The drain never\n                    // sleeps here, so a slow controller can't throttle the viewer fan-out.\n                    let evict = recover_log(&log_drain).append(chunk);\n                    if let Some(epoch) = evict {\n                        recover_log(&log_drain).mark_controller_gone(epoch);\n                    }\n                },\n                Box::new(move || recover_log(&log_cursor).grid_cursor()),\n            )\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\n        spawn_phase(\"drain_armed\", \"\");\n\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n        // session is removed from the table. Runs on its own thread; `wait`\n        // holds the child lock but `kill` uses a split killer.\n        let sessions_exit = Arc::clone(&self.sessions);\n        let log_exit = Arc::clone(&log);\n        let waiter = Arc::clone(&session);\n        // Carried for the Err arm's diagnostic below: a failed wait must name\n        // the session it belonged to, not just the error.\n        let exit_endpoint = req.endpoint.clone();\n        // TEST-RIG-ONLY FAULT INJECTION. Never set in production.\n        //\n        // NOT the same kind of valve as this file's two siblings\n        // (SPT_TEST_SPAWN_FRESH_UNSUPPORTED / SPT_TEST_STREAM_OPENER_UNSUPPORTED):\n        // those make this broker answer like an OLDER broker — a state that\n        // genuinely exists in the fleet — whereas this one makes it a BROKEN\n        // one. It SUPPRESSES a correctness mechanism, so tripping it in the\n        // field would leak sessions, not merely degrade a feature.\n        //\n        // THE FIELD CONDITION IT REPRODUCES: dead root + SURVIVING record —\n        // the wedged/missed exit-wait behind hertz's zombie rows, and the state\n        // `zombie_verdict`'s PRIMARY arm (\"dead root, surviving record\") exists\n        // to classify. A healthy waiter removes the row within milliseconds, so\n        // that arm is otherwise unreachable from any rig — which is how it went\n        // this long fed by an unsound oracle with nothing able to catch it.\n        //\n        // It SUPPRESSES ONLY: no row is fabricated, no verdict faked. The test\n        // observes the genuine state or it observes nothing.\n        // [impl->REQ-LIVENESS-ORACLE-SOUND]\n        let waiter_suppressed = std::env::var(\"SPT_TEST_EXIT_WAITER_SUPPRESSED\").is_ok();\n        // TEST-RIG-ONLY FAULT INJECTION. Never set in production.\n        //\n        // The NARROW sibling of the valve above, and it exists because that one\n        // is too WIDE for the face it would be borrowed for (releases#201): a\n        // suppressed waiter never removes the row, never quiesces the drain and\n        // never unlatches the perch, which is a DIFFERENT field state — dead\n        // root plus SURVIVING record. #201 is the opposite shape: the reap\n        // COMPLETES, the session is gone from the table and the perch is\n        // unlatched, and the attached terminal is simply never told.\n        //\n        // THE FIELD CONDITION IT REPRODUCES: `PtySession::wait` is\n        // `child.wait().map_err(SurfaceError::Io)`, so ANY Io error on the child\n        // handle takes the Err arm below — the kill-versus-natural reap race is\n        // the shape it takes in the field, our own best-effort kill contending\n        // with the child exiting on its own. That arm gates the whole Exit\n        // emission, so the terminal frame is dropped while every other teardown\n        // step still runs.\n        //\n        // It FALSIFIES ONE OUTCOME, IT SKIPS NOTHING: the wait below is still\n        // performed and still blocks until the child is genuinely dead, so a rig\n        // observes the real teardown with a real reap — only the status it\n        // yielded is replaced. No row is fabricated and no step is bypassed.\n        //\n        // Instrument for REQ-BROKER-EXIT-EVERY-REAP; the requirement is carried\n        // by the emission below, not by this valve.\n        let wait_err_injected = std::env::var(\"SPT_TEST_EXIT_WAIT_ERR\").is_ok();\n        thread::spawn(move || {\n            if waiter_suppressed {\n                // Never wait, never remove the row: the session stays tabled\n                // over a root that is about to die. The rig that set this owns\n                // the reap this thread would have performed.\n                return;\n            }\n            let status = waiter.wait();\n            // Replace the outcome AFTER the real wait returns: the child is\n            // reaped either way, so the injected Err costs the rig nothing but\n            // the status itself.\n            let status = if wait_err_injected {\n                Err(spt_term::SurfaceError::Io(io::Error::other(\n                    \"SPT_TEST_EXIT_WAIT_ERR\",\n                )))\n            } else {\n                status\n            };\n            // Reap the dead session from the table FIRST (ADR-0041 decision 4\n            // ordering): once removed, no NEW KIND_SESSIONS snapshot can see\n            // this session at all. Clearing before removal left a window where\n            // a poll snapshotted AFTER the generation bump — same generation,\n            // controller slot still occupied in the log — and legitimately\n            // re-wrote `controlled=true` onto the perch of a session that was\n            // about to vanish (the relatch the W2 int leg caught live). The\n            // removed entry hands back the DRAIN handle the ordering below needs.\n            let removed = recover(&sessions_exit).remove(&id);\n            // releases#199 instrument, THE FOURTH CANDIDATE the A/B/C set did not\n            // name: a harness that spawns and dies immediately produces a row that\n…\n        });\n…\n    fn dispatch_adapter_apply(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n…\n    }\n}\n\n/// The default broker-owned effect-journal path: `<spt_home>/daemon-effects.log`.\npub(crate) fn default_journal_path() -> std::path::PathBuf {\n    spt_store::perch::spt_home().join(\"daemon-effects.log\")\n}\n\n/// Write a frame to the brain, ignoring failures (brain gone / conn poisoned).\n/// Bounded + cancelable (round-4): a dispatch reply behind a wedged conn fails\n/// at the deadline instead of parking the dispatch thread.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\nfn send_frame(send: &SharedSend, frame: &Envelope) {\n    let _ = send.write(frame);\n}\n\n/// Send an `error` event to the brain (best-effort).\nfn send_error(send: &SharedSend, message: &str) {\n    let frame = Envelope::new(\n        KIND_ERROR,\n        serde_json::to_value(ErrorEvent {\n            message: message.to_string(),\n        })\n        .expect(\"ErrorEvent serializes\"),\n    );\n    send_frame(send, &frame);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::msg::KIND_VIEWER_EVICTED;\n\n…\n}","startLine":5627,"lineNumbers":[5627,null,6050,6051,6052,6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064,6065,6066,6067,6068,6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080,6081,6082,6083,6084,6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096,6097,6098,6099,6100,6101,6102,6103,6104,6105,6106,6107,6108,6109,6110,null,6112,6113,6114,null,6423,6424,null,6450,null,6467,null,6470,6471,6472,6473,6474,6475,6476,6477,6478,6479,6480,6481,6482,6483,6484,6485,6486,6487,6488,6489,6490,6491,6492,6493,6494,6495,6496,6497,6498,6499,6500,6501,6502,6503,6504,6505,6506,6507,6508,null,8360,8361,8362,8363,8364,8365,8366,8367,8368,8369,8370,8371,8372,8373,8374,8375,8376,8377,8378,8379,8380,8381,8382,8383,8384,8385,8386,8387,8388,8389,8390,8391,8392,8393,8394,8395,8396,8397,8398,8399,8400,8401,8402,8403,8404,8405,8406,8407,8408,8409,8410,8411,8412,8413,8414,8415,8416,8417,8418,8419,8420,8421,8422,8423,8424,8425,8426,8427,8428,8429,8430,8431,8432,8433,8434,8435,8436,8437,8438,8439,8440,8441,8442,8443,8444,8445,8446,8447,8448,8449,8450,8451,8452,8453,8454,8455,8456,8457,8458,8459,8460,8461,8462,8463,8464,8465,8466,8467,8468,8469,8470,8471,8472,8473,8474,8475,8476,8477,8478,8479,8480,8481,8482,8483,8484,8485,8486,8487,8488,8489,8490,8491,8492,8493,8494,8495,8496,8497,8498,8499,8500,8501,8502,8503,8504,8505,8506,8507,8508,8509,8510,8511,8512,8513,8514,8515,8516,8517,8518,8519,8520,8521,8522,8523,8524,8525,8526,8527,8528,8529,8530,8531,8532,8533,8534,8535,8536,8537,8538,8539,8540,8541,8542,8543,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554,8555,8556,8557,8558,8559,8560,null,8670,null,10086,null,10245,10246,10247,10248,10249,10250,10251,10252,10253,10254,10255,10256,10257,10258,10259,10260,10261,10262,10263,10264,10265,10266,10267,10268,10269,10270,10271,10272,10273,10274,10275,10276,10277,null,16852]},"fileSize":877537,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1789394573184}}
{"type":"message","id":"35574012","parentId":"7ebb0307","timestamp":"2026-09-14T14:02:53.198Z","message":{"role":"toolResult","toolCallId":"call_IwrX1VBbxGfIHTaQb5IleM64|fc_0a63d1e8a5b46c9c016aa7fe8d1da087d19ff9ebe27e8f0eeb","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## conn.rs\n 124|\n*125|const HEALTHY_LOG_INTERVAL: Duration = Duration::from_secs(60);\n 126|\n 127|struct HealthyWindow {\n 128|    since: Instant,\n...\n 525|    /// deadline (gate-wait + OS write completion) — the broker passes its\n*526|    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n 527|    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n 528|        static LOG: LazyLock<Arc<LifecycleLog>> = LazyLock::new(|| Arc::new(LifecycleLog::new()));\n 529|        Self::with_log(half, bound, Arc::clone(&LOG))\n...\n 598|    /// Bounded framed write. See [`Self::write_gated`].\n*599|    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n 600|        self.write_gated(env, || true)\n 601|    }\n 602|\n...\n 721|#[cfg(test)]\n*722|impl Drop for TestGatePin {\n*723|    fn drop(&mut self) {\n 724|        let mut g = recover(&self.inner.gate);\n 725|        g.half = self.half.take();\n 726|        drop(g);\n...\n 745|\n*746|impl Drop for BrokerConn {\n*747|    fn drop(&mut self) {\n 748|        // Only a healthy physical close is counted instead of logged per conn.\n 749|        // Error/timeout closes keep their full attribution and original start.\n 750|        // [impl->REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\n## brain.rs\n 2637|/// full 30s `PUMP_PEER_IO_TIMEOUT`).\n*2638|pub(crate) const PEER_REPLY_READ_BUDGET: Duration = Duration::from_secs(10);\n 2639|\n 2640|/// A verb whose reply read MUST be able to time out refuses an unbounded carrier\n 2641|/// here, before any wire I/O, instead of documenting the requirement and hoping.\n...\n 2714|        // Pump mode, 30s carrier: reply-read capped to the 10s budget.\n*2715|        let d = peer_reply_deadline(Some(Duration::from_secs(30)), now).expect(\"pump = Some\");\n 2716|        assert_eq!(d, now + PEER_REPLY_READ_BUDGET, \"capped at the 10s budget, not 30s\");\n*2717|        assert!(d < now + Duration::from_secs(30), \"strictly before the carrier deadline\");\n 2718|        // A carrier timeout SHORTER than the budget wins the min (never exceed it).\n*2719|        let short = peer_reply_deadline(Some(Duration::from_secs(3)), now).unwrap();\n*2720|        assert_eq!(short, now + Duration::from_secs(3), \"min honors a shorter carrier bound\");\n 2721|        // Non-pump: unbounded.\n 2722|        assert!(peer_reply_deadline(None, now).is_none(), \"non-pump stays unbounded\");\n 2723|    }\n## broker.rs\n 165|/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n*166|/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n 167|/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n 168|/// never trips the deadline.\n 169|const CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n...\n 178|/// untouched.\n*179|const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n 180|\n 181|/// The RESIZE BARRIER's drain-quiescence window (REQ-RC-RESIZE-GEOMETRY-EPOCH):\n 182|/// how long the appended high-water must hold STILL before the surface resize is\n...\n 213|/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n*214|/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n 215|/// protects the fan-out against ONE per-client conn falling behind and is\n 216|/// output-driven (the drain observes a Full channel); THIS bound is measured\n 217|/// full-with-**zero writer progress** and covers the case the output-driven path\n...\n 225|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*226|const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 227|\n*228|/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n*229|/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 230|/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 231|/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 232|/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n...\n 235|fn brain_write_deadline() -> Duration {\n*236|    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 237|        Ok(ms) => ms\n 238|            .parse::<u64>()\n 239|            .map(Duration::from_millis)\n*240|            .unwrap_or(BRAIN_WRITE_DEADLINE),\n*241|        Err(_) => BRAIN_WRITE_DEADLINE,\n 242|    }\n 243|}\n 244|\n...\n 249|/// `WakeClaim`'s phase stamps are the liveness itself, so the stand-down is now bounded\n*250|/// by the holder's PROGRESS (see `phase_wedge_ceiling`) and not by a clock.\n 251|/// Poll slice while waiting on a racing wake's session to appear.\n 252|const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 253|\n...\n 286|\n*287|impl Drop for WakeClaimGuard<'_> {\n*288|    fn drop(&mut self) {\n 289|        release_claim(&mut recover(self.map), &self.endpoint, self.generation);\n 290|    }\n 291|}\n...\n 315|/// detector, never a performance budget.\n*316|fn phase_wedge_ceiling(phase: &str) -> Duration {\n 317|    // TEST-ONLY SEAM, and it does not exist in a shipped binary: the int rig needs the\n 318|    // wedge arm to fire in milliseconds rather than after a real ceiling, but a stray\n 319|    // value in a deployment would be WORSE than a slow spawn — a ceiling of 0 makes\n...\n 341|    match phase {\n*342|        \"pty_creating\" => Duration::from_secs(120),\n*343|        _ => Duration::from_secs(30),\n 344|    }\n 345|}\n 346|\n...\n 393|        .map(Duration::from_millis)\n*394|        .unwrap_or(Duration::from_secs(30))\n 395|}\n 396|\n 397|/// The ONE cycle-verb liveness verdict (ADR-0041 decision 6,\n...\n 620|/// buggy translation binary can therefore stall operator input for at most this\n*621|/// long. Mirrors [`CONTROLLER_WRITE_DEADLINE`]'s order (5 s).\n*622|const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);\n 623|\n 624|/// The effective commit deadline. Production is the const above; the int gate\n 625|/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`\n...\n 827|/// advances `delivered_through` on each successful write, and it is never evicted\n*828|/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].\n 829|struct ControllerSink {\n 830|    /// The bounded live-frame queue to the controller's writer thread.\n 831|    tx: SyncSender<CtrlMsg>,\n...\n 865|    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n*866|    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n 867|    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n 868|    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n 869|    /// floor). Seeded `now` at [`OutputLog::become_controller`]."}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs, .worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs, .worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":32,"fileCount":3,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs","count":7},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs","count":5},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## conn.rs\n 124│\n*125│const HEALTHY_LOG_INTERVAL: Duration = Duration::from_secs(60);\n 126│\n 127│struct HealthyWindow {\n 128│    since: Instant,\n    │...\n 525│    /// deadline (gate-wait + OS write completion) — the broker passes its\n*526│    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n 527│    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n 528│        static LOG: LazyLock<Arc<LifecycleLog>> = LazyLock::new(|| Arc::new(LifecycleLog::new()));\n 529│        Self::with_log(half, bound, Arc::clone(&LOG))\n    │...\n 598│    /// Bounded framed write. See [`Self::write_gated`].\n*599│    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n 600│        self.write_gated(env, || true)\n 601│    }\n 602│\n    │...\n 721│#[cfg(test)]\n*722│impl Drop for TestGatePin {\n*723│    fn drop(&mut self) {\n 724│        let mut g = recover(&self.inner.gate);\n 725│        g.half = self.half.take();\n 726│        drop(g);\n    │...\n 745│\n*746│impl Drop for BrokerConn {\n*747│    fn drop(&mut self) {\n 748│        // Only a healthy physical close is counted instead of logged per conn.\n 749│        // Error/timeout closes keep their full attribution and original start.\n 750│        // [impl->REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\n## brain.rs\n 2637│/// full 30s `PUMP_PEER_IO_TIMEOUT`).\n*2638│pub(crate) const PEER_REPLY_READ_BUDGET: Duration = Duration::from_secs(10);\n 2639│\n 2640│/// A verb whose reply read MUST be able to time out refuses an unbounded carrier\n 2641│/// here, before any wire I/O, instead of documenting the requirement and hoping.\n     │...\n 2714│        // Pump mode, 30s carrier: reply-read capped to the 10s budget.\n*2715│        let d = peer_reply_deadline(Some(Duration::from_secs(30)), now).expect(\"pump = Some\");\n 2716│        assert_eq!(d, now + PEER_REPLY_READ_BUDGET, \"capped at the 10s budget, not 30s\");\n*2717│        assert!(d < now + Duration::from_secs(30), \"strictly before the carrier deadline\");\n 2718│        // A carrier timeout SHORTER than the budget wins the min (never exceed it).\n*2719│        let short = peer_reply_deadline(Some(Duration::from_secs(3)), now).unwrap();\n*2720│        assert_eq!(short, now + Duration::from_secs(3), \"min honors a shorter carrier bound\");\n 2721│        // Non-pump: unbounded.\n 2722│        assert!(peer_reply_deadline(None, now).is_none(), \"non-pump stays unbounded\");\n 2723│    }\n## broker.rs\n 165│/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel\n*166│/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via\n 167│/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n 168│/// never trips the deadline.\n 169│const CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n    │...\n 178│/// untouched.\n*179│const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);\n 180│\n 181│/// The RESIZE BARRIER's drain-quiescence window (REQ-RC-RESIZE-GEOMETRY-EPOCH):\n 182│/// how long the appended high-water must hold STILL before the surface resize is\n    │...\n 213│/// REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE). Distinct from and 3× longer than\n*214│/// [`CONTROLLER_WRITE_DEADLINE`] on PURPOSE (doyle ruling Q1): that 5 s bound\n 215│/// protects the fan-out against ONE per-client conn falling behind and is\n 216│/// output-driven (the drain observes a Full channel); THIS bound is measured\n 217│/// full-with-**zero writer progress** and covers the case the output-driven path\n    │...\n 225│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*226│const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 227│\n*228│/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n*229│/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 230│/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 231│/// the slot) is provable in ~1 s instead of 15 (mirrors the\n 232│/// `SPT_INJECT_COMMIT_DEADLINE_MS` env-knob precedent). Grants test wall-clock\n    │...\n 235│fn brain_write_deadline() -> Duration {\n*236│    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 237│        Ok(ms) => ms\n 238│            .parse::<u64>()\n 239│            .map(Duration::from_millis)\n*240│            .unwrap_or(BRAIN_WRITE_DEADLINE),\n*241│        Err(_) => BRAIN_WRITE_DEADLINE,\n 242│    }\n 243│}\n 244│\n    │...\n 249│/// `WakeClaim`'s phase stamps are the liveness itself, so the stand-down is now bounded\n*250│/// by the holder's PROGRESS (see `phase_wedge_ceiling`) and not by a clock.\n 251│/// Poll slice while waiting on a racing wake's session to appear.\n 252│const WAKE_DEDUP_POLL: Duration = Duration::from_millis(50);\n 253│\n    │...\n 286│\n*287│impl Drop for WakeClaimGuard<'_> {\n*288│    fn drop(&mut self) {\n 289│        release_claim(&mut recover(self.map), &self.endpoint, self.generation);\n 290│    }\n 291│}\n    │...\n 315│/// detector, never a performance budget.\n*316│fn phase_wedge_ceiling(phase: &str) -> Duration {\n 317│    // TEST-ONLY SEAM, and it does not exist in a shipped binary: the int rig needs the\n 318│    // wedge arm to fire in milliseconds rather than after a real ceiling, but a stray\n 319│    // value in a deployment would be WORSE than a slow spawn — a ceiling of 0 makes\n    │...\n 341│    match phase {\n*342│        \"pty_creating\" => Duration::from_secs(120),\n*343│        _ => Duration::from_secs(30),\n 344│    }\n 345│}\n 346│\n    │...\n 393│        .map(Duration::from_millis)\n*394│        .unwrap_or(Duration::from_secs(30))\n 395│}\n 396│\n 397│/// The ONE cycle-verb liveness verdict (ADR-0041 decision 6,\n    │...\n 620│/// buggy translation binary can therefore stall operator input for at most this\n*621│/// long. Mirrors [`CONTROLLER_WRITE_DEADLINE`]'s order (5 s).\n*622│const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);\n 623│\n 624│/// The effective commit deadline. Production is the const above; the int gate\n 625│/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`\n    │...\n 827│/// advances `delivered_through` on each successful write, and it is never evicted\n*828│/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].\n 829│struct ControllerSink {\n 830│    /// The bounded live-frame queue to the controller's writer thread.\n 831│    tx: SyncSender<CtrlMsg>,\n    │...\n 865│    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;\n*866│    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp\n 867│    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-\n 868│    /// alive controller just drops the chunk + falls behind the ring (resume-from-\n 869│    /// floor). Seeded `now` at [`OutputLog::become_controller`]."},"isError":false,"timestamp":1789394573197}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HpSFJiotZyBYQq4DADbo8jGK|fc_0a63d1e8a5b46c9c016aa7fe9a1e9087d1afeb08f7537e7e70","toolName":"grep","startedAt":"2026-09-14T14:03:06.207Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs;.worktrees/297-enforcement-shape/crates/spt-proto/src;.worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md;.worktrees/297-enforcement-sh…"},"intent":"Finding request schema and hazard sections"},"id":"3be49ce1","parentId":"35574012","timestamp":"2026-09-14T14:03:06.207Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TfLix6qnNELOK4ea68DOVcVo|fc_0a63d1e8a5b46c9c016aa7fe9a1e9c87d1964e55151e9a19cc","toolName":"read","startedAt":"2026-09-14T14:03:06.207Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs:598-686"},"intent":"Reading connection write critical path"},"id":"3c37feb3","parentId":"3be49ce1","timestamp":"2026-09-14T14:03:06.207Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6CwYQ8fgO11451OTmlQQcyJ0|fc_0a63d1e8a5b46c9c016aa7fe9a1ea487d1931ac206b32662ab","toolName":"glob","startedAt":"2026-09-14T14:03:06.207Z","args":{"path":".worktrees/297-enforcement-shape/docs/adr/*0041*;.worktrees/297-enforcement-shape/**/*DOMAIN*;.worktrees/297-enforcement-shape/**/*domain*.md"},"intent":"Locating process domain decisions"},"id":"30a33601","parentId":"3c37feb3","timestamp":"2026-09-14T14:03:06.207Z"}
{"type":"message","id":"095f0d7b","parentId":"30a33601","timestamp":"2026-09-14T14:03:06.207Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Tightening raw section reads**"},{"type":"toolCall","id":"call_HpSFJiotZyBYQq4DADbo8jGK|fc_0a63d1e8a5b46c9c016aa7fe9a1e9087d1afeb08f7537e7e70","name":"grep","arguments":{"i":"Finding request schema and hazard sections","pattern":"struct SpawnReq|struct Spawned|struct Envelope|fn new\\(|7\\.6|6\\.9|BRAIN-READ-BOUNDED|unbounded.brain|#302|NetHost2","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs;.worktrees/297-enforcement-shape/crates/spt-proto/src;.worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md;.worktrees/297-enforcement-shape/CONTEXT.md","case":false,"gitignore":false,"skip":null},"intent":"Finding request schema and hazard sections"},{"type":"toolCall","id":"call_TfLix6qnNELOK4ea68DOVcVo|fc_0a63d1e8a5b46c9c016aa7fe9a1e9c87d1964e55151e9a19cc","name":"read","arguments":{"i":"Reading connection write critical path","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs:598-686"},"intent":"Reading connection write critical path"},{"type":"toolCall","id":"call_6CwYQ8fgO11451OTmlQQcyJ0|fc_0a63d1e8a5b46c9c016aa7fe9a1ea487d1931ac206b32662ab","name":"glob","arguments":{"i":"Locating process domain decisions","path":".worktrees/297-enforcement-shape/docs/adr/*0041*;.worktrees/297-enforcement-shape/**/*DOMAIN*;.worktrees/297-enforcement-shape/**/*domain*.md","hidden":true,"gitignore":false,"limit":30},"intent":"Locating process domain decisions"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":8682,"output":312,"cacheRead":41088,"cacheWrite":0,"totalTokens":50082,"reasoningTokens":28,"cost":{"input":0.08682000000000001,"output":0.015600000000000001,"cacheRead":0.041088,"cacheWrite":0,"total":0.14350800000000002}},"stopReason":"toolUse","timestamp":1789394573202,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe8d797087d1b8435cc03a394b90","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7fe8eca7087d1a31b19eb44fff0f4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_6aihNdnHuiLz0OR1mBtCBWGzBEh-d2MendWtsJ8-eR1soN-2V8gfchfxtVNmoyvhBxusJLhWCm7FEdvSrIsBTWb4bGmIYJKz4zCJwgdTgkRduY0pdPj_3h2Czz80RDhF7IPAQcLwUzJKw-L3fTKCVaxeRrBBp9ZQJXsnrzPvi8yRBJ1pm202AxBtBPDo-M9WKcIqEKfZRq3-FMeUCyotYFOqw3qQ52T9KYBXKnE4gf4AzFeWqIkouJrQiSQPPGZbBurXnQ73-kNGDE4y1rLVONya4BUxEOQkIt2PSypYPXG__-Q_oDOCWjDvyPmQu1PnfGlU5O8pwLSZ4igaIoGOxh4LCx31BQ72tTWPPJT_AQl3oGrJojNaZQUM4UGwvNbYrnectgvkOsX2u9vd4d1N3UTrswriWMPMq2Tlk_cFGhzEu6Oe3LLeyXOoDOafchJ5jrJyhDC5JgHOiU4hZI8Q6XLDGWrXgDQy7x98vL_9XrWx4DXPjF7LbGF23tSImgTTWsdFF-tXb-P6wnkxmZLyMPKVCZUSlNP9krZAnlQkCsmrsf_AJ7eYY01WZogvCe1GLf-8Xfp7Typz1O-ONHYV4-C7TqWZFRJf7F3SVdDqV2wcKA5vmNTUw1Ef7Bep62J6k-8x_5VxKei9dpVEDG2PEkNn_c5yAr31f7DkXpzz3UqiWjEwc9tvSCgv_Ml6sEOKyTp2YsdYldVJJ_iVW42jGgkOQlJCHDGXUmavbhQ3uF6G_n35M1c9U8oo7qMAO92IslW6cQGNfAYb1XHOfC0Ecmb6-jFwOAbCg6JuRxIIuxgtmTtCPpWIryNZ8PTj1bNfXIEQEviPuszYolhE45oUKXqbzLcujB2acNjcxDZ2rFCnLm98hVYkVeJru2jKxqoWJF38_7-FqlXPUx1bpUm2FIj1hw8gKz6UA_Wjx2CrVsNKML08_tOop5m__L5uX4Q6zf1mEgOdEW1P_SFuriV6dIDBmDdaoM4huokOwXI_F7js2oZXb-hkTD8AN0QaeyFXO9tAXL0sOIqxNI7wknQL89_o2JcUVrfk50ZrIjoU0e-8Hs6TXIzxk-HOtWOh5xEpJQVzXEMeFfUyjOut0pBO3LNxJB7O4RRHDZjJUAlNJj8Ehrl7Un1RjhCSvLff8VfeJJInfe5W_NUq2-nWmnNpfugjfI63s5og98FLnaH1VYLmRCRMBWFKx8kTq_A1g61aqy98Pvd8Yq9M7SAhtIZeQ2obbG8Zxp6iP4nLQXxQ72pIDKT8-I1h-eVn_TdG2lEtYyKP1Trl6nv7jdx4efXGWk5pJKgOFYKEvfcSrK3igcuaLm6RSI2mLH8nmjXJpF4VlYIO9dX3i0jkIIjHM3y1438xp2UO72AZI-E5ccaTEItINyQmdajGoLL_dZw_nwMJkO29KbmSRPviGfswVAHAMzJYtn3dBUVmT_e3xnlZOCZtGjvTTl1LOYTZda3jMEgnCS","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"summary":[{"type":"summary_text","text":"**Tightening raw section reads**"}],"metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe9a1e9087d1afeb08f7537e7e70","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding request schema and hazard sections\",\"pattern\":\"struct SpawnReq|struct Spawned|struct Envelope|fn new\\\\(|7\\\\.6|6\\\\.9|BRAIN-READ-BOUNDED|unbounded.brain|#302|NetHost2\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs;.worktrees/297-enforcement-shape/crates/spt-proto/src;.worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md;.worktrees/297-enforcement-shape/CONTEXT.md\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_HpSFJiotZyBYQq4DADbo8jGK","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe9a1e9c87d1964e55151e9a19cc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection write critical path\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs:598-686\"}","call_id":"call_TfLix6qnNELOK4ea68DOVcVo","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fe9a1ea487d1931ac206b32662ab","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating process domain decisions\",\"path\":\".worktrees/297-enforcement-shape/docs/adr/*0041*;.worktrees/297-enforcement-shape/**/*DOMAIN*;.worktrees/297-enforcement-shape/**/*domain*.md\",\"hidden\":true,\"gitignore\":false,\"limit\":30}","call_id":"call_6CwYQ8fgO11451OTmlQQcyJ0","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"glob","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":12983.7567999959,"ttft":1628.181799992919,"completedAt":1789394586205,"contextSnapshot":{"promptTokens":49770,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"a5e17139","parentId":"095f0d7b","timestamp":"2026-09-14T14:03:06.225Z","message":{"role":"toolResult","toolCallId":"call_TfLix6qnNELOK4ea68DOVcVo|fc_0a63d1e8a5b46c9c016aa7fe9a1e9c87d1964e55151e9a19cc","toolName":"read","content":[{"type":"text","text":"\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // The gate serializes first-write initialization. Save the instant\n        // BEFORE I/O so a later failure still names when this conn went active.\n        // [impl->REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\n        if inner.first_write.get().is_none() {\n            let _ = inner.first_write.set(LogTime::now());\n            inner.lifecycle_log.healthy(true);\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n…\n    }\n\n[Showing lines 597-689 of 1000. Use :690 to continue]"}],"details":{"truncation":{"content":"\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n                return Err(poisoned_err());\r\n            }\r\n            if g.half.is_some() {\r\n                break;\r\n            }\r\n            let now = Instant::now();\r\n            if now >= deadline {\r\n                // The write we queued behind is stuck past OUR whole bound:\r\n                // that op is the wedge. Abort it out of band and fail — the\r\n                // watchdog's own deadline (armed at that op's entry) is also\r\n                // in flight; both paths converge on the same idempotent\r\n                // poison/cancel.\r\n                drop(g);\r\n                inner.timed_out.store(true, Ordering::Release);\r\n                inner.poison_and_cancel(None);\r\n                return Err(timeout_err());\r\n            }\r\n            let (g2, _) = inner\r\n                .gate_cv\r\n                .wait_timeout(g, deadline - now)\r\n                .unwrap_or_else(|p| p.into_inner());\r\n            g = g2;\r\n        }\r\n\r\n        if !pre() {\r\n            return Ok(ConnWrite::Superseded);\r\n        }\r\n\r\n        // The gate serializes first-write initialization. Save the instant\r\n        // BEFORE I/O so a later failure still names when this conn went active.\r\n        // [impl->REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\r\n        if inner.first_write.get().is_none() {\r\n            let _ = inner.first_write.set(LogTime::now());\r\n            inner.lifecycle_log.healthy(true);\r\n        }\r\n\r\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\r\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\r\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = Some((op, deadline));\r\n            inner.dog_cv.notify_all();\r\n        }\r\n        drop(g);\r\n\r\n        let res = write_frame(&mut half, env);\r\n\r\n        // Completion handshake: `write_frame` has returned, so the OS op —\r\n        // canceled or not — reported completion and the frame buffer is\r\n        // released. Clear the arm BEFORE deciding health, so the watchdog\r\n        // never severs while the op is formally in flight.\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = None;\r\n            inner.dog_cv.notify_all();\r\n        }\r\n\r\n        let mut g = recover(&inner.gate);\r\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\r\n            g.half = Some(half);\r\n            drop(g);\r\n            inner.gate_cv.notify_all();\r\n            return Ok(ConnWrite::Done);\r\n        }\r\n\r\n        // ── Retire: poison the whole conn, abort the read side so\r\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\r\n        // graveyard (its handle must outlive any late raw abort; never\r","truncated":true,"truncatedBy":"lines","totalLines":404,"totalBytes":3884,"outputLines":93,"outputBytes":3884,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1000,"displayContent":{"text":"\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n                return Err(poisoned_err());\r\n            }\r\n            if g.half.is_some() {\r\n                break;\r\n            }\r\n            let now = Instant::now();\r\n            if now >= deadline {\r\n                // The write we queued behind is stuck past OUR whole bound:\r\n                // that op is the wedge. Abort it out of band and fail — the\r\n                // watchdog's own deadline (armed at that op's entry) is also\r\n                // in flight; both paths converge on the same idempotent\r\n                // poison/cancel.\r\n                drop(g);\r\n                inner.timed_out.store(true, Ordering::Release);\r\n                inner.poison_and_cancel(None);\r\n                return Err(timeout_err());\r\n            }\r\n            let (g2, _) = inner\r\n                .gate_cv\r\n                .wait_timeout(g, deadline - now)\r\n                .unwrap_or_else(|p| p.into_inner());\r\n            g = g2;\r\n        }\r\n\r\n        if !pre() {\r\n            return Ok(ConnWrite::Superseded);\r\n        }\r\n\r\n        // The gate serializes first-write initialization. Save the instant\r\n        // BEFORE I/O so a later failure still names when this conn went active.\r\n        // [impl->REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\r\n        if inner.first_write.get().is_none() {\r\n            let _ = inner.first_write.set(LogTime::now());\r\n            inner.lifecycle_log.healthy(true);\r\n        }\r\n\r\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\r\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\r\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = Some((op, deadline));\r\n            inner.dog_cv.notify_all();\r\n        }\r\n        drop(g);\r\n\r\n        let res = write_frame(&mut half, env);\r\n\r\n        // Completion handshake: `write_frame` has returned, so the OS op —\r\n        // canceled or not — reported completion and the frame buffer is\r\n        // released. Clear the arm BEFORE deciding health, so the watchdog\r\n        // never severs while the op is formally in flight.\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = None;\r\n            inner.dog_cv.notify_all();\r\n        }\r\n\r\n        let mut g = recover(&inner.gate);\r\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\r\n            g.half = Some(half);\r\n            drop(g);\r\n            inner.gate_cv.notify_all();\r\n            return Ok(ConnWrite::Done);\r\n        }\r\n\r\n        // ── Retire: poison the whole conn, abort the read side so\r\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\r\n        // graveyard (its handle must outlive any late raw abort; never\r\n…\n    }","startLine":597,"lineNumbers":[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,null,708]},"fileSize":43321,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\conn.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1000,"totalBytes":3884,"outputLines":93,"outputBytes":3884,"shownRange":{"start":597,"end":689},"nextOffset":690}}},"isError":false,"timestamp":1789394586225}}
{"type":"message","id":"7ad966e3","parentId":"a5e17139","timestamp":"2026-09-14T14:03:06.244Z","message":{"role":"toolResult","toolCallId":"call_HpSFJiotZyBYQq4DADbo8jGK|fc_0a63d1e8a5b46c9c016aa7fe9a1e9087d1afeb08f7537e7e70","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/\n\n## crates/\n\n### spt-daemon/src/\n#### msg.rs\n 114|pub const KIND_NET_STATUS_REPLY: &str = \"net-status-reply\";\n*115|/// Opt-in releases#302 field diagnostic; no runtime scheduling or session locks.\n 116|pub const KIND_NET_REACTOR_DIAG: &str = \"net-reactor-diag\";\n 117|pub const KIND_NET_REACTOR_DIAG_REPLY: &str = \"net-reactor-diag-reply\";\n 118|/// Brain→broker: report the broker's OWN compiled image and process identity.\n...\n 497|#[derive(Debug, Clone, Serialize, Deserialize)]\n*498|pub struct SpawnReq {\n 499|    /// The program to exec (resolved on `PATH` by the OS spawn).\n 500|    pub program: String,\n 501|    /// Its arguments, verbatim.\n...\n 559|#[derive(Debug, Clone, Serialize, Deserialize)]\n*560|pub struct Spawned {\n 561|    /// The id the brain uses to address this session in later commands.\n 562|    pub session_id: u64,\n 563|    /// The child's OS pid, if the backend exposes one.\n\n### spt-proto/src/\n#### emit.rs\n 220|impl CountingWriter {\n*221|    pub fn new() -> Self {\n 222|        Self::default()\n 223|    }\n 224|\n...\n 280|impl ShortWritingWriter {\n*281|    pub fn new(chunk: usize) -> Self {\n 282|        assert!(chunk > 0, \"a writer that accepts nothing never terminates\");\n 283|        Self {\n 284|            chunk,\n\n## docs/\n### KNOWN-HAZARDS.md\n 428|\n*429|### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n 430|- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_t...\n 431|- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n 432|- **spt-core mapping:** the **other-session** face is unreachable today — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no other session to starve. The **same-session** face is reachable in direct `Brain` users: `spawn_session_pid` can consume and discard that session's own first `Output` frame while waiting for `Spawned` (measured in-process on Linux, 2026-08-25), so a caller must not treat the producer's first chunk as guaranteed post...\n...\n 500|\n*501|### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\n 502|<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\n 503|- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n 504|- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n...\n 507|\n*508|<!-- [doc->REQ-BRAIN-READ-BOUNDED-PER-CALL] -->\n 509|**Per-call enforcement (releases#293).** Every Brain reply-wait loop computes\n 510|`call_deadline()` once, after sending its request and before entering the loop,\n 511|and passes that same value to `read_event_until`. Unrelated frames do not renew\n...\n 538|<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n*539|- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n*540|- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n*541|- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIE...\n*542|- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n 543|\n 544|### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n 545|<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n...\n 802|- **spt-core mapping:** `spt-daemon/src/pump/mod.rs` `resolve_submit_addr` + `presence_state_effect` (`PRESENCE_DIAL_FAILED` arm); `spt-store/src/peeraddrs.rs` `PeerAddrStore::put`/`drop_seed`; `spt-daemon/src/seedproofx.rs` `gapfill_peeraddrs`; status renders (`daemon status` / `subnet status`).\n*803|- **Second clause (added 2026-07-31, see 7.59):** a peer is never stranded by an address that has ALREADY failed either. Preferring the roster address assumed the retained row was fresher than the failed one; the cross-site incident falsified that assumption (7.60 froze every self row), turning this entry's fix into a `fail → demote → reinstall-same → fail` livelock. The clause is ADDITIVE — everything above still holds, and a roster address DIFFERING from the failed one still resolves and still ...\n 804|- **Source:** MESH-RECOVERY W1 (hertz RCA `.claude/reports/2026-07-10-hertz-session/02-mesh-isolation-rca.md`, doyle seam-verified 2026-07-16; ADR-0039 — amends REQ-CONV-1's falsified \"a stale addr never strands a peer\" drop-on-fail mechanism).\n 805|\n 806|---\n...\n 949|- **Why the file lies even when nothing is stale.** `daemon.rs` writes the breadcrumb **before** `serve_seed_control` binds the seed socket, and that bind is what arbitrates single-daemon-per-home. A second starter therefore reaches the write, overwrites the winner's pid, *then* loses the bind and exits. The file now names a dead pid while the real daemon serves. Measured on HFENDULEAM 2026-07-31: `daemon.pid` = 40332, dead since startup; the daemon actually serving = 23416, started one second earlier. B...\n*950|- **How it bit (releases#43).** The inbound-reachability verify was first gated on `daemon.pid == std::process::id()` — \"only the resident daemon verifies\". On the box it was written on, that predicate is false inside the real daemon, so the feature would have been disabled in production while every test passed. A gate that reads the fault as \"not me\" fails silent, which is why this belongs with the 7.61 family: nothing renders, and nothing renders when all is well either.\n 951|- **Invariant:** identity is re-derived, never asserted by a file. Validate a recorded pid against the **process table plus the image path** — the pid must still resolve to an image and that image must be the recorded one (a pid alone is recyclable). And where the question is *\"am I the daemon\"*, do not ask an oracle at all: put the call at a site that **is** the daemon by construction — after its own successful bind — so no file is consulted.\n 952|- **Corrected pattern:** `spt_store::inbound` (record carries binder pid + image path, reader re-derives validity) with its trigger in `spt_daemon::daemon::try_start_net`'s success arm rather than inside `NetHost::start`, which the CLI also calls.\n 953|- **Open audit surface, stated not fixed:** the other `daemon.pid` readers — `crates/spt/src/cli.rs` ~4908 and ~5043 — were not audited by that change. Each needs its own reading of what it uses the value FOR; a display of \"the daemon's pid\" is not the same claim as a decision gated on it. Tracked separately on the board (doyle), not silently absorbed here.\n...\n 959|<!-- [doc->REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP] -->\n*960|- **Failure (paid-for, hertz field RCA 2026-07-30/31 — HFENDULEAM cross-site invisibility, live-fire proof on the running daemon):** 7.42's fix retained the roster address and taught the resolver to prefer it. The unstated assumption behind that preference — that a retained roster row is fresher than the address that just failed — was false fleet-wide, because every node's self row was frozen by a regressed lease (7.60). The result is a livelock, not a strand: `resolve_submit_addr` leg 2 re-served ...\n 961|- **Invariant:** the node keeps a node-LOCAL, RAM-only, TTL-bounded memory of the exact ADDRESSES its dials failed on — keyed on the full address actually dialed, by exact equality, built at the dial site and cleared by a successful dial or validation of that address. No path may re-serve or reinstall a just-failed address: route-chain leg 2 skips a roster address identical to it and falls through to discovery, and reconcile refuses to reinstall it over a suspect row (leaving the mark standing). A **di...\n 962|- **spt-core mapping:** `spt-daemon/src/failedaddr.rs` (the shared record + the ONE equality site); `spt-daemon/src/nethost.rs` `submit_dial` (built here — the site that owns the dialed address — and carried to the pump on `PRESENCE_DIAL_FAILED.remote_addr`); `spt-daemon/src/pump/mod.rs` `resolve_submit_addr` leg 2 + `presence_state_effect`; `spt-daemon/src/seedproofx.rs` `reconcile_peeraddrs` suspect arm.\n*963|- **Source:** WAN-TRUTH batch #50 lane 1 (issue #40; hertz RCA + live-fire injection 2026-07-30; doyle's address-keyed ruling 2026-07-31, which amended the RCA's per-leg/per-peer F2/F3 wording precisely because that shape would have deleted 7.42's connection-free strand-heal). Kin 7.42 (`REQ-HAZARD-MESH-BOOTSTRAP-TRAP`, the first clause), 7.60 (the lease freeze that falsified the freshness assumption).\n 964|\n 965|---\n 966|\n*967|### 7.60 A node's advertised address is repairable by the node itself — a regressed epoch counter must never freeze its own roster row  `[REQ-HAZARD-SELF-ROW-LEASE-FREEZE]`\n 968|\n 969|<!-- [doc->REQ-HAZARD-SELF-ROW-LEASE-FREEZE] -->\n 970|- **Failure (paid-for, hertz forensics 2026-07-30, crash-correlated to the minute on two independent nodes):** `EpochSource::next_epoch` persists through the NON-durable atomic write, whose own doc classifies epoch counters as reconstructible state for which a post-crash NUL husk \"simply parse-fails to absent and is regenerated\". That classification was the defect: `load_from` silently degrades a husk to `0`, and the counter's VALUE is the fleet-wide lease authority, so `merge_entry`'s strictly-greater r...\n...\n 976|\n*977|### 7.61 A monitor's green must not be producible by the fault it watches  `[REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE]`\n 978|\n 979|<!-- [doc->REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE] -->\n 980|- **Failure (paid-for twice in one batch, on the same surface):** a health surface whose green state can be *reached by the very failure it exists to report* does not merely miss the incident — it actively certifies the broken system as healthy, which is worse than having no monitor at all, because it terminates the operator's investigation. **Instance 1 (2026-07-10 sequester):** `net_up` meant \"a local NetHost exists\", the heartbeat meant \"the loop is alive\", and the durable counts meant \"membership\" ...\n...\n 986|\n*987|### 7.62 A new wire record carries a `kind` tag — the untagged shape is already claimed, and an older daemon delivers it as chat  `[REQ-HAZARD-WIRE-UNTAGGED-RECORD]`\n 988|\n 989|<!-- [doc->REQ-HAZARD-WIRE-UNTAGGED-RECORD] -->\n 990|- **Failure (caught in design, W4 T10 / releases#53 — before it shipped, by asking what an N-1 daemon actually does rather than assuming it would simply not understand):** the stream dispatcher classifies a feed by sniffing its FIRST LINE, and the message family is deliberately **untagged** — `classify_first_line` reads any first line carrying `target` + `op_id` with no `kind` as `StreamFamily::WanMsg`. A newly minted record type that omits a `kind` tag is therefore not \"unrecognized\" on an older pee...\n 991|- **Invariant:** every wire record minted from here on carries a `kind` tag routed by its own tagged arm, so an older daemon's `Some(_)` catch-all resolves it to `StreamFamily::Unknown` and DROPS it cleanly. The correct degrade for a request an old peer cannot serve is that it **never arrives** — never that it arrives as something else. A record whose absence is silent is recoverable; a record delivered as the wrong kind is not.\n*992|- **Proof obligation — three parts, or the pin goes vacuous:** (a) this version routes the record to its own family, AND is asserted NOT to route it to the message family; (b) the same bytes under a tag this version does not know resolve to `Unknown`; (c) the untagged shape is asserted to genuinely BE claimed by the message arm. Without (c) the test passes for the wrong reason the day the record's fields drift such that it would no longer have collided — the guard would then be measuring nothing whil...\n 993|- **spt-core mapping:** `spt-net/src/net/knockmsg.rs` (`KNOCK_KIND`, the tag on `KnockRecord`); `spt-daemon/src/dispatch.rs` `classify_first_line` (the tagged arm) + `StreamFamily::Unknown` (the clean drop); unit `a_knock_record_routes_here_and_drops_cleanly_on_an_n1_daemon` (all three parts, against the real classifier).\n 994|- **Source:** W4 knocking wave (releases#53 T10; doyle-ruled 2026-07-31 as a hazard CLASS rather than a one-off, since every future wire record faces the same trap).\n 995|\n...\n 997|\n*998|### 7.63 A dedupe key is never narrower than the identity of the record it replaces — a shared id with two owners must not collapse to one  `[REQ-HAZARD-MUTUAL-PREAUTH-SIBLING]`\n 999|\n 1000|<!-- [doc->REQ-HAZARD-MUTUAL-PREAUTH-SIBLING] -->\n*1001|- **Failure (DOORBELL, releases#83/#79 lane; the near-miss is the entry's point):** arming a mutual pre-authorization deliberately REPLACES rather than stacks, so that a re-knock cannot accumulate duplicate reverse grants. The dedupe is correct; its KEY is where the trap sits. One code id is legitimately held by **two** pre-authorizations with different owners — the minter's, armed at mint with an empty subject because no redeemer exists yet, and the redeemer's own, armed at redemption with the subject...\n 1002|- **Invariant:** a replace-in-place keyed lookup states the FULL identity of the record it replaces — here `(kind, key_id, owner)`. Where a key is deliberately NARROWER than a record's identity, that narrowness is a decision the seam states in words, and a test pins that two records sharing the narrow key both survive. Silent replacement is a forbidden outcome; refusing, stacking, or replacing are all defensible, but only if the seam chose one.\n 1003|- **The asymmetry is deliberate — do not \"fix\" it into agreement:** `consume_redeemed` filters on `(kind, key_id)` *without* the owner precisely because one redemption must make BOTH siblings due at once, each resolving its own subject. `armed_mutual`/`consume_mutual` also omit the owner, and are correct to: they serve the knock keyspace, where only the knocker's single record ever exists. Three seams, two key widths, one rule — the width matches what the seam is addressing, not what its neighbour us...\n 1004|- **spt-core mapping:** `spt-store/src/knock.rs` `arm_mutual` (the owner-inclusive match, with the rule stated in its own doc comment) beside `consume_redeemed` (the owner-blind consumption that takes both siblings); unit `a_redemption_consumes_what_it_makes_due_and_nothing_else` — genuinely discriminating rather than merely adjacent, since dropping `owner` from the arming key makes its `due.len() == 2` assertion fail.\n*1005|- **Source:** DOORBELL milestone (W2 `--mutual`/`--one-way` forced choice + W4 sealed-code redemption; doyle-ruled at the golden-head hand-off 2026-08-01 as a hazard CLASS — the trap is any dedupe whose key is narrower than its record's identity, and this milestone is simply where it was paid for). Kin 7.61 (the intuitive spelling being the one the fault satisfies).\n 1006|\n 1007|---\n 1008|\n*1009|### 7.64 Session-scoped authority does not survive a session-id rotation at rest — a boundary CARRIES it as a move, never leaves a copy  `[REQ-HAZARD-EMPOWER-SESSION-RESURRECT]`\n 1010|\n 1011|<!-- [doc->REQ-HAZARD-EMPOWER-SESSION-RESURRECT] -->\n 1012|- **Failure (LOCKSMITH t1, releases#117; MEASURED by red probe 2026-08-03, not inferred):** an engine-room empowerment is session-state — ADR-0052 says \"nothing to revoke at rest\" — and is filed under the session id the engine room's perch is bound to. `spt api boundary` rotates exactly that id, to an ARBITRARY `--to-session-id` past a guard that refuses only psyche-custody squats, and it is NEITHER of the two revocation edges ADR-0052 §3a names. The record was therefore left filed under the OLD ses...\n...\n 1018|- **spt-core mapping:** `spt-store/src/empower.rs` `carry_session`/`carry_between`/`neutralize_at` (the move, the two barriers, and the injectable removal seam the failure arm is only reachable through); `spt/src/api/reporting.rs` `cmd_boundary` (the ER-gated call, the old sid captured under the SAME lock as the rotation, and the loud lines — it never fails the boundary, because the harness has already rotated and a refusal would desync the record from the reality it describes); ADR-0052 §3c. Units: `...\n*1019|- **Source:** LOCKSMITH tranche 1 (releases#117; doyle-ruled 2026-08-03 — carry-as-move, fail-closed, a NEW third named ADR edge rather than a stretch of the existing text, no widening of the hygiene sweep). Kin 7.51 (a recycled identity must not read as the original), 7.63 (a key narrower than the identity of what it addresses).\n 1020|\n 1021|---\n 1022|\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs, .worktrees/297-enforcement-shape/crates/spt-proto/src, .worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md, .worktrees/297-enforcement-shape/CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":25,"fileCount":3,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs",".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs",".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs","count":3},{"path":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs","count":2},{"path":".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/297-enforcement-shape/\n\n## crates/\n\n### spt-daemon/src/\n#### msg.rs\n 114│pub const KIND_NET_STATUS_REPLY: &str = \"net-status-reply\";\n*115│/// Opt-in releases#302 field diagnostic; no runtime scheduling or session locks.\n 116│pub const KIND_NET_REACTOR_DIAG: &str = \"net-reactor-diag\";\n 117│pub const KIND_NET_REACTOR_DIAG_REPLY: &str = \"net-reactor-diag-reply\";\n 118│/// Brain→broker: report the broker's OWN compiled image and process identity.\n    │...\n 497│#[derive(Debug, Clone, Serialize, Deserialize)]\n*498│pub struct SpawnReq {\n 499│    /// The program to exec (resolved on `PATH` by the OS spawn).\n 500│    pub program: String,\n 501│    /// Its arguments, verbatim.\n    │...\n 559│#[derive(Debug, Clone, Serialize, Deserialize)]\n*560│pub struct Spawned {\n 561│    /// The id the brain uses to address this session in later commands.\n 562│    pub session_id: u64,\n 563│    /// The child's OS pid, if the backend exposes one.\n\n### spt-proto/src/\n#### emit.rs\n 220│impl CountingWriter {\n*221│    pub fn new() -> Self {\n 222│        Self::default()\n 223│    }\n 224│\n    │...\n 280│impl ShortWritingWriter {\n*281│    pub fn new(chunk: usize) -> Self {\n 282│        assert!(chunk > 0, \"a writer that accepts nothing never terminates\");\n 283│        Self {\n 284│            chunk,\n\n## docs/\n### KNOWN-HAZARDS.md\n  428│\n *429│### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n  430│- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_t...\n  431│- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n  432│- **spt-core mapping:** the **other-session** face is unreachable today — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no other session to starve. The **same-session** face is reachable in direct `Brain` users: `spawn_session_pid` can consume and discard that session's own first `Output` frame while waiting for `Spawned` (measured in-process on Linux, 2026-08-25), so a caller must not treat the producer's first chunk as guaranteed post...\n     │...\n  500│\n *501│### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\n  502│<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\n  503│- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop...\n  504│- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a heal...\n     │...\n  507│\n *508│<!-- [doc->REQ-BRAIN-READ-BOUNDED-PER-CALL] -->\n  509│**Per-call enforcement (releases#293).** Every Brain reply-wait loop computes\n  510│`call_deadline()` once, after sending its request and before entering the loop,\n  511│and passes that same value to `read_event_until`. Unrelated frames do not renew\n     │...\n  538│<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n *539│- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only v...\n *540│- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`...\n *541│- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIE...\n *542│- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n  543│\n  544│### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n  545│<!-- [doc->REQ-HAZARD-BROKER-SEED-WIRE-SKEW] -->\n     │...\n  802│- **spt-core mapping:** `spt-daemon/src/pump/mod.rs` `resolve_submit_addr` + `presence_state_effect` (`PRESENCE_DIAL_FAILED` arm); `spt-store/src/peeraddrs.rs` `PeerAddrStore::put`/`drop_seed`; `spt-daemon/src/seedproofx.rs` `gapfill_peeraddrs`; status renders (`daemon status` / `subnet status`).\n *803│- **Second clause (added 2026-07-31, see 7.59):** a peer is never stranded by an address that has ALREADY failed either. Preferring the roster address assumed the retained row was fresher than the failed one; the cross-site incident falsified that assumption (7.60 froze every self row), turning this entry's fix into a `fail → demote → reinstall-same → fail` livelock. The clause is ADDITIVE — everything above still holds, and a roster address DIFFERING from the failed one still resolves and still ...\n  804│- **Source:** MESH-RECOVERY W1 (hertz RCA `.claude/reports/2026-07-10-hertz-session/02-mesh-isolation-rca.md`, doyle seam-verified 2026-07-16; ADR-0039 — amends REQ-CONV-1's falsified \"a stale addr never strands a peer\" drop-on-fail mechanism).\n  805│\n  806│---\n     │...\n  949│- **Why the file lies even when nothing is stale.** `daemon.rs` writes the breadcrumb **before** `serve_seed_control` binds the seed socket, and that bind is what arbitrates single-daemon-per-home. A second starter therefore reaches the write, overwrites the winner's pid, *then* loses the bind and exits. The file now names a dead pid while the real daemon serves. Measured on HFENDULEAM 2026-07-31: `daemon.pid` = 40332, dead since startup; the daemon actually serving = 23416, started one second earlier. B...\n *950│- **How it bit (releases#43).** The inbound-reachability verify was first gated on `daemon.pid == std::process::id()` — \"only the resident daemon verifies\". On the box it was written on, that predicate is false inside the real daemon, so the feature would have been disabled in production while every test passed. A gate that reads the fault as \"not me\" fails silent, which is why this belongs with the 7.61 family: nothing renders, and nothing renders when all is well either.\n  951│- **Invariant:** identity is re-derived, never asserted by a file. Validate a recorded pid against the **process table plus the image path** — the pid must still resolve to an image and that image must be the recorded one (a pid alone is recyclable). And where the question is *\"am I the daemon\"*, do not ask an oracle at all: put the call at a site that **is** the daemon by construction — after its own successful bind — so no file is consulted.\n  952│- **Corrected pattern:** `spt_store::inbound` (record carries binder pid + image path, reader re-derives validity) with its trigger in `spt_daemon::daemon::try_start_net`'s success arm rather than inside `NetHost::start`, which the CLI also calls.\n  953│- **Open audit surface, stated not fixed:** the other `daemon.pid` readers — `crates/spt/src/cli.rs` ~4908 and ~5043 — were not audited by that change. Each needs its own reading of what it uses the value FOR; a display of \"the daemon's pid\" is not the same claim as a decision gated on it. Tracked separately on the board (doyle), not silently absorbed here.\n     │...\n  959│<!-- [doc->REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP] -->\n *960│- **Failure (paid-for, hertz field RCA 2026-07-30/31 — HFENDULEAM cross-site invisibility, live-fire proof on the running daemon):** 7.42's fix retained the roster address and taught the resolver to prefer it. The unstated assumption behind that preference — that a retained roster row is fresher than the address that just failed — was false fleet-wide, because every node's self row was frozen by a regressed lease (7.60). The result is a livelock, not a strand: `resolve_submit_addr` leg 2 re-served ...\n  961│- **Invariant:** the node keeps a node-LOCAL, RAM-only, TTL-bounded memory of the exact ADDRESSES its dials failed on — keyed on the full address actually dialed, by exact equality, built at the dial site and cleared by a successful dial or validation of that address. No path may re-serve or reinstall a just-failed address: route-chain leg 2 skips a roster address identical to it and falls through to discovery, and reconcile refuses to reinstall it over a suspect row (leaving the mark standing). A **di...\n  962│- **spt-core mapping:** `spt-daemon/src/failedaddr.rs` (the shared record + the ONE equality site); `spt-daemon/src/nethost.rs` `submit_dial` (built here — the site that owns the dialed address — and carried to the pump on `PRESENCE_DIAL_FAILED.remote_addr`); `spt-daemon/src/pump/mod.rs` `resolve_submit_addr` leg 2 + `presence_state_effect`; `spt-daemon/src/seedproofx.rs` `reconcile_peeraddrs` suspect arm.\n *963│- **Source:** WAN-TRUTH batch #50 lane 1 (issue #40; hertz RCA + live-fire injection 2026-07-30; doyle's address-keyed ruling 2026-07-31, which amended the RCA's per-leg/per-peer F2/F3 wording precisely because that shape would have deleted 7.42's connection-free strand-heal). Kin 7.42 (`REQ-HAZARD-MESH-BOOTSTRAP-TRAP`, the first clause), 7.60 (the lease freeze that falsified the freshness assumption).\n  964│\n  965│---\n  966│\n *967│### 7.60 A node's advertised address is repairable by the node itself — a regressed epoch counter must never freeze its own roster row  `[REQ-HAZARD-SELF-ROW-LEASE-FREEZE]`\n  968│\n  969│<!-- [doc->REQ-HAZARD-SELF-ROW-LEASE-FREEZE] -->\n  970│- **Failure (paid-for, hertz forensics 2026-07-30, crash-correlated to the minute on two independent nodes):** `EpochSource::next_epoch` persists through the NON-durable atomic write, whose own doc classifies epoch counters as reconstructible state for which a post-crash NUL husk \"simply parse-fails to absent and is regenerated\". That classification was the defect: `load_from` silently degrades a husk to `0`, and the counter's VALUE is the fleet-wide lease authority, so `merge_entry`'s strictly-greater r...\n     │...\n  976│\n *977│### 7.61 A monitor's green must not be producible by the fault it watches  `[REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE]`\n  978│\n  979│<!-- [doc->REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE] -->\n  980│- **Failure (paid-for twice in one batch, on the same surface):** a health surface whose green state can be *reached by the very failure it exists to report* does not merely miss the incident — it actively certifies the broken system as healthy, which is worse than having no monitor at all, because it terminates the operator's investigation. **Instance 1 (2026-07-10 sequester):** `net_up` meant \"a local NetHost exists\", the heartbeat meant \"the loop is alive\", and the durable counts meant \"membership\" ...\n     │...\n  986│\n *987│### 7.62 A new wire record carries a `kind` tag — the untagged shape is already claimed, and an older daemon delivers it as chat  `[REQ-HAZARD-WIRE-UNTAGGED-RECORD]`\n  988│\n  989│<!-- [doc->REQ-HAZARD-WIRE-UNTAGGED-RECORD] -->\n  990│- **Failure (caught in design, W4 T10 / releases#53 — before it shipped, by asking what an N-1 daemon actually does rather than assuming it would simply not understand):** the stream dispatcher classifies a feed by sniffing its FIRST LINE, and the message family is deliberately **untagged** — `classify_first_line` reads any first line carrying `target` + `op_id` with no `kind` as `StreamFamily::WanMsg`. A newly minted record type that omits a `kind` tag is therefore not \"unrecognized\" on an older pee...\n  991│- **Invariant:** every wire record minted from here on carries a `kind` tag routed by its own tagged arm, so an older daemon's `Some(_)` catch-all resolves it to `StreamFamily::Unknown` and DROPS it cleanly. The correct degrade for a request an old peer cannot serve is that it **never arrives** — never that it arrives as something else. A record whose absence is silent is recoverable; a record delivered as the wrong kind is not.\n *992│- **Proof obligation — three parts, or the pin goes vacuous:** (a) this version routes the record to its own family, AND is asserted NOT to route it to the message family; (b) the same bytes under a tag this version does not know resolve to `Unknown`; (c) the untagged shape is asserted to genuinely BE claimed by the message arm. Without (c) the test passes for the wrong reason the day the record's fields drift such that it would no longer have collided — the guard would then be measuring nothing whil...\n  993│- **spt-core mapping:** `spt-net/src/net/knockmsg.rs` (`KNOCK_KIND`, the tag on `KnockRecord`); `spt-daemon/src/dispatch.rs` `classify_first_line` (the tagged arm) + `StreamFamily::Unknown` (the clean drop); unit `a_knock_record_routes_here_and_drops_cleanly_on_an_n1_daemon` (all three parts, against the real classifier).\n  994│- **Source:** W4 knocking wave (releases#53 T10; doyle-ruled 2026-07-31 as a hazard CLASS rather than a one-off, since every future wire record faces the same trap).\n  995│\n     │...\n  997│\n *998│### 7.63 A dedupe key is never narrower than the identity of the record it replaces — a shared id with two owners must not collapse to one  `[REQ-HAZARD-MUTUAL-PREAUTH-SIBLING]`\n  999│\n 1000│<!-- [doc->REQ-HAZARD-MUTUAL-PREAUTH-SIBLING] -->\n*1001│- **Failure (DOORBELL, releases#83/#79 lane; the near-miss is the entry's point):** arming a mutual pre-authorization deliberately REPLACES rather than stacks, so that a re-knock cannot accumulate duplicate reverse grants. The dedupe is correct; its KEY is where the trap sits. One code id is legitimately held by **two** pre-authorizations with different owners — the minter's, armed at mint with an empty subject because no redeemer exists yet, and the redeemer's own, armed at redemption with the subject...\n 1002│- **Invariant:** a replace-in-place keyed lookup states the FULL identity of the record it replaces — here `(kind, key_id, owner)`. Where a key is deliberately NARROWER than a record's identity, that narrowness is a decision the seam states in words, and a test pins that two records sharing the narrow key both survive. Silent replacement is a forbidden outcome; refusing, stacking, or replacing are all defensible, but only if the seam chose one.\n 1003│- **The asymmetry is deliberate — do not \"fix\" it into agreement:** `consume_redeemed` filters on `(kind, key_id)` *without* the owner precisely because one redemption must make BOTH siblings due at once, each resolving its own subject. `armed_mutual`/`consume_mutual` also omit the owner, and are correct to: they serve the knock keyspace, where only the knocker's single record ever exists. Three seams, two key widths, one rule — the width matches what the seam is addressing, not what its neighbour us...\n 1004│- **spt-core mapping:** `spt-store/src/knock.rs` `arm_mutual` (the owner-inclusive match, with the rule stated in its own doc comment) beside `consume_redeemed` (the owner-blind consumption that takes both siblings); unit `a_redemption_consumes_what_it_makes_due_and_nothing_else` — genuinely discriminating rather than merely adjacent, since dropping `owner` from the arming key makes its `due.len() == 2` assertion fail.\n*1005│- **Source:** DOORBELL milestone (W2 `--mutual`/`--one-way` forced choice + W4 sealed-code redemption; doyle-ruled at the golden-head hand-off 2026-08-01 as a hazard CLASS — the trap is any dedupe whose key is narrower than its record's identity, and this milestone is simply where it was paid for). Kin 7.61 (the intuitive spelling being the one the fault satisfies).\n 1006│\n 1007│---\n 1008│\n*1009│### 7.64 Session-scoped authority does not survive a session-id rotation at rest — a boundary CARRIES it as a move, never leaves a copy  `[REQ-HAZARD-EMPOWER-SESSION-RESURRECT]`\n 1010│\n 1011│<!-- [doc->REQ-HAZARD-EMPOWER-SESSION-RESURRECT] -->\n 1012│- **Failure (LOCKSMITH t1, releases#117; MEASURED by red probe 2026-08-03, not inferred):** an engine-room empowerment is session-state — ADR-0052 says \"nothing to revoke at rest\" — and is filed under the session id the engine room's perch is bound to. `spt api boundary` rotates exactly that id, to an ARBITRARY `--to-session-id` past a guard that refuses only psyche-custody squats, and it is NEITHER of the two revocation edges ADR-0052 §3a names. The record was therefore left filed under the OLD ses...\n     │...\n 1018│- **spt-core mapping:** `spt-store/src/empower.rs` `carry_session`/`carry_between`/`neutralize_at` (the move, the two barriers, and the injectable removal seam the failure arm is only reachable through); `spt/src/api/reporting.rs` `cmd_boundary` (the ER-gated call, the old sid captured under the SAME lock as the rotation, and the loud lines — it never fails the boundary, because the harness has already rotated and a refusal would desync the record from the reality it describes); ADR-0052 §3c. Units: `...\n*1019│- **Source:** LOCKSMITH tranche 1 (releases#117; doyle-ruled 2026-08-03 — carry-as-move, fail-closed, a NEW third named ADR edge rather than a stretch of the existing text, no widening of the hygiene sweep). Kin 7.51 (a recycled identity must not read as the original), 7.63 (a key narrower than the identity of what it addresses).\n 1020│\n 1021│---\n 1022│","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789394586243}}
{"type":"message","id":"006e3790","parentId":"7ad966e3","timestamp":"2026-09-14T14:03:06.444Z","message":{"role":"toolResult","toolCallId":"call_6CwYQ8fgO11451OTmlQQcyJ0|fc_0a63d1e8a5b46c9c016aa7fe9a1ea487d1931ac206b32662ab","toolName":"glob","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/docs/adr/\n0041-endpoint-lifecycle-truth.md"}],"details":{"scopePath":".worktrees/297-enforcement-shape/docs/adr/*0041*, .worktrees/297-enforcement-shape/**/*DOMAIN*, .worktrees/297-enforcement-shape/**/*domain*.md","fileCount":1,"files":[".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789394586443}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dCylxjG01PgH375vgxsAJNw9|fc_0a63d1e8a5b46c9c016aa7fea2499087d1864cbe4dc8db0740","toolName":"read","startedAt":"2026-09-14T14:03:14.379Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs:498-569"},"intent":"Reading request reply wire contracts"},"id":"5576cbe5","parentId":"006e3790","timestamp":"2026-09-14T14:03:14.379Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_VEpPRPQOEHLoyjvWxbvWxr86|fc_0a63d1e8a5b46c9c016aa7fea249a087d18f1166370c63b764","toolName":"read","startedAt":"2026-09-14T14:03:14.379Z","args":{"path":".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md:1-135"},"intent":"Reading lifecycle decision"},"id":"0996f5c4","parentId":"5576cbe5","timestamp":"2026-09-14T14:03:14.379Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GDDitYHudyHqh9djCrN0Woh2|fc_0a63d1e8a5b46c9c016aa7fea249a887d1bc60cf7a58ecd9d7","toolName":"read","startedAt":"2026-09-14T14:03:14.379Z","args":{"path":".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md:501-541"},"intent":"Reading deadline hazard constraints"},"id":"116c52aa","parentId":"0996f5c4","timestamp":"2026-09-14T14:03:14.379Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lhIvcfo8bxAaCJamYh6v2sMl|fc_0a63d1e8a5b46c9c016aa7fea249b087d1883a0772c73d22c2","toolName":"grep","startedAt":"2026-09-14T14:03:14.379Z","args":{"path":".worktrees/297-enforcement-shape/crates"},"intent":"Locating envelope wire definition"},"id":"15344658","parentId":"116c52aa","timestamp":"2026-09-14T14:03:14.379Z"}
{"type":"message","id":"b48c81c2","parentId":"15344658","timestamp":"2026-09-14T14:03:14.379Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_dCylxjG01PgH375vgxsAJNw9|fc_0a63d1e8a5b46c9c016aa7fea2499087d1864cbe4dc8db0740","name":"read","arguments":{"i":"Reading request reply wire contracts","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs:498-569"},"intent":"Reading request reply wire contracts"},{"type":"toolCall","id":"call_VEpPRPQOEHLoyjvWxbvWxr86|fc_0a63d1e8a5b46c9c016aa7fea249a087d18f1166370c63b764","name":"read","arguments":{"i":"Reading lifecycle decision","path":".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md:1-135"},"intent":"Reading lifecycle decision"},{"type":"toolCall","id":"call_GDDitYHudyHqh9djCrN0Woh2|fc_0a63d1e8a5b46c9c016aa7fea249a887d1bc60cf7a58ecd9d7","name":"read","arguments":{"i":"Reading deadline hazard constraints","path":".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md:501-541"},"intent":"Reading deadline hazard constraints"},{"type":"toolCall","id":"call_lhIvcfo8bxAaCJamYh6v2sMl|fc_0a63d1e8a5b46c9c016aa7fea249b087d1883a0772c73d22c2","name":"grep","arguments":{"i":"Locating envelope wire definition","pattern":"struct Envelope","path":".worktrees/297-enforcement-shape/crates","case":true,"gitignore":false,"skip":null},"intent":"Locating envelope wire definition"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":6557,"output":217,"cacheRead":49664,"cacheWrite":0,"totalTokens":56438,"reasoningTokens":19,"cost":{"input":0.06557,"output":0.01085,"cacheRead":0.049664,"cacheWrite":0,"total":0.126084}},"stopReason":"toolUse","timestamp":1789394586451,"responseId":"resp_0a63d1e8a5b46c9c016aa7fe9aa88087d18ea1d53918a80bf7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7fe9bd2c887d199b8bf5635fcefcf","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_6iVgALdT9zfO8rBNipdkq4Jysqd041XyK_WrnoRupef4AcW6ii1_W9diEajKMJ82w-QUXZ2361LRbczGBl3rjs94yUeKUefwcOPKpzdTky-bJsFbhETHtEPdqdsuqflT9L3ifPxHCAymffd9ggLLyHD64QSmRnYZYmtfHvlrpoVWVazSUiOxxQ4xwaW7jK0Zbx5yZ2RriLmu64kSsz0vQCxHp-p0XRNye5ry6tpkJkL8yuiH4BFEGrFcuw_7ML8ZNrO2vOcoKz3Ehpd9GQD2IYyZTAYnNHC1trPNiFwA-T2Vk4TicMajSsfEnPGm6SEkcbunYsJ3W0vvyL11Y-WncjwSjVu73_4ySNG6N8G87eCKupemFR9ZFBTxM74rN4YQCQPTNz_Jk-3VFf_PxX662Nbq0U97jkdCKLJA3Fkf_Vm2SQsYjnb0LWGAgcF3W3EjT1wfcZtfYEKmjB6SgM4MxQFcOEfH1wBacvG5VkeWOfsrdn-w9ejqCrrGrL7dmLnn-lJLRpss5xW6rAdUMC4aZfYsfzvq1_fx2vE4oQiFdlw6ml8QescCjaBbOgFtFdNkj9ImpUCTWDfFASgbzbJFnPYCwn4r6h0UNuEsLhELKJnqaHTSmamSGSlZWyqhuqfQSYeShS9eaVS-XggFiJkEVUA_V0W4RNi5FdTTNN9GfWxqVsSk5kCakfcnMU1D5sBkS4YkIf5MX5tg43Q_P2LdF8--gd1gRNdoHRA65R8DSvavYNQnoyNFyB4eRccC2JMnzuqsk7Se6wMiRHkUO8unYiVkFxBuZ6ykToAtlnvcJH69s86EeGeR5iaCO3VlI2mQSw650Ue6LXt81CtzpDysfyTenR7FtKaWOKKsSp5N22EmEeiHiWYUuaW_87Y65LVq1I59jy10Q2f7Cr8n8AxM1iUR2HeddLkiJrgU02aBBJoJ_r7rgD_1iLNIYrdEDwx9Y1OXA5B0v2Qo-mhiDencjJ2Ca6l62hR5WNZvp_H_W4XgnjQfIGFeJYR5RiaJt8rETmr91ui0gqOvFSywitraOA-JmdUfEVDmcUUoKgjpIIycCjD0EliYmaXkIxZIvv3qI7blHD6k140FmhAeSXYb9DRQ_f6cgJ3OBJPeLD_0iy8oCVlCflx4b7ZnZn0pCW4eqLwiqTavDfvtfqDEF40KW7MJ9GlN1YkcDvLFPC4kYg8JCrJeODaPi2M7geIMdDa074pqWu3njfiMMIOjiP-3p9ATjN9XqMiQ-sd2Oy82k8zVZFi5OalHqkmOdbNtPFD2CweBnijc7881XIx-rLOAkZ2dFnk-tStDxTQlkikzUENx7AMD7gO23NGRQLyKj2vB_SWQinyt1cyt4Stp9N_EbITw==","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"summary":[],"metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fea2499087d1864cbe4dc8db0740","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading request reply wire contracts\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs:498-569\"}","call_id":"call_dCylxjG01PgH375vgxsAJNw9","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fea249a087d18f1166370c63b764","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading lifecycle decision\",\"path\":\".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md:1-135\"}","call_id":"call_VEpPRPQOEHLoyjvWxbvWxr86","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fea249a887d1bc60cf7a58ecd9d7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading deadline hazard constraints\",\"path\":\".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md:501-541\"}","call_id":"call_GDDitYHudyHqh9djCrN0Woh2","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fea249b087d1883a0772c73d22c2","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating envelope wire definition\",\"pattern\":\"struct Envelope\",\"path\":\".worktrees/297-enforcement-shape/crates\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_lhIvcfo8bxAaCJamYh6v2sMl","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":7926.333799988031,"ttft":1413.8437999933958,"completedAt":1789394594378,"contextSnapshot":{"promptTokens":56221,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"50e43810","parentId":"b48c81c2","timestamp":"2026-09-14T14:03:14.384Z","message":{"role":"toolResult","toolCallId":"call_VEpPRPQOEHLoyjvWxbvWxr86|fc_0a63d1e8a5b46c9c016aa7fea249a087d18f1166370c63b764","toolName":"read","content":[{"type":"text","text":"# ADR-0041: Endpoint lifecycle truth — online classification, terminal normalization, stamp convergence, truthful create\n\n- Status: accepted (doyle triage 2026-07-17, REGISTRY-LIFECYCLE milestone)\n- Source: three hertz reports (2026-07-16, doyle seam-verified same day — emphasys triple,\n  rest-normalize, spawn-fresh) + the operator-field deployah stop/run wedge. KNOWN-HAZARDS\n  7.45. Kin: ADR-0027 (unbound endpoint state), ADR-0033 (wake-resume via reconcile\n  intent), KH 7.27/7.29.\n\n## Context\n\nFour proven defect families share one root shape: **endpoint lifecycle state is written by\nmultiple non-converging paths, and several of them stamp optimistic state that no\nauthority ever verifies or normalizes.**\n\n1. **Hybrid false-ONLINE (emphasys C1).** `cmd_listen` stamps `status=online` from manifest\n   `psyche_init` alone (live_capable) — no persisted-state or hosting-authority check —\n   minting `ready_agent`/`controllable=false` hybrid rows born online-authoritative. The\n   livehost reconcile skips `state != live_agent` rows AND gates cleanup on\n   `controllable=true`, so a dead-PID hybrid survives every restart; `is_perch_alive`\n   trusts `status=online` and skips dead-PID rejection — every reader faithfully\n   resurrects the lie.\n2. **Zombie WAKE_RESUME (rest-normalize).** `rest_state` is intent, distinct from liveness.\n   Definitive session death (broker session loss, liveness reap, `cmd_stop`) stamps\n   `status=offline` ONLY — never touches `rest_state`/`dormant_since_ms`. A dead session\n   with preserved `Active` intent = an outstanding wake order: reconcile offlines it, a\n   later pass RESUMES it from the ledger, repeatedly, across generations (perri field:\n   repeated `WAKE_RESUME` loops).\n3. **Poll-vs-reap relatch (emphasys C3).** `KIND_SESSIONS` snapshots controller truth under\n   lock but writes stamps after unlock; an exit-waiter reap in between lets the stale poll\n   relatch `controlled=true` on a session that just died.\n4. **Untruthful create + contradictory liveness (spawn-fresh + cycle wedge).**\n   `endpoint run --create` promises a fresh session but dispatch discards `create`; the\n   broker dedups every labeled SpawnReq into `Spawned(existing)` with no disposition — the\n   CLI prints success for a spawn that never happened. Meanwhile the ALREADY_LIVE dup-guard\n   and the shutdown state machine consult DIFFERENT liveness sources and contradict each\n   other on the same zombie (deployah wedge: run says ALREADY_LIVE, list says OFFLINE,\n   shutdown says NO_EDGE).\n\n## Decision\n\n<!-- [doc->REQ-ENDPOINT-ONLINE-TRUTH] -->\n1. **Online is earned, not declared.** A creator may stamp `status=online` only from actual\n   persisted state + hosting authority — never from manifest capability alone. Legacy\n   hybrid rows self-heal at reconcile, but only after a SUCCESSFUL broker query: a broker\n   failure is never interpreted as an empty session set (no mass-offline on a hiccup).\n2. **Control cleanup splits from offline classification.** Reconcile clears\n   `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth —\n   regardless of state or controllability — while offline classification keeps its narrow\n   gate (live_agent + controllable). A row's classification quirk can never shield stale\n   control stamps.\n<!-- [doc->REQ-REST-TERMINAL-NORMALIZE] -->\n3. **Definitive death normalizes terminally and atomically.** One store-level mutation —\n   `status=offline` + `rest_state=suspended` + clear `dormant_since_ms` in the SAME\n   info.json write — invoked at every definitive hosted-session-loss point (authoritative\n   broker session loss, liveness reap, `cmd_stop`). Explicitly NOT\n   `daemon_rest_event(Suspend)` (no edge when effective state is already Suspended — the\n   raw Active intent survives), and NOT a reader-side blanket offline⇒suspended (that\n   destroys explicit-Wake semantics: wake writes intent first, reconcile consumes it).\n   Graceful shutdown keeps its echo-before-teardown; this is terminal normalization for\n   non-graceful death.\n<!-- [doc->REQ-CONTROL-STAMP-CONVERGENCE] -->\n4. **Control stamps converge, validated.** Stamp writes carry session/generation validation\n   (or serialize per-endpoint stamp transitions) so a pre-reap snapshot can never overwrite\n   a post-reap truth. No fs I/O under global locks (KH 7.12). Every no-session path clears\n   all three control fields.\n<!-- [doc->REQ-SPAWN-FRESH-TRUTHFUL] -->\n5. **Create is truthful at the broker, atomically.** Spawn policies split:\n   Fresh/RejectExisting vs ResumeOrReuse. New `KIND_SPAWN_FRESH` wire verb with a TYPED\n   conflict outcome (a distinct kind fails loud on an N-1 broker; a serde-defaulted field\n   would be silently ignored — wire discipline), sharing the single-flight/PTY spawn body.\n   Fresh at AlreadyLive/lost-race returns the conflict, never `Spawned(existing)`. CLI:\n   exit 1 + stable `ENDPOINT_CREATE_CONFLICT:<id>:…no session was created`, no attach, no\n   state mutation, never kills the live session. Omitted `--resume` keeps the documented\n   create-default (fresh-and-conflict, same as explicit `--create`); ensure/attach would be\n   a NEW documented mode, never a silent overload. Preflight stays UX-only (cannot close\n   TOCTOU).\n<!-- [doc->REQ-ENDPOINT-CYCLE-HONEST] -->\n6. **One liveness authority for cycle verbs.** The ALREADY_LIVE dup-guard liveness-probes\n   the claimed session's client tree before refusing (dead tree ⇒ reap + respawn\n   honestly); the shutdown state machine consults the same source, so \"is it live\" has one\n   answer (no ALREADY_LIVE/OFFLINE/NO_EDGE three-way contradiction).\n\n<!-- [doc->REQ-BROKER-ZOMBIE-IDENTITY] -->\n   **Zombie-kill identity guard (releases#285).** Liveness is not permission to\n   kill a remembered PID. The broker records the root's OS creation time just\n   after PTY spawn, before starting its exit waiter. The child is still owned:\n   on Windows its handle is retained; on Unix it has not been reaped, so its\n   number cannot yet be recycled. The expected identity is never refreshed at\n   reap time. Start time is preferred over image because a launcher can `exec`\n   another image without becoming a different process.\n\n   At the zombie kill boundary, a fresh owned-process death probe permits\n   stale-row cleanup for a dead root **without any PID/tree kill**. A fresh\n   start-time query that differs from the captured stamp also proves the\n   remembered child is gone: `SPAWN_ZOMBIE_REAP_RECYCLED` records both births,\n   skips signaling the replacement process, and proceeds through the existing\n   bounded de-table and stamp-clear loop so the next spawn can succeed.\n   Only a matching live birth permits tree-kill.\n   Unknown PID or missing/unreadable identity emits\n   `SPAWN_ZOMBIE_REAP_DECLINE` and fails the spawn request **before** the forced\n   de-table loop. There is no fallback `session.kill`, teardown dispatch, or\n   session-resource drop on refusal; the old row remains for its normal exit\n   waiter. This trades automatic recovery of an **unverifiable** live-number\n   row for safety, but never strands a row whose recorded birth is proven gone.\n   Fresh-dead cleanup retains the existing snapshot-less Unix liveness fallback.\n\n   **Limits:** this is a checked numeric tree-kill, not an atomic handle/pidfd\n   tree termination. A process can exit and its PID can be reused after the\n   identity query and before a signal; descendant enumeration/kill retains the\n   same race. Linux start times have clock-tick resolution, so same-tick reuse\n   is indistinguishable; Windows uses creation FILETIME. This scoped guard does\n   not change generic `kill_pid_tree` or other kill sites.\n\n## Consequences\n\n- A dead endpoint converges to offline+suspended+uncontrolled within one reconcile, from\n  every death path, and stays there until an explicit wake or run — no resurrection loops,\n  no zombie control latches, no immortal hybrid rows.\n- `--create` becomes a contract: success ⇒ a fresh session exists; conflict ⇒ typed, loud,\n  side-effect-free. Existing no-dup safety tests remain valid (they codify the dedup body\n  this shares).\n- New wire surface: `KIND_SPAWN_FRESH` (additive verb). N-1 broker answers unknown-kind ⇒\n  CLI reports the loud IPC failure, never a fake Created.\n- Explicit-Wake semantics are preserved by construction (intent normalization happens only\n  on definitive death observations, never speculatively in readers).\n\n## Requirements\n\n`REQ-ENDPOINT-ONLINE-TRUTH`, `REQ-REST-TERMINAL-NORMALIZE`, `REQ-CONTROL-STAMP-CONVERGENCE`,\n`REQ-STREAM-LEASE-CLASSES` (mechanism in ADR-0040 decision 6),\n`REQ-HAZARD-ENDPOINT-LIFECYCLE` (KH 7.45); W3: `REQ-SPAWN-FRESH-TRUTHFUL`,\n`REQ-ENDPOINT-CYCLE-HONEST`.\n\n<!-- [doc->REQ-HOSTING-AUTHORITY-CONTROLLABLE] -->\n## Amendment (RC-RENDER-TRUTH v0.38.1 leg 3, doyle ruling 2026-07-19): one hosting authority — the online-earn authority splits by hosting topology\n\nPersisted `state` is the durable endpoint TYPE (REQ-EP-6 open type system;\n`establish_perch`'s prior-type preserve is INTENTIONAL and stays). The source-definitive\nBROKER-PTY hosting authority is `controllable == Some(true)`, NOT `state`.\n…\nRequirement: `REQ-HOSTING-AUTHORITY-CONTROLLABLE`.\n\n[Showing lines 1-138 of 182. Use :139 to continue]"}],"details":{"truncation":{"content":"# ADR-0041: Endpoint lifecycle truth — online classification, terminal normalization, stamp convergence, truthful create\r\n\r\n- Status: accepted (doyle triage 2026-07-17, REGISTRY-LIFECYCLE milestone)\r\n- Source: three hertz reports (2026-07-16, doyle seam-verified same day — emphasys triple,\r\n  rest-normalize, spawn-fresh) + the operator-field deployah stop/run wedge. KNOWN-HAZARDS\r\n  7.45. Kin: ADR-0027 (unbound endpoint state), ADR-0033 (wake-resume via reconcile\r\n  intent), KH 7.27/7.29.\r\n\r\n## Context\r\n\r\nFour proven defect families share one root shape: **endpoint lifecycle state is written by\r\nmultiple non-converging paths, and several of them stamp optimistic state that no\r\nauthority ever verifies or normalizes.**\r\n\r\n1. **Hybrid false-ONLINE (emphasys C1).** `cmd_listen` stamps `status=online` from manifest\r\n   `psyche_init` alone (live_capable) — no persisted-state or hosting-authority check —\r\n   minting `ready_agent`/`controllable=false` hybrid rows born online-authoritative. The\r\n   livehost reconcile skips `state != live_agent` rows AND gates cleanup on\r\n   `controllable=true`, so a dead-PID hybrid survives every restart; `is_perch_alive`\r\n   trusts `status=online` and skips dead-PID rejection — every reader faithfully\r\n   resurrects the lie.\r\n2. **Zombie WAKE_RESUME (rest-normalize).** `rest_state` is intent, distinct from liveness.\r\n   Definitive session death (broker session loss, liveness reap, `cmd_stop`) stamps\r\n   `status=offline` ONLY — never touches `rest_state`/`dormant_since_ms`. A dead session\r\n   with preserved `Active` intent = an outstanding wake order: reconcile offlines it, a\r\n   later pass RESUMES it from the ledger, repeatedly, across generations (perri field:\r\n   repeated `WAKE_RESUME` loops).\r\n3. **Poll-vs-reap relatch (emphasys C3).** `KIND_SESSIONS` snapshots controller truth under\r\n   lock but writes stamps after unlock; an exit-waiter reap in between lets the stale poll\r\n   relatch `controlled=true` on a session that just died.\r\n4. **Untruthful create + contradictory liveness (spawn-fresh + cycle wedge).**\r\n   `endpoint run --create` promises a fresh session but dispatch discards `create`; the\r\n   broker dedups every labeled SpawnReq into `Spawned(existing)` with no disposition — the\r\n   CLI prints success for a spawn that never happened. Meanwhile the ALREADY_LIVE dup-guard\r\n   and the shutdown state machine consult DIFFERENT liveness sources and contradict each\r\n   other on the same zombie (deployah wedge: run says ALREADY_LIVE, list says OFFLINE,\r\n   shutdown says NO_EDGE).\r\n\r\n## Decision\r\n\r\n<!-- [doc->REQ-ENDPOINT-ONLINE-TRUTH] -->\r\n1. **Online is earned, not declared.** A creator may stamp `status=online` only from actual\r\n   persisted state + hosting authority — never from manifest capability alone. Legacy\r\n   hybrid rows self-heal at reconcile, but only after a SUCCESSFUL broker query: a broker\r\n   failure is never interpreted as an empty session set (no mass-offline on a hiccup).\r\n2. **Control cleanup splits from offline classification.** Reconcile clears\r\n   `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth —\r\n   regardless of state or controllability — while offline classification keeps its narrow\r\n   gate (live_agent + controllable). A row's classification quirk can never shield stale\r\n   control stamps.\r\n<!-- [doc->REQ-REST-TERMINAL-NORMALIZE] -->\r\n3. **Definitive death normalizes terminally and atomically.** One store-level mutation —\r\n   `status=offline` + `rest_state=suspended` + clear `dormant_since_ms` in the SAME\r\n   info.json write — invoked at every definitive hosted-session-loss point (authoritative\r\n   broker session loss, liveness reap, `cmd_stop`). Explicitly NOT\r\n   `daemon_rest_event(Suspend)` (no edge when effective state is already Suspended — the\r\n   raw Active intent survives), and NOT a reader-side blanket offline⇒suspended (that\r\n   destroys explicit-Wake semantics: wake writes intent first, reconcile consumes it).\r\n   Graceful shutdown keeps its echo-before-teardown; this is terminal normalization for\r\n   non-graceful death.\r\n<!-- [doc->REQ-CONTROL-STAMP-CONVERGENCE] -->\r\n4. **Control stamps converge, validated.** Stamp writes carry session/generation validation\r\n   (or serialize per-endpoint stamp transitions) so a pre-reap snapshot can never overwrite\r\n   a post-reap truth. No fs I/O under global locks (KH 7.12). Every no-session path clears\r\n   all three control fields.\r\n<!-- [doc->REQ-SPAWN-FRESH-TRUTHFUL] -->\r\n5. **Create is truthful at the broker, atomically.** Spawn policies split:\r\n   Fresh/RejectExisting vs ResumeOrReuse. New `KIND_SPAWN_FRESH` wire verb with a TYPED\r\n   conflict outcome (a distinct kind fails loud on an N-1 broker; a serde-defaulted field\r\n   would be silently ignored — wire discipline), sharing the single-flight/PTY spawn body.\r\n   Fresh at AlreadyLive/lost-race returns the conflict, never `Spawned(existing)`. CLI:\r\n   exit 1 + stable `ENDPOINT_CREATE_CONFLICT:<id>:…no session was created`, no attach, no\r\n   state mutation, never kills the live session. Omitted `--resume` keeps the documented\r\n   create-default (fresh-and-conflict, same as explicit `--create`); ensure/attach would be\r\n   a NEW documented mode, never a silent overload. Preflight stays UX-only (cannot close\r\n   TOCTOU).\r\n<!-- [doc->REQ-ENDPOINT-CYCLE-HONEST] -->\r\n6. **One liveness authority for cycle verbs.** The ALREADY_LIVE dup-guard liveness-probes\r\n   the claimed session's client tree before refusing (dead tree ⇒ reap + respawn\r\n   honestly); the shutdown state machine consults the same source, so \"is it live\" has one\r\n   answer (no ALREADY_LIVE/OFFLINE/NO_EDGE three-way contradiction).\r\n\r\n<!-- [doc->REQ-BROKER-ZOMBIE-IDENTITY] -->\r\n   **Zombie-kill identity guard (releases#285).** Liveness is not permission to\r\n   kill a remembered PID. The broker records the root's OS creation time just\r\n   after PTY spawn, before starting its exit waiter. The child is still owned:\r\n   on Windows its handle is retained; on Unix it has not been reaped, so its\r\n   number cannot yet be recycled. The expected identity is never refreshed at\r\n   reap time. Start time is preferred over image because a launcher can `exec`\r\n   another image without becoming a different process.\r\n\r\n   At the zombie kill boundary, a fresh owned-process death probe permits\r\n   stale-row cleanup for a dead root **without any PID/tree kill**. A fresh\r\n   start-time query that differs from the captured stamp also proves the\r\n   remembered child is gone: `SPAWN_ZOMBIE_REAP_RECYCLED` records both births,\r\n   skips signaling the replacement process, and proceeds through the existing\r\n   bounded de-table and stamp-clear loop so the next spawn can succeed.\r\n   Only a matching live birth permits tree-kill.\r\n   Unknown PID or missing/unreadable identity emits\r\n   `SPAWN_ZOMBIE_REAP_DECLINE` and fails the spawn request **before** the forced\r\n   de-table loop. There is no fallback `session.kill`, teardown dispatch, or\r\n   session-resource drop on refusal; the old row remains for its normal exit\r\n   waiter. This trades automatic recovery of an **unverifiable** live-number\r\n   row for safety, but never strands a row whose recorded birth is proven gone.\r\n   Fresh-dead cleanup retains the existing snapshot-less Unix liveness fallback.\r\n\r\n   **Limits:** this is a checked numeric tree-kill, not an atomic handle/pidfd\r\n   tree termination. A process can exit and its PID can be reused after the\r\n   identity query and before a signal; descendant enumeration/kill retains the\r\n   same race. Linux start times have clock-tick resolution, so same-tick reuse\r\n   is indistinguishable; Windows uses creation FILETIME. This scoped guard does\r\n   not change generic `kill_pid_tree` or other kill sites.\r\n\r\n## Consequences\r\n\r\n- A dead endpoint converges to offline+suspended+uncontrolled within one reconcile, from\r\n  every death path, and stays there until an explicit wake or run — no resurrection loops,\r\n  no zombie control latches, no immortal hybrid rows.\r\n- `--create` becomes a contract: success ⇒ a fresh session exists; conflict ⇒ typed, loud,\r\n  side-effect-free. Existing no-dup safety tests remain valid (they codify the dedup body\r\n  this shares).\r\n- New wire surface: `KIND_SPAWN_FRESH` (additive verb). N-1 broker answers unknown-kind ⇒\r\n  CLI reports the loud IPC failure, never a fake Created.\r\n- Explicit-Wake semantics are preserved by construction (intent normalization happens only\r\n  on definitive death observations, never speculatively in readers).\r\n\r\n## Requirements\r\n\r\n`REQ-ENDPOINT-ONLINE-TRUTH`, `REQ-REST-TERMINAL-NORMALIZE`, `REQ-CONTROL-STAMP-CONVERGENCE`,\r\n`REQ-STREAM-LEASE-CLASSES` (mechanism in ADR-0040 decision 6),\r\n`REQ-HAZARD-ENDPOINT-LIFECYCLE` (KH 7.45); W3: `REQ-SPAWN-FRESH-TRUTHFUL`,\r\n`REQ-ENDPOINT-CYCLE-HONEST`.\r\n\r\n<!-- [doc->REQ-HOSTING-AUTHORITY-CONTROLLABLE] -->\r\n## Amendment (RC-RENDER-TRUTH v0.38.1 leg 3, doyle ruling 2026-07-19): one hosting authority — the online-earn authority splits by hosting topology\r\n\r\nPersisted `state` is the durable endpoint TYPE (REQ-EP-6 open type system;\r\n`establish_perch`'s prior-type preserve is INTENTIONAL and stays). The source-definitive\r","truncated":true,"truncatedBy":"lines","totalLines":182,"totalBytes":9240,"outputLines":138,"outputBytes":9240,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":182,"displayContent":{"text":"# ADR-0041: Endpoint lifecycle truth — online classification, terminal normalization, stamp convergence, truthful create\r\n\r\n- Status: accepted (doyle triage 2026-07-17, REGISTRY-LIFECYCLE milestone)\r\n- Source: three hertz reports (2026-07-16, doyle seam-verified same day — emphasys triple,\r\n  rest-normalize, spawn-fresh) + the operator-field deployah stop/run wedge. KNOWN-HAZARDS\r\n  7.45. Kin: ADR-0027 (unbound endpoint state), ADR-0033 (wake-resume via reconcile\r\n  intent), KH 7.27/7.29.\r\n\r\n## Context\r\n\r\nFour proven defect families share one root shape: **endpoint lifecycle state is written by\r\nmultiple non-converging paths, and several of them stamp optimistic state that no\r\nauthority ever verifies or normalizes.**\r\n\r\n1. **Hybrid false-ONLINE (emphasys C1).** `cmd_listen` stamps `status=online` from manifest\r\n   `psyche_init` alone (live_capable) — no persisted-state or hosting-authority check —\r\n   minting `ready_agent`/`controllable=false` hybrid rows born online-authoritative. The\r\n   livehost reconcile skips `state != live_agent` rows AND gates cleanup on\r\n   `controllable=true`, so a dead-PID hybrid survives every restart; `is_perch_alive`\r\n   trusts `status=online` and skips dead-PID rejection — every reader faithfully\r\n   resurrects the lie.\r\n2. **Zombie WAKE_RESUME (rest-normalize).** `rest_state` is intent, distinct from liveness.\r\n   Definitive session death (broker session loss, liveness reap, `cmd_stop`) stamps\r\n   `status=offline` ONLY — never touches `rest_state`/`dormant_since_ms`. A dead session\r\n   with preserved `Active` intent = an outstanding wake order: reconcile offlines it, a\r\n   later pass RESUMES it from the ledger, repeatedly, across generations (perri field:\r\n   repeated `WAKE_RESUME` loops).\r\n3. **Poll-vs-reap relatch (emphasys C3).** `KIND_SESSIONS` snapshots controller truth under\r\n   lock but writes stamps after unlock; an exit-waiter reap in between lets the stale poll\r\n   relatch `controlled=true` on a session that just died.\r\n4. **Untruthful create + contradictory liveness (spawn-fresh + cycle wedge).**\r\n   `endpoint run --create` promises a fresh session but dispatch discards `create`; the\r\n   broker dedups every labeled SpawnReq into `Spawned(existing)` with no disposition — the\r\n   CLI prints success for a spawn that never happened. Meanwhile the ALREADY_LIVE dup-guard\r\n   and the shutdown state machine consult DIFFERENT liveness sources and contradict each\r\n   other on the same zombie (deployah wedge: run says ALREADY_LIVE, list says OFFLINE,\r\n   shutdown says NO_EDGE).\r\n\r\n## Decision\r\n\r\n<!-- [doc->REQ-ENDPOINT-ONLINE-TRUTH] -->\r\n1. **Online is earned, not declared.** A creator may stamp `status=online` only from actual\r\n   persisted state + hosting authority — never from manifest capability alone. Legacy\r\n   hybrid rows self-heal at reconcile, but only after a SUCCESSFUL broker query: a broker\r\n   failure is never interpreted as an empty session set (no mass-offline on a hiccup).\r\n2. **Control cleanup splits from offline classification.** Reconcile clears\r\n   `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth —\r\n   regardless of state or controllability — while offline classification keeps its narrow\r\n   gate (live_agent + controllable). A row's classification quirk can never shield stale\r\n   control stamps.\r\n<!-- [doc->REQ-REST-TERMINAL-NORMALIZE] -->\r\n3. **Definitive death normalizes terminally and atomically.** One store-level mutation —\r\n   `status=offline` + `rest_state=suspended` + clear `dormant_since_ms` in the SAME\r\n   info.json write — invoked at every definitive hosted-session-loss point (authoritative\r\n   broker session loss, liveness reap, `cmd_stop`). Explicitly NOT\r\n   `daemon_rest_event(Suspend)` (no edge when effective state is already Suspended — the\r\n   raw Active intent survives), and NOT a reader-side blanket offline⇒suspended (that\r\n   destroys explicit-Wake semantics: wake writes intent first, reconcile consumes it).\r\n   Graceful shutdown keeps its echo-before-teardown; this is terminal normalization for\r\n   non-graceful death.\r\n<!-- [doc->REQ-CONTROL-STAMP-CONVERGENCE] -->\r\n4. **Control stamps converge, validated.** Stamp writes carry session/generation validation\r\n   (or serialize per-endpoint stamp transitions) so a pre-reap snapshot can never overwrite\r\n   a post-reap truth. No fs I/O under global locks (KH 7.12). Every no-session path clears\r\n   all three control fields.\r\n<!-- [doc->REQ-SPAWN-FRESH-TRUTHFUL] -->\r\n5. **Create is truthful at the broker, atomically.** Spawn policies split:\r\n   Fresh/RejectExisting vs ResumeOrReuse. New `KIND_SPAWN_FRESH` wire verb with a TYPED\r\n   conflict outcome (a distinct kind fails loud on an N-1 broker; a serde-defaulted field\r\n   would be silently ignored — wire discipline), sharing the single-flight/PTY spawn body.\r\n   Fresh at AlreadyLive/lost-race returns the conflict, never `Spawned(existing)`. CLI:\r\n   exit 1 + stable `ENDPOINT_CREATE_CONFLICT:<id>:…no session was created`, no attach, no\r\n   state mutation, never kills the live session. Omitted `--resume` keeps the documented\r\n   create-default (fresh-and-conflict, same as explicit `--create`); ensure/attach would be\r\n   a NEW documented mode, never a silent overload. Preflight stays UX-only (cannot close\r\n   TOCTOU).\r\n<!-- [doc->REQ-ENDPOINT-CYCLE-HONEST] -->\r\n6. **One liveness authority for cycle verbs.** The ALREADY_LIVE dup-guard liveness-probes\r\n   the claimed session's client tree before refusing (dead tree ⇒ reap + respawn\r\n   honestly); the shutdown state machine consults the same source, so \"is it live\" has one\r\n   answer (no ALREADY_LIVE/OFFLINE/NO_EDGE three-way contradiction).\r\n\r\n<!-- [doc->REQ-BROKER-ZOMBIE-IDENTITY] -->\r\n   **Zombie-kill identity guard (releases#285).** Liveness is not permission to\r\n   kill a remembered PID. The broker records the root's OS creation time just\r\n   after PTY spawn, before starting its exit waiter. The child is still owned:\r\n   on Windows its handle is retained; on Unix it has not been reaped, so its\r\n   number cannot yet be recycled. The expected identity is never refreshed at\r\n   reap time. Start time is preferred over image because a launcher can `exec`\r\n   another image without becoming a different process.\r\n\r\n   At the zombie kill boundary, a fresh owned-process death probe permits\r\n   stale-row cleanup for a dead root **without any PID/tree kill**. A fresh\r\n   start-time query that differs from the captured stamp also proves the\r\n   remembered child is gone: `SPAWN_ZOMBIE_REAP_RECYCLED` records both births,\r\n   skips signaling the replacement process, and proceeds through the existing\r\n   bounded de-table and stamp-clear loop so the next spawn can succeed.\r\n   Only a matching live birth permits tree-kill.\r\n   Unknown PID or missing/unreadable identity emits\r\n   `SPAWN_ZOMBIE_REAP_DECLINE` and fails the spawn request **before** the forced\r\n   de-table loop. There is no fallback `session.kill`, teardown dispatch, or\r\n   session-resource drop on refusal; the old row remains for its normal exit\r\n   waiter. This trades automatic recovery of an **unverifiable** live-number\r\n   row for safety, but never strands a row whose recorded birth is proven gone.\r\n   Fresh-dead cleanup retains the existing snapshot-less Unix liveness fallback.\r\n\r\n   **Limits:** this is a checked numeric tree-kill, not an atomic handle/pidfd\r\n   tree termination. A process can exit and its PID can be reused after the\r\n   identity query and before a signal; descendant enumeration/kill retains the\r\n   same race. Linux start times have clock-tick resolution, so same-tick reuse\r\n   is indistinguishable; Windows uses creation FILETIME. This scoped guard does\r\n   not change generic `kill_pid_tree` or other kill sites.\r\n\r\n## Consequences\r\n\r\n- A dead endpoint converges to offline+suspended+uncontrolled within one reconcile, from\r\n  every death path, and stays there until an explicit wake or run — no resurrection loops,\r\n  no zombie control latches, no immortal hybrid rows.\r\n- `--create` becomes a contract: success ⇒ a fresh session exists; conflict ⇒ typed, loud,\r\n  side-effect-free. Existing no-dup safety tests remain valid (they codify the dedup body\r\n  this shares).\r\n- New wire surface: `KIND_SPAWN_FRESH` (additive verb). N-1 broker answers unknown-kind ⇒\r\n  CLI reports the loud IPC failure, never a fake Created.\r\n- Explicit-Wake semantics are preserved by construction (intent normalization happens only\r\n  on definitive death observations, never speculatively in readers).\r\n\r\n## Requirements\r\n\r\n`REQ-ENDPOINT-ONLINE-TRUTH`, `REQ-REST-TERMINAL-NORMALIZE`, `REQ-CONTROL-STAMP-CONVERGENCE`,\r\n`REQ-STREAM-LEASE-CLASSES` (mechanism in ADR-0040 decision 6),\r\n`REQ-HAZARD-ENDPOINT-LIFECYCLE` (KH 7.45); W3: `REQ-SPAWN-FRESH-TRUTHFUL`,\r\n`REQ-ENDPOINT-CYCLE-HONEST`.\r\n\r\n<!-- [doc->REQ-HOSTING-AUTHORITY-CONTROLLABLE] -->\r\n## Amendment (RC-RENDER-TRUTH v0.38.1 leg 3, doyle ruling 2026-07-19): one hosting authority — the online-earn authority splits by hosting topology\r\n\r\nPersisted `state` is the durable endpoint TYPE (REQ-EP-6 open type system;\r\n`establish_perch`'s prior-type preserve is INTENTIONAL and stays). The source-definitive\r\nBROKER-PTY hosting authority is `controllable == Some(true)`, NOT `state`.\n…\nRequirement: `REQ-HOSTING-AUTHORITY-CONTROLLABLE`.","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,null,182]},"fileSize":12170,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\docs\\adr\\0041-endpoint-lifecycle-truth.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":182,"totalBytes":9240,"outputLines":138,"outputBytes":9240,"shownRange":{"start":1,"end":138},"nextOffset":139}}},"isError":false,"timestamp":1789394594384}}
{"type":"message","id":"5efe0877","parentId":"50e43810","timestamp":"2026-09-14T14:03:14.393Z","message":{"role":"toolResult","toolCallId":"call_dCylxjG01PgH375vgxsAJNw9|fc_0a63d1e8a5b46c9c016aa7fea2499087d1864cbe4dc8db0740","toolName":"read","content":[{"type":"text","text":"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct SpawnReq {\n    /// The program to exec (resolved on `PATH` by the OS spawn).\n    pub program: String,\n    /// Its arguments, verbatim.\n    #[serde(default)]\n    pub args: Vec<String>,\n    /// Initial PTY rows.\n    pub rows: u16,\n    /// Initial PTY cols.\n    pub cols: u16,\n    /// The endpoint this session belongs to (D9-1: the remote-drive\n    /// dispatcher's gate subject — `serve_attach` gates on the *owning*\n    /// endpoint's whitelist, resolved from the broker's table, never from\n    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher\n    /// refuses to serve an unlabeled session, fail-closed.\n    #[serde(default)]\n    pub endpoint: String,\n    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\n    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\n    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\n    /// consumer (Claude Code) reads its `.claude`, session history, and digest\n    /// source relative to cwd, so a project-scoped launcher MUST place the\n    /// harness in the user's project root, not the daemon's. Additive +\n    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub cwd: Option<String>,\n    /// Environment variables to INJECT into the spawned child, on top of the\n    /// inherited env (manifest `[env.<VAR>]` direction = inject, with `{key}`\n    /// substitution already applied — REQ-HAZARD-ENV-SUBST). Empty = inherit only\n    /// (the legacy behavior). Additive + serde-defaulted: an N-1 brain/broker\n    /// omits it and the child simply inherits the broker's env (no inject).\n    #[serde(default, skip_serializing_if = \"std::collections::BTreeMap::is_empty\")]\n    pub env: std::collections::BTreeMap<String, String>,\n    /// The adapter's idle-delivery translation binary as a resolved **argv**\n    /// (program + args), if it declares one (`[message-idle-translation-binary]`\n    /// `command` or the deprecated `path`, ADR-0022 / ADR-0029). `argv[0]` is the\n    /// install-dir-resolved program; `argv[1..]` the resolved `command` args. The\n    /// broker LIFECYCLE-manages it: spawns it alongside this spt-hosted session and\n    /// reaps it on session-down. `None` ⇒ no translation binary (idle inbound\n    /// spools; the v0.11.0 raw-inject was removed as a delivery path, ADR-0022\n    /// amendment). Additive + serde-defaulted: an N-1 brain/broker omits it and the\n    /// session simply runs without a translation binary.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub translation_binary: Option<Vec<String>>,\n    /// The adapter name owning this session (W3a, ADR-0025). The broker records\n    /// it on the `HostedSession` so a live adapter update can find every live\n    /// session running adapter X to cycle its resident children. Empty = unknown\n    /// (a legacy/test/shell spawn not tied to a registered adapter). Additive +\n    /// serde-defaulted: an N-1 brain/broker omits it.\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    #[serde(default, skip_serializing_if = \"String::is_empty\")]\n    pub adapter: String,\n    /// The adapter's install dir for this session (W3a, ADR-0025) — where its\n    /// binaries live and a live update swaps files. `None` = unknown (legacy/test).\n    /// Additive + serde-defaulted.\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub install_dir: Option<String>,\n}\n\n/// `spawned` payload — the broker-minted session id + the child's OS pid.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Spawned {\n    /// The id the brain uses to address this session in later commands.\n    pub session_id: u64,\n    /// The child's OS pid, if the backend exposes one.\n    #[serde(default)]\n    pub pid: Option<u32>,\n}\n\n/// `spawn-conflict` payload (ADR-0041 decision 5) — the typed refusal a\n/// [`KIND_SPAWN_FRESH`] gets when the endpoint already holds a live session.\n/// Carries the EXISTING session so the caller can report it truthfully; the\n/// contract is side-effect-free: no session created, no state mutated, the\n/// live session untouched.\n\n[Showing lines 497-572 of 2360. Use :573 to continue]"}],"details":{"truncation":{"content":"#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct SpawnReq {\r\n    /// The program to exec (resolved on `PATH` by the OS spawn).\r\n    pub program: String,\r\n    /// Its arguments, verbatim.\r\n    #[serde(default)]\r\n    pub args: Vec<String>,\r\n    /// Initial PTY rows.\r\n    pub rows: u16,\r\n    /// Initial PTY cols.\r\n    pub cols: u16,\r\n    /// The endpoint this session belongs to (D9-1: the remote-drive\r\n    /// dispatcher's gate subject — `serve_attach` gates on the *owning*\r\n    /// endpoint's whitelist, resolved from the broker's table, never from\r\n    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher\r\n    /// refuses to serve an unlabeled session, fail-closed.\r\n    #[serde(default)]\r\n    pub endpoint: String,\r\n    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\r\n    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\r\n    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\r\n    /// consumer (Claude Code) reads its `.claude`, session history, and digest\r\n    /// source relative to cwd, so a project-scoped launcher MUST place the\r\n    /// harness in the user's project root, not the daemon's. Additive +\r\n    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n    /// Environment variables to INJECT into the spawned child, on top of the\r\n    /// inherited env (manifest `[env.<VAR>]` direction = inject, with `{key}`\r\n    /// substitution already applied — REQ-HAZARD-ENV-SUBST). Empty = inherit only\r\n    /// (the legacy behavior). Additive + serde-defaulted: an N-1 brain/broker\r\n    /// omits it and the child simply inherits the broker's env (no inject).\r\n    #[serde(default, skip_serializing_if = \"std::collections::BTreeMap::is_empty\")]\r\n    pub env: std::collections::BTreeMap<String, String>,\r\n    /// The adapter's idle-delivery translation binary as a resolved **argv**\r\n    /// (program + args), if it declares one (`[message-idle-translation-binary]`\r\n    /// `command` or the deprecated `path`, ADR-0022 / ADR-0029). `argv[0]` is the\r\n    /// install-dir-resolved program; `argv[1..]` the resolved `command` args. The\r\n    /// broker LIFECYCLE-manages it: spawns it alongside this spt-hosted session and\r\n    /// reaps it on session-down. `None` ⇒ no translation binary (idle inbound\r\n    /// spools; the v0.11.0 raw-inject was removed as a delivery path, ADR-0022\r\n    /// amendment). Additive + serde-defaulted: an N-1 brain/broker omits it and the\r\n    /// session simply runs without a translation binary.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub translation_binary: Option<Vec<String>>,\r\n    /// The adapter name owning this session (W3a, ADR-0025). The broker records\r\n    /// it on the `HostedSession` so a live adapter update can find every live\r\n    /// session running adapter X to cycle its resident children. Empty = unknown\r\n    /// (a legacy/test/shell spawn not tied to a registered adapter). Additive +\r\n    /// serde-defaulted: an N-1 brain/broker omits it.\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[serde(default, skip_serializing_if = \"String::is_empty\")]\r\n    pub adapter: String,\r\n    /// The adapter's install dir for this session (W3a, ADR-0025) — where its\r\n    /// binaries live and a live update swaps files. `None` = unknown (legacy/test).\r\n    /// Additive + serde-defaulted.\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub install_dir: Option<String>,\r\n}\r\n\r\n/// `spawned` payload — the broker-minted session id + the child's OS pid.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct Spawned {\r\n    /// The id the brain uses to address this session in later commands.\r\n    pub session_id: u64,\r\n    /// The child's OS pid, if the backend exposes one.\r\n    #[serde(default)]\r\n    pub pid: Option<u32>,\r\n}\r\n\r\n/// `spawn-conflict` payload (ADR-0041 decision 5) — the typed refusal a\r\n/// [`KIND_SPAWN_FRESH`] gets when the endpoint already holds a live session.\r\n/// Carries the EXISTING session so the caller can report it truthfully; the\r\n/// contract is side-effect-free: no session created, no state mutated, the\r\n/// live session untouched.\r","truncated":true,"truncatedBy":"lines","totalLines":1864,"totalBytes":4354,"outputLines":76,"outputBytes":4354,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":2360,"displayContent":{"text":"#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct SpawnReq {\r\n    /// The program to exec (resolved on `PATH` by the OS spawn).\r\n    pub program: String,\r\n    /// Its arguments, verbatim.\r\n    #[serde(default)]\r\n    pub args: Vec<String>,\r\n    /// Initial PTY rows.\r\n    pub rows: u16,\r\n    /// Initial PTY cols.\r\n    pub cols: u16,\r\n    /// The endpoint this session belongs to (D9-1: the remote-drive\r\n    /// dispatcher's gate subject — `serve_attach` gates on the *owning*\r\n    /// endpoint's whitelist, resolved from the broker's table, never from\r\n    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher\r\n    /// refuses to serve an unlabeled session, fail-closed.\r\n    #[serde(default)]\r\n    pub endpoint: String,\r\n    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\r\n    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\r\n    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\r\n    /// consumer (Claude Code) reads its `.claude`, session history, and digest\r\n    /// source relative to cwd, so a project-scoped launcher MUST place the\r\n    /// harness in the user's project root, not the daemon's. Additive +\r\n    /// serde-defaulted: an N-1 brain/broker omits it and gets the inherited cwd.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub cwd: Option<String>,\r\n    /// Environment variables to INJECT into the spawned child, on top of the\r\n    /// inherited env (manifest `[env.<VAR>]` direction = inject, with `{key}`\r\n    /// substitution already applied — REQ-HAZARD-ENV-SUBST). Empty = inherit only\r\n    /// (the legacy behavior). Additive + serde-defaulted: an N-1 brain/broker\r\n    /// omits it and the child simply inherits the broker's env (no inject).\r\n    #[serde(default, skip_serializing_if = \"std::collections::BTreeMap::is_empty\")]\r\n    pub env: std::collections::BTreeMap<String, String>,\r\n    /// The adapter's idle-delivery translation binary as a resolved **argv**\r\n    /// (program + args), if it declares one (`[message-idle-translation-binary]`\r\n    /// `command` or the deprecated `path`, ADR-0022 / ADR-0029). `argv[0]` is the\r\n    /// install-dir-resolved program; `argv[1..]` the resolved `command` args. The\r\n    /// broker LIFECYCLE-manages it: spawns it alongside this spt-hosted session and\r\n    /// reaps it on session-down. `None` ⇒ no translation binary (idle inbound\r\n    /// spools; the v0.11.0 raw-inject was removed as a delivery path, ADR-0022\r\n    /// amendment). Additive + serde-defaulted: an N-1 brain/broker omits it and the\r\n    /// session simply runs without a translation binary.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub translation_binary: Option<Vec<String>>,\r\n    /// The adapter name owning this session (W3a, ADR-0025). The broker records\r\n    /// it on the `HostedSession` so a live adapter update can find every live\r\n    /// session running adapter X to cycle its resident children. Empty = unknown\r\n    /// (a legacy/test/shell spawn not tied to a registered adapter). Additive +\r\n    /// serde-defaulted: an N-1 brain/broker omits it.\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[serde(default, skip_serializing_if = \"String::is_empty\")]\r\n    pub adapter: String,\r\n    /// The adapter's install dir for this session (W3a, ADR-0025) — where its\r\n    /// binaries live and a live update swaps files. `None` = unknown (legacy/test).\r\n    /// Additive + serde-defaulted.\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub install_dir: Option<String>,\r\n}\r\n\r\n/// `spawned` payload — the broker-minted session id + the child's OS pid.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct Spawned {\r\n    /// The id the brain uses to address this session in later commands.\r\n    pub session_id: u64,\r\n    /// The child's OS pid, if the backend exposes one.\r\n    #[serde(default)]\r\n    pub pid: Option<u32>,\r\n}\r\n\r\n/// `spawn-conflict` payload (ADR-0041 decision 5) — the typed refusal a\r\n/// [`KIND_SPAWN_FRESH`] gets when the endpoint already holds a live session.\r\n/// Carries the EXISTING session so the caller can report it truthfully; the\r\n/// contract is side-effect-free: no session created, no state mutated, the\r\n/// live session untouched.\r","startLine":497,"lineNumbers":[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]},"fileSize":119647,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\msg.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2360,"totalBytes":4354,"outputLines":76,"outputBytes":4354,"shownRange":{"start":497,"end":572},"nextOffset":573}}},"isError":false,"timestamp":1789394594392}}
{"type":"message","id":"f906f279","parentId":"5efe0877","timestamp":"2026-09-14T14:03:14.456Z","message":{"role":"toolResult","toolCallId":"call_GDDitYHudyHqh9djCrN0Woh2|fc_0a63d1e8a5b46c9c016aa7fea249a887d1bc60cf7a58ecd9d7","toolName":"read","content":[{"type":"text","text":"### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`\n…\n- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.\n…\n- **Sender-stamp note (W2b, `REQ-MSG-SENDER-STAMP`, 2026-07-29):** the invariant above is preserved for the **origin node** and for **`from`**, and both sentences stand verbatim — the gate's node subject is still transport truth, and `from` is still never an authorization subject. One field is now carved out of \"a forged one decodes as an ignored unknown field and influences nothing\": `WanMessage.sender_proven` IS decoded and **acted on** as the access chain's tier-1 sender subject. It does not weaken this hazard, because it is **daemon-stamped from the sending session** (never `--from`, which a caller controls) and it can only ever name a *sender endpoint* — never the node. Forging it therefore requires a malicious member node, which `REQ-MSG-6`'s ratified …\n\n### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\n<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\n- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** w…\n- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_tim…\n- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).\n- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\n\n<!-- [doc->REQ-BRAIN-READ-BOUNDED-PER-CALL] -->\n**Per-call enforcement (releases#293).** Every Brain reply-wait loop computes\n`call_deadline()` once, after sending its request and before entering the loop,\nand passes that same value to `read_event_until`. Unrelated frames do not renew\nthe reply budget. The unbounded `read_event()` convenience method is removed:\ncallers must name their deadline explicitly. Whole-carrier callers retain\n`None` and their existing blocking semantics; this change does not invent a\ntimeout the carrier cannot honor.\nStream loops (`serve_*_feed` and the attach serve loop) are not reply-waits:\nthey use an explicit `None` for their subscriber-owned lifetime, independent\nof the carrier's call budget. Any future silence bound is per read, not per call.\n`xtask brain-read-check` enforces the retired-reader and stream-budget policy\nagainst crate sources, including tests, and checks representative reply-wait\npositive controls. The CI lint job runs this command on code-changing PRs;\n`xtask check` also includes it. Predicate tests use synthetic Rust text, while\nruntime recovery remains covered by separate pump-deadline integration cells.\nThe six WAN stream reply-waits in `wan.rs` follow `REQ-WAN-REPLY-BOUND` (a budget\nrenewed only by data on the matching stream) and are EXCLUDED from the audit's\nfixed-deadline policy controls by record: a tokenizer cannot prove that a guarded\nre-arm is the only renewal, and a false assurance is worse than a named exclusion.\nThe exclusion does not imply the renewal behaviour is tested (register follow-up).\n\n### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\n\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\n\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\n…\n\n\n[Showing lines 500-544 of 1132. Use :545 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"\r\n### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\r\n- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** w…\n- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_tim…\n- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).\r\n- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\r\n\r\n<!-- [doc->REQ-BRAIN-READ-BOUNDED-PER-CALL] -->\r\n**Per-call enforcement (releases#293).** Every Brain reply-wait loop computes\r\n`call_deadline()` once, after sending its request and before entering the loop,\r\nand passes that same value to `read_event_until`. Unrelated frames do not renew\r\nthe reply budget. The unbounded `read_event()` convenience method is removed:\r\ncallers must name their deadline explicitly. Whole-carrier callers retain\r\n`None` and their existing blocking semantics; this change does not invent a\r\ntimeout the carrier cannot honor.\r\nStream loops (`serve_*_feed` and the attach serve loop) are not reply-waits:\r\nthey use an explicit `None` for their subscriber-owned lifetime, independent\r\nof the carrier's call budget. Any future silence bound is per read, not per call.\r\n`xtask brain-read-check` enforces the retired-reader and stream-budget policy\r\nagainst crate sources, including tests, and checks representative reply-wait\r\npositive controls. The CI lint job runs this command on code-changing PRs;\r\n`xtask check` also includes it. Predicate tests use synthetic Rust text, while\r\nruntime recovery remains covered by separate pump-deadline integration cells.\r\nThe six WAN stream reply-waits in `wan.rs` follow `REQ-WAN-REPLY-BOUND` (a budget\r\nrenewed only by data on the matching stream) and are EXCLUDED from the audit's\r\nfixed-deadline policy controls by record: a tokenizer cannot prove that a guarded\r\nre-arm is the only renewal, and a false assurance is worse than a named exclusion.\r\nThe exclusion does not imply the renewal behaviour is tested (register follow-up).\r\n\r\n### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\r\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\r\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\r\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\r\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\r\n\r\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\r\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\r\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\r\n\r\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\r","truncated":true,"truncatedBy":"lines","totalLines":633,"totalBytes":11279,"outputLines":45,"outputBytes":11279,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1132,"displayContent":{"text":"### 7.5 WAN-inbound origin is transport truth, never payload  `[REQ-HAZARD-WAN-ORIGIN-AUTH]`\n…\n- **Failure:** the ADR-0009 access whitelist gates **unsolicited wire inbound by origin node**. If the gate's subject is read from record bytes (an `origin_node`/`from`/`node` field a sender wrote), any sender forges any origin and the whitelist is decoration — same spoof class as 7.3's Psyche-supplied `from=`, now on the cross-node surface.\n…\n- **Sender-stamp note (W2b, `REQ-MSG-SENDER-STAMP`, 2026-07-29):** the invariant above is preserved for the **origin node** and for **`from`**, and both sentences stand verbatim — the gate's node subject is still transport truth, and `from` is still never an authorization subject. One field is now carved out of \"a forged one decodes as an ignored unknown field and influences nothing\": `WanMessage.sender_proven` IS decoded and **acted on** as the access chain's tier-1 sender subject. It does not weaken this hazard, because it is **daemon-stamped from the sending session** (never `--from`, which a caller controls) and it can only ever name a *sender endpoint* — never the node. Forging it therefore requires a malicious member node, which `REQ-MSG-6`'s ratified …\n\r\n### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-PUMP-IPC-DEADLINE] -->\r\n- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** w…\n- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_tim…\n- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).\r\n- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.\r\n\r\n<!-- [doc->REQ-BRAIN-READ-BOUNDED-PER-CALL] -->\r\n**Per-call enforcement (releases#293).** Every Brain reply-wait loop computes\r\n`call_deadline()` once, after sending its request and before entering the loop,\r\nand passes that same value to `read_event_until`. Unrelated frames do not renew\r\nthe reply budget. The unbounded `read_event()` convenience method is removed:\r\ncallers must name their deadline explicitly. Whole-carrier callers retain\r\n`None` and their existing blocking semantics; this change does not invent a\r\ntimeout the carrier cannot honor.\r\nStream loops (`serve_*_feed` and the attach serve loop) are not reply-waits:\r\nthey use an explicit `None` for their subscriber-owned lifetime, independent\r\nof the carrier's call budget. Any future silence bound is per read, not per call.\r\n`xtask brain-read-check` enforces the retired-reader and stream-budget policy\r\nagainst crate sources, including tests, and checks representative reply-wait\r\npositive controls. The CI lint job runs this command on code-changing PRs;\r\n`xtask check` also includes it. Predicate tests use synthetic Rust text, while\r\nruntime recovery remains covered by separate pump-deadline integration cells.\r\nThe six WAN stream reply-waits in `wan.rs` follow `REQ-WAN-REPLY-BOUND` (a budget\r\nrenewed only by data on the matching stream) and are EXCLUDED from the audit's\r\nfixed-deadline policy controls by record: a tokenizer cannot prove that a guarded\r\nre-arm is the only renewal, and a false assurance is worse than a named exclusion.\r\nThe exclusion does not imply the renewal behaviour is tested (register follow-up).\r\n\r\n### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`\r\n<!-- [doc->REQ-HAZARD-VIEWER-ISOLATION] -->\r\n- **Failure:** the W2.5 controller/viewer model lets ANY number of read-only `--view` attachers ride one session's broker `OutputLog`. The single drain thread fans each output chunk to every attacher. If a viewer's socket is fanned out with a **blocking** write under the log lock (the controller's authoritative path), one wedged viewer (a slow terminal, a black-holed WAN peer, a hostile non-reader) stalls the drain — freezing the controller's stream and backing up the PTY child. A single watcher must never be able to degrade or hang the driver.\r\n- **Invariant:** the drain writes the **controller** on the authoritative blocking bounded path (it alone advances `delivered_through`), but each **viewer** gets an **isolated bounded SPSC queue + a dedicated writer thread**; the drain `try_send`s under the log lock and **evicts** any viewer whose queue is `Full` (fell behind the live stream) or `Disconnected` (its writer died on a dead socket) — the drain thread **never touches a viewer socket**, so no viewer write can backpressure it. A **soft cap** (`MAX_VIEWERS`) bounds the writer-thread count (a viewer attach beyond it is refused). Viewer eviction never perturbs the controller stream, the `delivered_through` cursor, or the child. Ring **replay-at-attach** is owned by the writer thread (not the bounded l…\n- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).\r\n- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).\r\n\r\n### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`\r\n<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->\r\n- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).\r\n- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-pee…\n- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run…\n- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.\r\n\r\n### 7.9 A daemon-state wire change needs a deliberate BROKER restart (the broker is resident across a brain self-update)  `[REQ-HAZARD-BROKER-SEED-WIRE-SKEW]`\r\n…\n","startLine":492,"lineNumbers":[492,null,494,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,null,550]},"fileSize":360311,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1132,"totalBytes":11279,"outputLines":45,"outputBytes":11279,"shownRange":{"start":500,"end":544},"nextOffset":545},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789394594456}}
{"type":"message","id":"9041f6cf","parentId":"f906f279","timestamp":"2026-09-14T14:03:14.457Z","message":{"role":"toolResult","toolCallId":"call_lhIvcfo8bxAaCJamYh6v2sMl|fc_0a63d1e8a5b46c9c016aa7fea249b087d1883a0772c73d22c2","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## frame.rs\n 124|#[derive(Debug, Clone, Serialize, Deserialize)]\n*125|pub struct Envelope {\n 126|    /// The protocol version of the sender (for scoped interpretation).\n 127|    pub protocol_version: u32,\n 128|    /// The message-kind discriminator. A string, not an enum, so an unrecognized"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":1,"fileCount":1,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs","count":1}],"truncated":false,"displayContent":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## frame.rs\n 124│#[derive(Debug, Clone, Serialize, Deserialize)]\n*125│pub struct Envelope {\n 126│    /// The protocol version of the sender (for scoped interpretation).\n 127│    pub protocol_version: u32,\n 128│    /// The message-kind discriminator. A string, not an enum, so an unrecognized"},"isError":false,"timestamp":1789394594457}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Z5jqUoBZ9toSW6mZ3YX4qAq2|fc_0a63d1e8a5b46c9c016aa7fea73c8487d1ae4053fc466c5437","toolName":"hub","startedAt":"2026-09-14T14:03:21.240Z","intent":"Confirming fixture budget evidence"},"id":"2eb201a7","parentId":"9041f6cf","timestamp":"2026-09-14T14:03:21.240Z"}
{"type":"message","id":"87b92601","parentId":"2eb201a7","timestamp":"2026-09-14T14:03:21.242Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Confirming deadline via message**"},{"type":"toolCall","id":"call_Z5jqUoBZ9toSW6mZ3YX4qAq2|fc_0a63d1e8a5b46c9c016aa7fea73c8487d1ae4053fc466c5437","name":"hub","arguments":{"i":"Confirming fixture budget evidence","op":"send","to":"Main","message":"Source route proves fixed deadline armed after spawn send; Split channel Timeout exact error; broker spawn has no cancellation/deadline in SpawnReq and can outlive client. waiter_armed8672 -> build_translation -> translation_ready8696 -> table insert -> row_inserted8728 -> Spawned8738. Please confirm attach_ipc_deadline()126 exact 2s/overrides for report (avoiding your fixture ownership). NetRuntimeTrace owns translation subpath."},"intent":"Confirming fixture budget evidence"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":6624,"output":144,"cacheRead":56064,"cacheWrite":0,"totalTokens":62832,"reasoningTokens":15,"cost":{"input":0.06624000000000001,"output":0.007200000000000001,"cacheRead":0.056063999999999996,"cacheWrite":0,"total":0.129504}},"stopReason":"toolUse","timestamp":1789394594482,"responseId":"resp_0a63d1e8a5b46c9c016aa7fea2af4c87d19be627ae558c1dfc","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7fea4e06087d1b4c6ae7ac4628637","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_6npNAU9JEfe8qo1VOL4YdDZd_mYIt5TeXhZvBNVHMgF9G7lPRz3BY3GUzZKMP4noeX3mT2QEEzI3V3Agt71qrUp2hM06-tgVvyywfk2jewCTYIFYN5NRVTrzj0r1jZ6P8oqzJ04E7x9S41yccoCZNCUSY7mV3VRczOU6z9P0WkI9kZCPTFFWZHFdaiJTeeQl5Y0T0oqKXTDKVXodiRH7E9EzKYeHWgVh7c2n7eODQIniGiJOHLNNadmE5w6L_okgzCOftBNxFzcDRcOOAK87b5DSjL5TFJy9K5Va3lnY71tyiJ509knWEFRNTCeaRF6KgWsSBcMkY02DvEDMUA04y1PCMlGTni-pGiLnxeaaFt0zZpDqh21BTwIg285jS4166ti25tYT1EMTKSX_ladusF5Tyv0VOPwhebVmlRNgPGSwJl40QzBSBRTHR-1PZpom6GRE5-GYJrgIEm8R0LLaGg7CqzjW7K-Mj6nVwf9-aFJlOy1qPtuCuQ_xchyg5WMB203Cfdb45tEjT4E-aXqepfCiWMj1lNQajz0qzrGL4XwKp2Jrfzjvb7VofL7M__LyMYsPUmwMX64csejbnp-A4qTrX4VWi00MdSSlRJV0BRul39vez0VPj_Hw3ckai6ibu8BiANvPJXOu6xyqcgATfxuhbMTyvbYgQQJVkhazXAeBjW-f_fw-LMH7BCNPpHn4qMAJ-5FYnetjVPQxm2vfC3YHxTvaVbgMHDNwPMG9u9Py0XUazC8sEmFZe11Ybm9CvJt0DZMjretnyOj1KzKkYKuB1dGIfWCaPaZsQ5DAQUwJs5N4FM-dM3pqC3lALDONiGFRjiSFF4fYCaU3SOVaYrE309zp97OgDkAxiwqGADDTjDPnaIs5XBsscpWAN7Q6Z1FiD5D9emfmF4AnOdMwxwYhAi6CSObFpCAPHN1y0IgZkTgiy_f8eXfNWR2klrBgcXRkqZaCxQBOVixe8t1wKKlD-fKhmPI-OEAdj_cEmBNxEI85h6GZTWENLinH2x4ZWg59-29hPLPMDUjjS6zRA4dq8JcYMa3oUi4cumsGfWW_x4Vh8Bx3ribUde1Pq2u_8ovdjHxIDuNJcer8J1afk1kbDd_O8m4B103wxMj3BpSBgcoHz_hH-9J1FW2vKpR0ok0X3hvMJdtCT1iLxbsQUUaq_tiozjyL1PHFrXt2LQApwRIziXv5HPIvbrWH9GIo7KgLJX_aTp9ts23we7Wi4p5UBrGAhsPAhAH5hMtcrvfL3aI9MoaEOwo9zy1fbLMhitep8AVoiGHN1Op72dBi6FXTr_tBYiKKxxNz-R7xqMurQmdofeGeCFt9en5NBQQlPWN2law5Z7SFOnr6bWahmRZQ==","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"summary":[{"type":"summary_text","text":"**Confirming deadline via message**"}],"metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fea73c8487d1ae4053fc466c5437","type":"function_call","status":"completed","arguments":"{\"i\":\"Confirming fixture budget evidence\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Source route proves fixed deadline armed after spawn send; Split channel Timeout exact error; broker spawn has no cancellation/deadline in SpawnReq and can outlive client. waiter_armed8672 -> build_translation -> translation_ready8696 -> table insert -> row_inserted8728 -> Spawned8738. Please confirm attach_ipc_deadline()126 exact 2s/overrides for report (avoiding your fixture ownership). NetRuntimeTrace owns translation subpath.\"}","call_id":"call_Z5jqUoBZ9toSW6mZ3YX4qAq2","internal_chat_message_metadata_passthrough":{"create_time":1789394594.84592,"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"hub","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":6750.901199996471,"ttft":2415.4917000085115,"completedAt":1789394601236,"contextSnapshot":{"promptTokens":62688,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"0b2c6453","parentId":"87b92601","timestamp":"2026-09-14T14:03:21.250Z","message":{"role":"toolResult","toolCallId":"call_Z5jqUoBZ9toSW6mZ3YX4qAq2|fc_0a63d1e8a5b46c9c016aa7fea73c8487d1ae4053fc466c5437","toolName":"hub","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"SpawnReplyTrace","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1789394601248}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ooJq8Wxbh0rmUPeYIxxmnogE|fc_0a63d1e8a5b46c9c016aa7feb0f4a087d18c7ac684bfb3f8b5","toolName":"read","startedAt":"2026-09-14T14:03:29.065Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs:120-198"},"intent":"Reading frame protocol definition"},"id":"abf25980","parentId":"0b2c6453","timestamp":"2026-09-14T14:03:29.065Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pSp9BKli5q5RpmyKyDpOLpp6|fc_0a63d1e8a5b46c9c016aa7feb0f4b087d185bd2308291a68c4","toolName":"read","startedAt":"2026-09-14T14:03:29.065Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:294-345"},"intent":"Reading phase trace ceiling semantics"},"id":"1b491a19","parentId":"abf25980","timestamp":"2026-09-14T14:03:29.065Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HJ8cy5g9eGlCtxV3eE8vbWx7|fc_0a63d1e8a5b46c9c016aa7feb0f4b887d1954519fa16c41d53","toolName":"grep","startedAt":"2026-09-14T14:03:29.065Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs;.worktrees/297-enforcement-shape/crates/spt-proto/src/lib.rs"},"intent":"Locating phase logger blocking behavior"},"id":"22c9d933","parentId":"1b491a19","timestamp":"2026-09-14T14:03:29.065Z"}
{"type":"message","id":"6a7db2f4","parentId":"22c9d933","timestamp":"2026-09-14T14:03:29.066Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_ooJq8Wxbh0rmUPeYIxxmnogE|fc_0a63d1e8a5b46c9c016aa7feb0f4a087d18c7ac684bfb3f8b5","name":"read","arguments":{"i":"Reading frame protocol definition","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs:120-198"},"intent":"Reading frame protocol definition"},{"type":"toolCall","id":"call_pSp9BKli5q5RpmyKyDpOLpp6|fc_0a63d1e8a5b46c9c016aa7feb0f4b087d185bd2308291a68c4","name":"read","arguments":{"i":"Reading phase trace ceiling semantics","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:294-345"},"intent":"Reading phase trace ceiling semantics"},{"type":"toolCall","id":"call_HJ8cy5g9eGlCtxV3eE8vbWx7|fc_0a63d1e8a5b46c9c016aa7feb0f4b887d1954519fa16c41d53","name":"grep","arguments":{"i":"Locating phase logger blocking behavior","pattern":"macro_rules! emit_line_err|fn emit_line_err|stderr\\(|fn write_line|fn emit","path":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs;.worktrees/297-enforcement-shape/crates/spt-proto/src/lib.rs","case":true,"gitignore":false,"skip":null},"intent":"Locating phase logger blocking behavior"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":262,"output":200,"cacheRead":62592,"cacheWrite":0,"totalTokens":63054,"cost":{"input":0.0026200000000000004,"output":0.01,"cacheRead":0.062592,"cacheWrite":0,"total":0.075212}},"stopReason":"toolUse","timestamp":1789394601334,"responseId":"resp_0a63d1e8a5b46c9c016aa7fea98d8487d184cc1517d7610511","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7feb0f4a087d18c7ac684bfb3f8b5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading frame protocol definition\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs:120-198\"}","call_id":"call_ooJq8Wxbh0rmUPeYIxxmnogE","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7feb0f4b087d185bd2308291a68c4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading phase trace ceiling semantics\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:294-345\"}","call_id":"call_pSp9BKli5q5RpmyKyDpOLpp6","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"read","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7feb0f4b887d1954519fa16c41d53","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating phase logger blocking behavior\",\"pattern\":\"macro_rules! emit_line_err|fn emit_line_err|stderr\\\\(|fn write_line|fn emit\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs;.worktrees/297-enforcement-shape/crates/spt-proto/src/lib.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_HJ8cy5g9eGlCtxV3eE8vbWx7","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"grep","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":7728.550799995661,"ttft":7648.312099993229,"completedAt":1789394609063,"contextSnapshot":{"promptTokens":62854,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"74b74435","parentId":"6a7db2f4","timestamp":"2026-09-14T14:03:29.074Z","message":{"role":"toolResult","toolCallId":"call_ooJq8Wxbh0rmUPeYIxxmnogE|fc_0a63d1e8a5b46c9c016aa7feb0f4a087d18c7ac684bfb3f8b5","toolName":"read","content":[{"type":"text","text":"/// type decodes cleanly (the logic layer decides what to do with an unknown\n/// kind), and `payload` is opaque JSON so each kind owns its own shape without\n/// the envelope needing to know it. Unknown trailing fields are ignored on\n/// decode (serde default), so a newer brain may extend a frame and an older\n/// broker still parses it.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Envelope {\n    /// The protocol version of the sender (for scoped interpretation).\n    pub protocol_version: u32,\n    /// The message-kind discriminator. A string, not an enum, so an unrecognized\n    /// kind is never a decode failure on an older peer.\n    pub kind: String,\n    /// The kind-specific body. Absent bodies default to JSON `null`.\n    #[serde(default)]\n    pub payload: serde_json::Value,\n}\n\nimpl Envelope {\n    /// Build an envelope for this build's protocol version with a JSON payload.\n    pub fn new(kind: impl Into<String>, payload: serde_json::Value) -> Self {\n        Envelope {\n            protocol_version: IPC_PROTOCOL_VERSION,\n            kind: kind.into(),\n            payload,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n    // The load-bearing forward-compat property: a frame a *newer* brain emits —\n    // carrying an unknown trailing field — must decode cleanly on an *older*\n    // broker that has never heard of that field. We simulate the newer wire form\n    // with raw JSON (a superset of today's Envelope) and decode into the current\n    // struct; the unknown field is dropped, not rejected.\n    #[test]\n    fn envelope_with_unknown_trailing_field_decodes_on_older_peer() {\n        let newer_wire = json!({\n            \"protocol_version\": 2,\n            \"kind\": \"attach\",\n            \"payload\": { \"session\": \"abc\" },\n            \"future_field_an_older_broker_never_heard_of\": { \"nested\": [1, 2, 3] }\n        })\n        .to_string();\n\n        let decoded: Envelope =\n            serde_json::from_str(&newer_wire).expect(\"older peer must tolerate unknown fields\");\n        assert_eq!(decoded.protocol_version, 2);\n        assert_eq!(decoded.kind, \"attach\");\n        assert_eq!(decoded.payload, json!({ \"session\": \"abc\" }));\n    }\n\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n    // An unknown *kind* is not a decode error — the envelope parses and the\n    // logic layer is what decides it cannot handle the kind.\n    #[test]\n    fn unknown_kind_is_not_a_decode_error() {\n        let wire = json!({ \"protocol_version\": 1, \"kind\": \"a_kind_from_the_future\" }).to_string();\n        let decoded: Envelope = serde_json::from_str(&wire).unwrap();\n        assert_eq!(decoded.kind, \"a_kind_from_the_future\");\n        assert!(decoded.payload.is_null()); // payload defaulted\n    }\n\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n    // A newer brain (higher protocol version, extra Hello fields) is accepted by\n    // an older broker — the exact \"newer brain ↔ older broker\" direction 2.3\n    // demands.\n    #[test]\n    fn handshake_accepts_newer_brain_on_older_broker() {\n        let newer_hello_wire = json!({\n            \"protocol_version\": IPC_PROTOCOL_VERSION + 5,\n            \"role\": \"brain\",\n            \"added_in_a_future_release\": true\n        })\n        .to_string();\n        let hello: Hello = serde_json::from_str(&newer_hello_wire).unwrap();\n        assert_eq!(accept_hello(&hello, Role::Brain), Ok(()));\n    }\n\n…\n}\n\n[Showing lines 119-201 of 225. Use :202 to continue]"}],"details":{"truncation":{"content":"/// type decodes cleanly (the logic layer decides what to do with an unknown\r\n/// kind), and `payload` is opaque JSON so each kind owns its own shape without\r\n/// the envelope needing to know it. Unknown trailing fields are ignored on\r\n/// decode (serde default), so a newer brain may extend a frame and an older\r\n/// broker still parses it.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct Envelope {\r\n    /// The protocol version of the sender (for scoped interpretation).\r\n    pub protocol_version: u32,\r\n    /// The message-kind discriminator. A string, not an enum, so an unrecognized\r\n    /// kind is never a decode failure on an older peer.\r\n    pub kind: String,\r\n    /// The kind-specific body. Absent bodies default to JSON `null`.\r\n    #[serde(default)]\r\n    pub payload: serde_json::Value,\r\n}\r\n\r\nimpl Envelope {\r\n    /// Build an envelope for this build's protocol version with a JSON payload.\r\n    pub fn new(kind: impl Into<String>, payload: serde_json::Value) -> Self {\r\n        Envelope {\r\n            protocol_version: IPC_PROTOCOL_VERSION,\r\n            kind: kind.into(),\r\n            payload,\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use serde_json::json;\r\n\r\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n    // The load-bearing forward-compat property: a frame a *newer* brain emits —\r\n    // carrying an unknown trailing field — must decode cleanly on an *older*\r\n    // broker that has never heard of that field. We simulate the newer wire form\r\n    // with raw JSON (a superset of today's Envelope) and decode into the current\r\n    // struct; the unknown field is dropped, not rejected.\r\n    #[test]\r\n    fn envelope_with_unknown_trailing_field_decodes_on_older_peer() {\r\n        let newer_wire = json!({\r\n            \"protocol_version\": 2,\r\n            \"kind\": \"attach\",\r\n            \"payload\": { \"session\": \"abc\" },\r\n            \"future_field_an_older_broker_never_heard_of\": { \"nested\": [1, 2, 3] }\r\n        })\r\n        .to_string();\r\n\r\n        let decoded: Envelope =\r\n            serde_json::from_str(&newer_wire).expect(\"older peer must tolerate unknown fields\");\r\n        assert_eq!(decoded.protocol_version, 2);\r\n        assert_eq!(decoded.kind, \"attach\");\r\n        assert_eq!(decoded.payload, json!({ \"session\": \"abc\" }));\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n    // An unknown *kind* is not a decode error — the envelope parses and the\r\n    // logic layer is what decides it cannot handle the kind.\r\n    #[test]\r\n    fn unknown_kind_is_not_a_decode_error() {\r\n        let wire = json!({ \"protocol_version\": 1, \"kind\": \"a_kind_from_the_future\" }).to_string();\r\n        let decoded: Envelope = serde_json::from_str(&wire).unwrap();\r\n        assert_eq!(decoded.kind, \"a_kind_from_the_future\");\r\n        assert!(decoded.payload.is_null()); // payload defaulted\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n    // A newer brain (higher protocol version, extra Hello fields) is accepted by\r\n    // an older broker — the exact \"newer brain ↔ older broker\" direction 2.3\r\n    // demands.\r\n    #[test]\r\n    fn handshake_accepts_newer_brain_on_older_broker() {\r\n        let newer_hello_wire = json!({\r\n            \"protocol_version\": IPC_PROTOCOL_VERSION + 5,\r\n            \"role\": \"brain\",\r\n            \"added_in_a_future_release\": true\r\n        })\r\n        .to_string();\r\n        let hello: Hello = serde_json::from_str(&newer_hello_wire).unwrap();\r\n        assert_eq!(accept_hello(&hello, Role::Brain), Ok(()));\r\n    }\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":107,"totalBytes":3534,"outputLines":83,"outputBytes":3534,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":225,"displayContent":{"text":"/// type decodes cleanly (the logic layer decides what to do with an unknown\r\n/// kind), and `payload` is opaque JSON so each kind owns its own shape without\r\n/// the envelope needing to know it. Unknown trailing fields are ignored on\r\n/// decode (serde default), so a newer brain may extend a frame and an older\r\n/// broker still parses it.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct Envelope {\r\n    /// The protocol version of the sender (for scoped interpretation).\r\n    pub protocol_version: u32,\r\n    /// The message-kind discriminator. A string, not an enum, so an unrecognized\r\n    /// kind is never a decode failure on an older peer.\r\n    pub kind: String,\r\n    /// The kind-specific body. Absent bodies default to JSON `null`.\r\n    #[serde(default)]\r\n    pub payload: serde_json::Value,\r\n}\r\n\r\nimpl Envelope {\r\n    /// Build an envelope for this build's protocol version with a JSON payload.\r\n    pub fn new(kind: impl Into<String>, payload: serde_json::Value) -> Self {\r\n        Envelope {\r\n            protocol_version: IPC_PROTOCOL_VERSION,\r\n            kind: kind.into(),\r\n            payload,\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use serde_json::json;\r\n\r\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n    // The load-bearing forward-compat property: a frame a *newer* brain emits —\r\n    // carrying an unknown trailing field — must decode cleanly on an *older*\r\n    // broker that has never heard of that field. We simulate the newer wire form\r\n    // with raw JSON (a superset of today's Envelope) and decode into the current\r\n    // struct; the unknown field is dropped, not rejected.\r\n    #[test]\r\n    fn envelope_with_unknown_trailing_field_decodes_on_older_peer() {\r\n        let newer_wire = json!({\r\n            \"protocol_version\": 2,\r\n            \"kind\": \"attach\",\r\n            \"payload\": { \"session\": \"abc\" },\r\n            \"future_field_an_older_broker_never_heard_of\": { \"nested\": [1, 2, 3] }\r\n        })\r\n        .to_string();\r\n\r\n        let decoded: Envelope =\r\n            serde_json::from_str(&newer_wire).expect(\"older peer must tolerate unknown fields\");\r\n        assert_eq!(decoded.protocol_version, 2);\r\n        assert_eq!(decoded.kind, \"attach\");\r\n        assert_eq!(decoded.payload, json!({ \"session\": \"abc\" }));\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n    // An unknown *kind* is not a decode error — the envelope parses and the\r\n    // logic layer is what decides it cannot handle the kind.\r\n    #[test]\r\n    fn unknown_kind_is_not_a_decode_error() {\r\n        let wire = json!({ \"protocol_version\": 1, \"kind\": \"a_kind_from_the_future\" }).to_string();\r\n        let decoded: Envelope = serde_json::from_str(&wire).unwrap();\r\n        assert_eq!(decoded.kind, \"a_kind_from_the_future\");\r\n        assert!(decoded.payload.is_null()); // payload defaulted\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\r\n    // A newer brain (higher protocol version, extra Hello fields) is accepted by\r\n    // an older broker — the exact \"newer brain ↔ older broker\" direction 2.3\r\n    // demands.\r\n    #[test]\r\n    fn handshake_accepts_newer_brain_on_older_broker() {\r\n        let newer_hello_wire = json!({\r\n            \"protocol_version\": IPC_PROTOCOL_VERSION + 5,\r\n            \"role\": \"brain\",\r\n            \"added_in_a_future_release\": true\r\n        })\r\n        .to_string();\r\n        let hello: Hello = serde_json::from_str(&newer_hello_wire).unwrap();\r\n        assert_eq!(accept_hello(&hello, Role::Brain), Ok(()));\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,null,225]},"fileSize":9595,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\frame.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":225,"totalBytes":3534,"outputLines":83,"outputBytes":3534,"shownRange":{"start":119,"end":201},"nextOffset":202}}},"isError":false,"timestamp":1789394609074}}
{"type":"message","id":"ec53073b","parentId":"74b74435","timestamp":"2026-09-14T14:03:29.078Z","message":{"role":"toolResult","toolCallId":"call_pSp9BKli5q5RpmyKyDpOLpp6|fc_0a63d1e8a5b46c9c016aa7feb0f4b087d185bd2308291a68c4","toolName":"read","content":[{"type":"text","text":"/// Release a claim only if the entry still belongs to `generation`.\n///\n/// THE CASE THIS EXISTS FOR: a wedged holder is taken over, the new claimant\n/// holds generation N+1, and the old holder finally completes and drops. A\n/// generation-blind remove would free the NEW claimant's claim while its spawn\n/// is still in flight, so the next duplicate wake sails through the gate —\n/// re-opening, one wake later, exactly the double-create this guard exists to\n/// close. Pure over the map so the no-op is a unit.\nfn release_claim(map: &mut HashMap<String, WakeClaim>, endpoint: &str, generation: u64) {\n    if map.get(endpoint).is_some_and(|c| c.generation == generation) {\n        map.remove(endpoint);\n    }\n}\n\n/// How long one spawn phase may sit without progress before its holder reads as\n/// WEDGED rather than slow.\n///\n/// PER-PHASE AND DELIBERATELY GENEROUS. `pty_creating` is the measured-slow\n/// phase — the #235 double-create came from a winner whose ConPTY create ran\n/// past the old 2s global wait — so its ceiling errs large: the cost of waiting\n/// on a slow-but-live holder is latency, while the cost of taking over from one\n/// is the duplicate session this guard exists to prevent. The ceiling is a WEDGE\n/// detector, never a performance budget.\nfn phase_wedge_ceiling(phase: &str) -> Duration {\n    // TEST-ONLY SEAM, and it does not exist in a shipped binary: the int rig needs the\n    // wedge arm to fire in milliseconds rather than after a real ceiling, but a stray\n    // value in a deployment would be WORSE than a slow spawn — a ceiling of 0 makes\n    // every claim read stalled, so every duplicate wake takes over and the double-create\n    // this function exists to prevent comes back at scale. Compiled out of release\n    // entirely, parsed once, and floored so that even in a debug build it cannot be\n    // driven below a value the gate could mistake for a wedge on a healthy holder.\n    #[cfg(debug_assertions)]\n    {\n        static OVERRIDE: std::sync::OnceLock<Option<Duration>> = std::sync::OnceLock::new();\n        if let Some(d) = OVERRIDE.get_or_init(|| {\n            let raw = std::env::var(\"SPT_WAKE_WEDGE_CEILING_MS\")\n                .ok()\n                .and_then(|v| v.parse::<u64>().ok())?;\n            let clamped = raw.clamp(50, 600_000);\n            eprintln!(\n                \"SPAWN_WEDGE_CEILING_OVERRIDE: test seam active — every phase ceiling is \\\n                 {clamped}ms (asked {raw}ms). Debug builds only; never a shipped path.\"\n            );\n            Some(Duration::from_millis(clamped))\n        }) {\n            return *d;\n        }\n    }\n    match phase {\n        \"pty_creating\" => Duration::from_secs(120),\n        _ => Duration::from_secs(30),\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\n[Showing lines 293-348 of 16852. Use :349 to continue]"}],"details":{"truncation":{"content":"/// Release a claim only if the entry still belongs to `generation`.\r\n///\r\n/// THE CASE THIS EXISTS FOR: a wedged holder is taken over, the new claimant\r\n/// holds generation N+1, and the old holder finally completes and drops. A\r\n/// generation-blind remove would free the NEW claimant's claim while its spawn\r\n/// is still in flight, so the next duplicate wake sails through the gate —\r\n/// re-opening, one wake later, exactly the double-create this guard exists to\r\n/// close. Pure over the map so the no-op is a unit.\r\nfn release_claim(map: &mut HashMap<String, WakeClaim>, endpoint: &str, generation: u64) {\r\n    if map.get(endpoint).is_some_and(|c| c.generation == generation) {\r\n        map.remove(endpoint);\r\n    }\r\n}\r\n\r\n/// How long one spawn phase may sit without progress before its holder reads as\r\n/// WEDGED rather than slow.\r\n///\r\n/// PER-PHASE AND DELIBERATELY GENEROUS. `pty_creating` is the measured-slow\r\n/// phase — the #235 double-create came from a winner whose ConPTY create ran\r\n/// past the old 2s global wait — so its ceiling errs large: the cost of waiting\r\n/// on a slow-but-live holder is latency, while the cost of taking over from one\r\n/// is the duplicate session this guard exists to prevent. The ceiling is a WEDGE\r\n/// detector, never a performance budget.\r\nfn phase_wedge_ceiling(phase: &str) -> Duration {\r\n    // TEST-ONLY SEAM, and it does not exist in a shipped binary: the int rig needs the\r\n    // wedge arm to fire in milliseconds rather than after a real ceiling, but a stray\r\n    // value in a deployment would be WORSE than a slow spawn — a ceiling of 0 makes\r\n    // every claim read stalled, so every duplicate wake takes over and the double-create\r\n    // this function exists to prevent comes back at scale. Compiled out of release\r\n    // entirely, parsed once, and floored so that even in a debug build it cannot be\r\n    // driven below a value the gate could mistake for a wedge on a healthy holder.\r\n    #[cfg(debug_assertions)]\r\n    {\r\n        static OVERRIDE: std::sync::OnceLock<Option<Duration>> = std::sync::OnceLock::new();\r\n        if let Some(d) = OVERRIDE.get_or_init(|| {\r\n            let raw = std::env::var(\"SPT_WAKE_WEDGE_CEILING_MS\")\r\n                .ok()\r\n                .and_then(|v| v.parse::<u64>().ok())?;\r\n            let clamped = raw.clamp(50, 600_000);\r\n            eprintln!(\r\n                \"SPAWN_WEDGE_CEILING_OVERRIDE: test seam active — every phase ceiling is \\\r\n                 {clamped}ms (asked {raw}ms). Debug builds only; never a shipped path.\"\r\n            );\r\n            Some(Duration::from_millis(clamped))\r\n        }) {\r\n            return *d;\r\n        }\r\n    }\r\n    match phase {\r\n        \"pty_creating\" => Duration::from_secs(120),\r\n        _ => Duration::from_secs(30),\r\n    }\r\n}\r\n\r\n/// The pure single-flight wake decision (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP), over\r\n/// the three observable inputs read together under the broker lock. `AlreadyLive` wins\r","truncated":true,"truncatedBy":"lines","totalLines":16560,"totalBytes":2970,"outputLines":56,"outputBytes":2970,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":16852,"displayContent":{"text":"/// Release a claim only if the entry still belongs to `generation`.\r\n///\r\n/// THE CASE THIS EXISTS FOR: a wedged holder is taken over, the new claimant\r\n/// holds generation N+1, and the old holder finally completes and drops. A\r\n/// generation-blind remove would free the NEW claimant's claim while its spawn\r\n/// is still in flight, so the next duplicate wake sails through the gate —\r\n/// re-opening, one wake later, exactly the double-create this guard exists to\r\n/// close. Pure over the map so the no-op is a unit.\r\nfn release_claim(map: &mut HashMap<String, WakeClaim>, endpoint: &str, generation: u64) {\r\n    if map.get(endpoint).is_some_and(|c| c.generation == generation) {\r\n        map.remove(endpoint);\r\n    }\r\n}\r\n\r\n/// How long one spawn phase may sit without progress before its holder reads as\r\n/// WEDGED rather than slow.\r\n///\r\n/// PER-PHASE AND DELIBERATELY GENEROUS. `pty_creating` is the measured-slow\r\n/// phase — the #235 double-create came from a winner whose ConPTY create ran\r\n/// past the old 2s global wait — so its ceiling errs large: the cost of waiting\r\n/// on a slow-but-live holder is latency, while the cost of taking over from one\r\n/// is the duplicate session this guard exists to prevent. The ceiling is a WEDGE\r\n/// detector, never a performance budget.\r\nfn phase_wedge_ceiling(phase: &str) -> Duration {\r\n    // TEST-ONLY SEAM, and it does not exist in a shipped binary: the int rig needs the\r\n    // wedge arm to fire in milliseconds rather than after a real ceiling, but a stray\r\n    // value in a deployment would be WORSE than a slow spawn — a ceiling of 0 makes\r\n    // every claim read stalled, so every duplicate wake takes over and the double-create\r\n    // this function exists to prevent comes back at scale. Compiled out of release\r\n    // entirely, parsed once, and floored so that even in a debug build it cannot be\r\n    // driven below a value the gate could mistake for a wedge on a healthy holder.\r\n    #[cfg(debug_assertions)]\r\n    {\r\n        static OVERRIDE: std::sync::OnceLock<Option<Duration>> = std::sync::OnceLock::new();\r\n        if let Some(d) = OVERRIDE.get_or_init(|| {\r\n            let raw = std::env::var(\"SPT_WAKE_WEDGE_CEILING_MS\")\r\n                .ok()\r\n                .and_then(|v| v.parse::<u64>().ok())?;\r\n            let clamped = raw.clamp(50, 600_000);\r\n            eprintln!(\r\n                \"SPAWN_WEDGE_CEILING_OVERRIDE: test seam active — every phase ceiling is \\\r\n                 {clamped}ms (asked {raw}ms). Debug builds only; never a shipped path.\"\r\n            );\r\n            Some(Duration::from_millis(clamped))\r\n        }) {\r\n            return *d;\r\n        }\r\n    }\r\n    match phase {\r\n        \"pty_creating\" => Duration::from_secs(120),\r\n        _ => Duration::from_secs(30),\r\n    }\r\n}\r\n\r\n/// The pure single-flight wake decision (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP), over\r\n/// the three observable inputs read together under the broker lock. `AlreadyLive` wins\r","startLine":293,"lineNumbers":[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]},"fileSize":877537,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":16852,"totalBytes":2970,"outputLines":56,"outputBytes":2970,"shownRange":{"start":293,"end":348},"nextOffset":349}}},"isError":false,"timestamp":1789394609078}}
{"type":"message","id":"ddce5e49","parentId":"ec53073b","timestamp":"2026-09-14T14:03:29.080Z","message":{"role":"toolResult","toolCallId":"call_HJ8cy5g9eGlCtxV3eE8vbWx7|fc_0a63d1e8a5b46c9c016aa7feb0f4b887d1954519fa16c41d53","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-proto/src/\n## emit.rs\n 29|//!\n*30|//! The handle is a parameter everywhere here. A hardcoded `stderr()` would make\n 31|//! the write count unobservable, and an unobservable property is not a property.\n 32|\n 33|use std::io::{self, Write};\n...\n 121|// [impl->REQ-EMIT-SINGLE-WRITE]\n*122|pub fn write_line<W: Write + ?Sized>(w: &mut W, rendered: &str) -> io::Result<()> {\n 123|    debug_assert!(\n 124|        !has_interior_break(rendered),\n 125|        \"emit_line! got an interior line break — use emit_block! if the emission \\\n...\n 186|#[macro_export]\n*187|macro_rules! emit_line_err {\n 188|    ($($arg:tt)*) => {{\n 189|        let _ = $crate::emit::write_line(\n*190|            &mut ::std::io::stderr().lock(),\n 191|            &::std::format!($($arg)*),\n 192|        );\n 193|    }};\n...\n 202|        let _ = $crate::emit::write_block(\n*203|            &mut ::std::io::stderr().lock(),\n 204|            &::std::format!($($arg)*),\n 205|        );\n 206|    }};\n...\n 360|    #[test]\n*361|    fn emit_line_issues_exactly_one_write_of_the_whole_line() {\n 362|        let id = \"endpoint-7\";\n 363|        let detail = \"the store did not persist\";\n 364|\n...\n 377|    #[test]\n*378|    fn emit_line_is_one_write_for_a_static_literal_too() {\n 379|        let mut w = CountingWriter::new();\n 380|        emit_line!(&mut w, \"PUMP_IPC_READER: exited\").expect(\"write\");\n 381|        assert_eq!(w.sole_write(), b\"PUMP_IPC_READER: exited\\n\");\n...\n 433|    #[should_panic(expected = \"interior line break\")]\n*434|    fn write_line_debug_asserts_on_an_interior_newline() {\n 435|        let mut w = CountingWriter::new();\n 436|        let _ = write_line(&mut w, \"SERVICE_STARTUP_FAULT:svc: 3 exits\\ntail line\");\n 437|    }\n...\n 455|    #[test]\n*456|    fn emit_block_keeps_interior_newlines_and_is_still_one_write() {\n 457|        let fix = \"netsh advfirewall ...\";\n 458|        let mut w = CountingWriter::new();\n 459|        emit_block!(\n...\n 533|    #[test]\n*534|    fn emit_block_terminates_exactly_once() {\n 535|        let mut w = CountingWriter::new();\n 536|        emit_block!(&mut w, \"TOKEN:a: line one\\nline two\").expect(\"write\");\n 537|        assert_eq!(w.sole_write(), b\"TOKEN:a: line one\\nline two\\n\");"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs, .worktrees/297-enforcement-shape/crates/spt-proto/src/lib.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-proto\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":10,"fileCount":1,"files":[".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs","count":10}],"truncated":false,"displayContent":"# .worktrees/297-enforcement-shape/crates/spt-proto/src/\n## emit.rs\n  29│//!\n *30│//! The handle is a parameter everywhere here. A hardcoded `stderr()` would make\n  31│//! the write count unobservable, and an unobservable property is not a property.\n  32│\n  33│use std::io::{self, Write};\n    │...\n 121│// [impl->REQ-EMIT-SINGLE-WRITE]\n*122│pub fn write_line<W: Write + ?Sized>(w: &mut W, rendered: &str) -> io::Result<()> {\n 123│    debug_assert!(\n 124│        !has_interior_break(rendered),\n 125│        \"emit_line! got an interior line break — use emit_block! if the emission \\\n    │...\n 186│#[macro_export]\n*187│macro_rules! emit_line_err {\n 188│    ($($arg:tt)*) => {{\n 189│        let _ = $crate::emit::write_line(\n*190│            &mut ::std::io::stderr().lock(),\n 191│            &::std::format!($($arg)*),\n 192│        );\n 193│    }};\n    │...\n 202│        let _ = $crate::emit::write_block(\n*203│            &mut ::std::io::stderr().lock(),\n 204│            &::std::format!($($arg)*),\n 205│        );\n 206│    }};\n    │...\n 360│    #[test]\n*361│    fn emit_line_issues_exactly_one_write_of_the_whole_line() {\n 362│        let id = \"endpoint-7\";\n 363│        let detail = \"the store did not persist\";\n 364│\n    │...\n 377│    #[test]\n*378│    fn emit_line_is_one_write_for_a_static_literal_too() {\n 379│        let mut w = CountingWriter::new();\n 380│        emit_line!(&mut w, \"PUMP_IPC_READER: exited\").expect(\"write\");\n 381│        assert_eq!(w.sole_write(), b\"PUMP_IPC_READER: exited\\n\");\n    │...\n 433│    #[should_panic(expected = \"interior line break\")]\n*434│    fn write_line_debug_asserts_on_an_interior_newline() {\n 435│        let mut w = CountingWriter::new();\n 436│        let _ = write_line(&mut w, \"SERVICE_STARTUP_FAULT:svc: 3 exits\\ntail line\");\n 437│    }\n    │...\n 455│    #[test]\n*456│    fn emit_block_keeps_interior_newlines_and_is_still_one_write() {\n 457│        let fix = \"netsh advfirewall ...\";\n 458│        let mut w = CountingWriter::new();\n 459│        emit_block!(\n    │...\n 533│    #[test]\n*534│    fn emit_block_terminates_exactly_once() {\n 535│        let mut w = CountingWriter::new();\n 536│        emit_block!(&mut w, \"TOKEN:a: line one\\nline two\").expect(\"write\");\n 537│        assert_eq!(w.sole_write(), b\"TOKEN:a: line one\\nline two\\n\");"},"isError":false,"timestamp":1789394609080}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IrPitsFrDVURGDJYfC3pQ4wL|fc_0a63d1e8a5b46c9c016aa7feb67afc87d1a6ca56e8208d800c","toolName":"hub","startedAt":"2026-09-14T14:03:34.960Z","intent":"Sharing phase trace caveat"},"id":"56475b85","parentId":"ddce5e49","timestamp":"2026-09-14T14:03:34.960Z"}
{"type":"message","id":"35b7b0e0","parentId":"56475b85","timestamp":"2026-09-14T14:03:34.961Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Refining waiter stamp ordering**"},{"type":"toolCall","id":"call_IrPitsFrDVURGDJYfC3pQ4wL|fc_0a63d1e8a5b46c9c016aa7feb67afc87d1a6ca56e8208d800c","name":"hub","arguments":{"i":"Sharing phase trace caveat","op":"send","to":"Main","message":"Important precise interval: spawn_phase closure prints BEFORE recover(wake_inflight) at broker8113-8120. Seeing waiter_armed does not strictly prove build_translation entered: it may stall acquiring/updating claim after print. Next translation_ready similarly emits before claim update. Logging takes stderr lock emit.rs187-193; absent next phase localizes interval, not exact syscall. Plain reply-writer/correlation cause would normally require row_inserted first (not observed)."},"intent":"Sharing phase trace caveat"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":2627,"output":146,"cacheRead":62720,"cacheWrite":0,"totalTokens":65493,"reasoningTokens":18,"cost":{"input":0.02627,"output":0.0073,"cacheRead":0.06272,"cacheWrite":0,"total":0.09629}},"stopReason":"toolUse","timestamp":1789394609110,"responseId":"resp_0a63d1e8a5b46c9c016aa7feb17e3087d19d8896431d7a4834","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7feb28e3c87d1a5cf7f3aa26d59d9","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_62Pxb1zJVLcxftD366Q5PsDoSewmbjGp1h_8R5zFLXy7z3uhL55PkfouU_NNLV5PgA4gMAH_Yh4b23iHlL_uZg9fwfXfXrlzP58mRcGSvUCARlho9UoUR5UT0pSJ1aSv4fisBNRfcZJJtJqQMHABDhq4pHBX1aDXBEavp1Ap-9GpLwsQ6IG1-p8gPv9euF7W6NgWEoq7O4XmpaUcVJEJ-J263BQrsJbrjMrjTLU2Ntgl0eh0xbdCj0C0c_OacSnOvB4PjA-r_QjIbYCphH0C6p5FHllOehFFR5qub9vsDd7l3nGewvdyU1YJvD2iKGjxAhJg6yFDvbOetAmax_em0joyLy-9YBPILwdk_y9hgtWzsVS8EYpz_YWRTPYDeGp3CPJ7zdCo7Ximr86UCFIzdMY0CluSgDxz9S3FGb9UztbyLYfcNnW8ng1eciokPlWWchcOChvw-Q0WbVINkXZAZRD9nabEOcvCQ7yoo_Lmy0f-BlBBIvOYA3jF1dy51CGWfGEUT_3cR-5STYP3VQsEPt63xJHbs-aSTbklZA8wZJvwNVIi63LdIc3UE5l8U1FF9CfE-05uQR9_TBSJ-aqZ7aAhcCMQdTYvnYAWl8bU7Ins70iY9OEJ1bc58Mtv-NcvMYFMrUvVwEpY4wPYi_DTFn-YH3i0qCBYcIOV6aJ5-8BD7IyEbhIHcLlibltFGsfQyzawsWd5DJFwdmMKe9uYSIsqJ37THZwB9oV7fwDpJsC1Er2jsKe2Tdx1KP5c9qAEWAJO0rouU-kZm0RL8e-EyMXGgswGCfcVtOXEPOGPC2UJpLVywmRKM7tmrAVKbu1PA4NfVz-jpSBiSEpEKbwq0x-o6Xtq11MuQcCSjsqEyqGwFMa72AA6dRpAOngYwMGxr-TCI6OQMfYjad31zGeSmuLfCPsYuFmz3_1wgfylofnaxmqcKl_UzDCcEIsZG2uNuNuf3WfsQSJbihAuXkhZdMoz9QxiP2oiz0fIRQW4LTv1L1HWgpV9kpEG047y0P2QzRnBq5NO5F0SfCbW51iK0dgBXvui_jELKw3nzGdNk-4X86Ch1ndh8y6wPX9y9Y0smK9RYtLqSD_JRWNgi_4iMUX7e-2v4xiL_Bb3L598DYjvoaCSU4auvKCZNRCUBTBRLWFc4_Ql6tYpklzMXMZNahNoGEXGJpYMI6cwOf2pK5xh0xQjb5XJ0qaLSu7aN4-B6Dpludj9SnQzkO-c4211eBLq3J3TKzujSQIThWlcYmaOABCL5RbAfOhjoXC-TV5QDIJKoEMcFR4SmIjIWigvjeCKPdxMvdeUpupZdxEcksUXH550cHmNBjD8E7tGvm0l699NhmYKEbHaTFmP6abpafvFsJTyX8x2d1kPpxa2vQSkc=","internal_chat_message_metadata_passthrough":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"summary":[{"type":"summary_text","text":"**Refining waiter stamp ordering**"}],"metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}},{"id":"fc_0a63d1e8a5b46c9c016aa7feb67afc87d1a6ca56e8208d800c","type":"function_call","status":"completed","arguments":"{\"i\":\"Sharing phase trace caveat\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Important precise interval: spawn_phase closure prints BEFORE recover(wake_inflight) at broker8113-8120. Seeing waiter_armed does not strictly prove build_translation entered: it may stall acquiring/updating claim after print. Next translation_ready similarly emits before claim update. Logging takes stderr lock emit.rs187-193; absent next phase localizes interval, not exact syscall. Plain reply-writer/correlation cause would normally require row_inserted first (not observed).\"}","call_id":"call_IrPitsFrDVURGDJYfC3pQ4wL","internal_chat_message_metadata_passthrough":{"create_time":1789394609.663084,"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"},"name":"hub","metadata":{"turn_id":"51f7073d-e633-49a4-ab90-55c40e869a8e"}}]},"duration":5848.781100004911,"ttft":1468.6891000121832,"completedAt":1789394614960,"contextSnapshot":{"promptTokens":65347,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"d5792680","parentId":"35b7b0e0","timestamp":"2026-09-14T14:03:34.961Z","message":{"role":"toolResult","toolCallId":"call_IrPitsFrDVURGDJYfC3pQ4wL|fc_0a63d1e8a5b46c9c016aa7feb67afc87d1a6ca56e8208d800c","toolName":"hub","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"SpawnReplyTrace","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1789394614961}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `NetRuntimeTrace`:\n\nDecisive exact path: broker4702 build_translation ONLY TranslationChild::spawn -> os_hostname -> child.send Init -> std thread spawn; NO NetHost or journal/stream call and NO translation ready handshake. Translation spawn std Command::spawn translation266 unbounded; Init synchronous stdin mutex/write_all/flush 311-320 unbounded. NetHost existing before cold_start (fixture570); budget includes PTY ~0.9/1.3s then those startup ops. Sending Main same.\n\nSent while waiting/working. Active interruptible wait stopped early for immediate reading.\n\nIf response expected, reply via `hub` (`op: \"send\"`, `to: \"NetRuntimeTrace\"`); may finish current step first. No one replies on your behalf.\n</irc>","display":true,"details":{"id":"157f5c0e1706a0cd","from":"NetRuntimeTrace","message":"Decisive exact path: broker4702 build_translation ONLY TranslationChild::spawn -> os_hostname -> child.send Init -> std thread spawn; NO NetHost or journal/stream call and NO translation ready handshake. Translation spawn std Command::spawn translation266 unbounded; Init synchronous stdin mutex/write_all/flush 311-320 unbounded. NetHost existing before cold_start (fixture570); budget includes PTY ~0.9/1.3s then those startup ops. Sending Main same."},"attribution":"agent","id":"504e3b5a","parentId":"d5792680","timestamp":"2026-09-14T14:03:30.269Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BJrYRanAE15Zc0PTpxMoMBqe|fc_0a63d1e8a5b46c9c016aa7fec1368487d1abdc8fccd2d2f125","toolName":"read","startedAt":"2026-09-14T14:03:45.333Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:4690-4795"},"intent":"Reading translation startup choke"},"id":"7dfeea22","parentId":"504e3b5a","timestamp":"2026-09-14T14:03:45.333Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mLao8oHu5cBfr8NRWfzrUcGL|fc_0a63d1e8a5b46c9c016aa7fec1369087d1adc143996d3a3443","toolName":"read","startedAt":"2026-09-14T14:03:45.333Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/translation.rs:228-323"},"intent":"Reading direct translation operations"},"id":"e29982c1","parentId":"7dfeea22","timestamp":"2026-09-14T14:03:45.333Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Utd4gzd3nv2mqoCbkxGswORd|fc_0a63d1e8a5b46c9c016aa7fec1369887d193a4987835cd3160","toolName":"grep","startedAt":"2026-09-14T14:03:45.334Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs"},"intent":"Locating hostname and connection diagnostics"},"id":"083faae6","parentId":"e29982c1","timestamp":"2026-09-14T14:03:45.334Z"}
{"type":"message","id":"bdc05902","parentId":"083faae6","timestamp":"2026-09-14T14:03:45.334Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_BJrYRanAE15Zc0PTpxMoMBqe|fc_0a63d1e8a5b46c9c016aa7fec1368487d1abdc8fccd2d2f125","name":"read","arguments":{"i":"Reading translation startup choke","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:4690-4795"},"intent":"Reading translation startup choke"},{"type":"toolCall","id":"call_mLao8oHu5cBfr8NRWfzrUcGL|fc_0a63d1e8a5b46c9c016aa7fec1369087d1adc143996d3a3443","name":"read","arguments":{"i":"Reading direct translation operations","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/translation.rs:228-323"},"intent":"Reading direct translation operations"},{"type":"toolCall","id":"call_Utd4gzd3nv2mqoCbkxGswORd|fc_0a63d1e8a5b46c9c016aa7fec1369887d193a4987835cd3160","name":"grep","arguments":{"i":"Locating hostname and connection diagnostics","pattern":"fn os_hostname|CONN_|kind=|frame.kind","path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs","case":true,"gitignore":false,"skip":null},"intent":"Locating hostname and connection diagnostics"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":538,"output":194,"cacheRead":65152,"cacheWrite":0,"totalTokens":65884,"cost":{"input":0.00538,"output":0.0097,"cacheRead":0.065152,"cacheWrite":0,"total":0.080232}},"stopReason":"toolUse","timestamp":1789394614967,"responseId":"resp_0a63d1e8a5b46c9c016aa7feb7415087d1aa651610f1106bae","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fec1368487d1abdc8fccd2d2f125","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading translation startup choke\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs:4690-4795\"}","call_id":"call_BJrYRanAE15Zc0PTpxMoMBqe","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"read","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fec1369087d1adc143996d3a3443","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading direct translation operations\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/translation.rs:228-323\"}","call_id":"call_mLao8oHu5cBfr8NRWfzrUcGL","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"read","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fec1369887d193a4987835cd3160","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating hostname and connection diagnostics\",\"pattern\":\"fn os_hostname|CONN_|kind=|frame.kind\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs;.worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_Utd4gzd3nv2mqoCbkxGswORd","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"grep","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}}]},"duration":10362.019600003958,"ttft":10300.510800004005,"completedAt":1789394625330,"contextSnapshot":{"promptTokens":65690,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"e032920b","parentId":"bdc05902","timestamp":"2026-09-14T14:03:45.373Z","message":{"role":"toolResult","toolCallId":"call_mLao8oHu5cBfr8NRWfzrUcGL|fc_0a63d1e8a5b46c9c016aa7fec1369087d1adc143996d3a3443","toolName":"read","content":[{"type":"text","text":"pub struct TranslationChild {\n…\n    /// content-free pings — Layer C).\n    child: Mutex<Option<Child>>,\n    /// `None` after [`close_stdin`](Self::close_stdin) / reap has taken it.\n    stdin: Mutex<Option<ChildStdin>>,\n    _reader: JoinHandle<()>,\n    pid: Option<u32>,\n}\n\nimpl TranslationChild {\n    /// Spawn `argv` (program + args) as a stdin→stdout filter, forwarding each\n    /// parsed [`KeyCmd`] on `cmds`. stderr is inherited so an adapter author can\n    /// see the binary's own diagnostics. Fails closed: a missing/unspawnable\n    /// binary returns the OS error (the broker logs it and `None` → idle inbound\n    /// spools, never raw-injected). `argv[0]` is the (install-dir-resolved) program;\n    /// `argv[1..]` are the resolved `command` args (ADR-0029).\n    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n    // [impl->REQ-TRANSLATE-COMMAND]\n    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\n    pub fn spawn(argv: &[String], cmds: Sender<KeyCmd>) -> io::Result<Self> {\n        let (program, args) = argv\n            .split_first()\n            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, \"empty translation argv\"))?;\n        let mut command = Command::new(program);\n        command.args(args);\n        command\n            .stdin(Stdio::piped())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::inherit());\n        // On Windows a GUI-subsystem parent (the harness) spawning this console\n        // child pops a VISIBLE console window; CREATE_NO_WINDOW suppresses it\n        // without detaching (the raw `Command::new` bypassed ManifestRuntime, which\n        // sets this for its spawns — runtime/brainproc parity). No-op elsewhere.\n        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\n        #[cfg(windows)]\n        {\n            use std::os::windows::process::CommandExt;\n            const CREATE_NO_WINDOW: u32 = 0x0800_0000;\n            command.creation_flags(CREATE_NO_WINDOW);\n        }\n        let mut child = command.spawn()?;\n        let pid = child.id().into();\n        let stdout = child\n            .stdout\n            .take()\n            .expect(\"stdout piped at spawn\");\n        let stdin = child.stdin.take().expect(\"stdin piped at spawn\");\n        let reader = thread::spawn(move || {\n            let buf = BufReader::new(stdout);\n            for line in buf.lines() {\n                let Ok(line) = line else { break }; // child stdout closed\n                let trimmed = line.trim();\n                if trimmed.is_empty() {\n                    continue;\n                }\n                match serde_json::from_str::<KeyCmd>(trimmed) {\n                    Ok(cmd) => {\n                        if cmds.send(cmd).is_err() {\n                            break; // broker dropped the receiver — stop reading\n                        }\n                    }\n                    Err(e) => {\n                        spt_proto::emit_line_err!(\"TRANSLATION_BIN_BAD_LINE: {e}: {trimmed}\");\n                    }\n                }\n            }\n        });\n        Ok(TranslationChild {\n            child: Mutex::new(Some(child)),\n            stdin: Mutex::new(Some(stdin)),\n            _reader: reader,\n            pid,\n        })\n    }\n\n    /// The child's OS pid (for tests / introspection / the no-zombie assertion).\n    pub fn pid(&self) -> Option<u32> {\n        self.pid\n    }\n\n    /// Write one [`ToBinary`] line to the child's stdin (newline-terminated,\n    /// flushed). A closed stdin (post-shutdown) is a no-op `Ok`.\n    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n    pub fn send(&self, msg: &ToBinary) -> io::Result<()> {\n        let line = serde_json::to_string(msg).map_err(io::Error::other)?;\n        let mut guard = self.stdin.lock().unwrap();\n        if let Some(w) = guard.as_mut() {\n            w.write_all(line.as_bytes())?;\n            w.write_all(b\"\\n\")?;\n            w.flush()?;\n        }\n        Ok(())\n    }\n\n    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then\n    /// exits on its own. Idempotent.\n    fn close_stdin(&self) {\n        let _ = self.stdin.lock().unwrap().take();\n    }\n\n    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child\n…\n}\n\n[Showing lines 227-326 of 507. Use :327 to continue]"}],"details":{"truncation":{"content":"    /// content-free pings — Layer C).\r\n    child: Mutex<Option<Child>>,\r\n    /// `None` after [`close_stdin`](Self::close_stdin) / reap has taken it.\r\n    stdin: Mutex<Option<ChildStdin>>,\r\n    _reader: JoinHandle<()>,\r\n    pid: Option<u32>,\r\n}\r\n\r\nimpl TranslationChild {\r\n    /// Spawn `argv` (program + args) as a stdin→stdout filter, forwarding each\r\n    /// parsed [`KeyCmd`] on `cmds`. stderr is inherited so an adapter author can\r\n    /// see the binary's own diagnostics. Fails closed: a missing/unspawnable\r\n    /// binary returns the OS error (the broker logs it and `None` → idle inbound\r\n    /// spools, never raw-injected). `argv[0]` is the (install-dir-resolved) program;\r\n    /// `argv[1..]` are the resolved `command` args (ADR-0029).\r\n    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n    // [impl->REQ-TRANSLATE-COMMAND]\r\n    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\r\n    pub fn spawn(argv: &[String], cmds: Sender<KeyCmd>) -> io::Result<Self> {\r\n        let (program, args) = argv\r\n            .split_first()\r\n            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, \"empty translation argv\"))?;\r\n        let mut command = Command::new(program);\r\n        command.args(args);\r\n        command\r\n            .stdin(Stdio::piped())\r\n            .stdout(Stdio::piped())\r\n            .stderr(Stdio::inherit());\r\n        // On Windows a GUI-subsystem parent (the harness) spawning this console\r\n        // child pops a VISIBLE console window; CREATE_NO_WINDOW suppresses it\r\n        // without detaching (the raw `Command::new` bypassed ManifestRuntime, which\r\n        // sets this for its spawns — runtime/brainproc parity). No-op elsewhere.\r\n        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\r\n        #[cfg(windows)]\r\n        {\r\n            use std::os::windows::process::CommandExt;\r\n            const CREATE_NO_WINDOW: u32 = 0x0800_0000;\r\n            command.creation_flags(CREATE_NO_WINDOW);\r\n        }\r\n        let mut child = command.spawn()?;\r\n        let pid = child.id().into();\r\n        let stdout = child\r\n            .stdout\r\n            .take()\r\n            .expect(\"stdout piped at spawn\");\r\n        let stdin = child.stdin.take().expect(\"stdin piped at spawn\");\r\n        let reader = thread::spawn(move || {\r\n            let buf = BufReader::new(stdout);\r\n            for line in buf.lines() {\r\n                let Ok(line) = line else { break }; // child stdout closed\r\n                let trimmed = line.trim();\r\n                if trimmed.is_empty() {\r\n                    continue;\r\n                }\r\n                match serde_json::from_str::<KeyCmd>(trimmed) {\r\n                    Ok(cmd) => {\r\n                        if cmds.send(cmd).is_err() {\r\n                            break; // broker dropped the receiver — stop reading\r\n                        }\r\n                    }\r\n                    Err(e) => {\r\n                        spt_proto::emit_line_err!(\"TRANSLATION_BIN_BAD_LINE: {e}: {trimmed}\");\r\n                    }\r\n                }\r\n            }\r\n        });\r\n        Ok(TranslationChild {\r\n            child: Mutex::new(Some(child)),\r\n            stdin: Mutex::new(Some(stdin)),\r\n            _reader: reader,\r\n            pid,\r\n        })\r\n    }\r\n\r\n    /// The child's OS pid (for tests / introspection / the no-zombie assertion).\r\n    pub fn pid(&self) -> Option<u32> {\r\n        self.pid\r\n    }\r\n\r\n    /// Write one [`ToBinary`] line to the child's stdin (newline-terminated,\r\n    /// flushed). A closed stdin (post-shutdown) is a no-op `Ok`.\r\n    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n    pub fn send(&self, msg: &ToBinary) -> io::Result<()> {\r\n        let line = serde_json::to_string(msg).map_err(io::Error::other)?;\r\n        let mut guard = self.stdin.lock().unwrap();\r\n        if let Some(w) = guard.as_mut() {\r\n            w.write_all(line.as_bytes())?;\r\n            w.write_all(b\"\\n\")?;\r\n            w.flush()?;\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then\r\n    /// exits on its own. Idempotent.\r\n    fn close_stdin(&self) {\r\n        let _ = self.stdin.lock().unwrap().take();\r\n    }\r\n\r\n    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child\r","truncated":true,"truncatedBy":"lines","totalLines":281,"totalBytes":4229,"outputLines":100,"outputBytes":4229,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":507,"displayContent":{"text":"pub struct TranslationChild {\n…\n    /// content-free pings — Layer C).\r\n    child: Mutex<Option<Child>>,\r\n    /// `None` after [`close_stdin`](Self::close_stdin) / reap has taken it.\r\n    stdin: Mutex<Option<ChildStdin>>,\r\n    _reader: JoinHandle<()>,\r\n    pid: Option<u32>,\r\n}\r\n\r\nimpl TranslationChild {\r\n    /// Spawn `argv` (program + args) as a stdin→stdout filter, forwarding each\r\n    /// parsed [`KeyCmd`] on `cmds`. stderr is inherited so an adapter author can\r\n    /// see the binary's own diagnostics. Fails closed: a missing/unspawnable\r\n    /// binary returns the OS error (the broker logs it and `None` → idle inbound\r\n    /// spools, never raw-injected). `argv[0]` is the (install-dir-resolved) program;\r\n    /// `argv[1..]` are the resolved `command` args (ADR-0029).\r\n    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n    // [impl->REQ-TRANSLATE-COMMAND]\r\n    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\r\n    pub fn spawn(argv: &[String], cmds: Sender<KeyCmd>) -> io::Result<Self> {\r\n        let (program, args) = argv\r\n            .split_first()\r\n            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, \"empty translation argv\"))?;\r\n        let mut command = Command::new(program);\r\n        command.args(args);\r\n        command\r\n            .stdin(Stdio::piped())\r\n            .stdout(Stdio::piped())\r\n            .stderr(Stdio::inherit());\r\n        // On Windows a GUI-subsystem parent (the harness) spawning this console\r\n        // child pops a VISIBLE console window; CREATE_NO_WINDOW suppresses it\r\n        // without detaching (the raw `Command::new` bypassed ManifestRuntime, which\r\n        // sets this for its spawns — runtime/brainproc parity). No-op elsewhere.\r\n        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]\r\n        #[cfg(windows)]\r\n        {\r\n            use std::os::windows::process::CommandExt;\r\n            const CREATE_NO_WINDOW: u32 = 0x0800_0000;\r\n            command.creation_flags(CREATE_NO_WINDOW);\r\n        }\r\n        let mut child = command.spawn()?;\r\n        let pid = child.id().into();\r\n        let stdout = child\r\n            .stdout\r\n            .take()\r\n            .expect(\"stdout piped at spawn\");\r\n        let stdin = child.stdin.take().expect(\"stdin piped at spawn\");\r\n        let reader = thread::spawn(move || {\r\n            let buf = BufReader::new(stdout);\r\n            for line in buf.lines() {\r\n                let Ok(line) = line else { break }; // child stdout closed\r\n                let trimmed = line.trim();\r\n                if trimmed.is_empty() {\r\n                    continue;\r\n                }\r\n                match serde_json::from_str::<KeyCmd>(trimmed) {\r\n                    Ok(cmd) => {\r\n                        if cmds.send(cmd).is_err() {\r\n                            break; // broker dropped the receiver — stop reading\r\n                        }\r\n                    }\r\n                    Err(e) => {\r\n                        spt_proto::emit_line_err!(\"TRANSLATION_BIN_BAD_LINE: {e}: {trimmed}\");\r\n                    }\r\n                }\r\n            }\r\n        });\r\n        Ok(TranslationChild {\r\n            child: Mutex::new(Some(child)),\r\n            stdin: Mutex::new(Some(stdin)),\r\n            _reader: reader,\r\n            pid,\r\n        })\r\n    }\r\n\r\n    /// The child's OS pid (for tests / introspection / the no-zombie assertion).\r\n    pub fn pid(&self) -> Option<u32> {\r\n        self.pid\r\n    }\r\n\r\n    /// Write one [`ToBinary`] line to the child's stdin (newline-terminated,\r\n    /// flushed). A closed stdin (post-shutdown) is a no-op `Ok`.\r\n    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n    pub fn send(&self, msg: &ToBinary) -> io::Result<()> {\r\n        let line = serde_json::to_string(msg).map_err(io::Error::other)?;\r\n        let mut guard = self.stdin.lock().unwrap();\r\n        if let Some(w) = guard.as_mut() {\r\n            w.write_all(line.as_bytes())?;\r\n            w.write_all(b\"\\n\")?;\r\n            w.flush()?;\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then\r\n    /// exits on its own. Idempotent.\r\n    fn close_stdin(&self) {\r\n        let _ = self.stdin.lock().unwrap().take();\r\n    }\r\n\r\n    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child\r\n…\n}","startLine":223,"lineNumbers":[223,null,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,null,359]},"fileSize":22675,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\translation.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":507,"totalBytes":4229,"outputLines":100,"outputBytes":4229,"shownRange":{"start":227,"end":326},"nextOffset":327}}},"isError":false,"timestamp":1789394625372}}
{"type":"message","id":"9f82f3b2","parentId":"e032920b","timestamp":"2026-09-14T14:03:45.379Z","message":{"role":"toolResult","toolCallId":"call_BJrYRanAE15Zc0PTpxMoMBqe|fc_0a63d1e8a5b46c9c016aa7fec1368487d1abdc8fccd2d2f125","toolName":"read","content":[{"type":"text","text":"/// the \"never park forever\" bound applied to injection.\n/// Bring up a translation binary at `path` for `endpoint_id`: spawn the\n/// stdin→stdout filter, send the identity `Init`, start the inject worker that\n/// owns the atomic PTY apply, and return the `Arc<Translation>`. A spawn failure\n/// FAILS CLOSED to SPOOLING (`None`, logged): `None` now MEANS the inbound spools\n/// (idle delivery is translation-binary-only, never a raw PTY inject) — a missing\n/// binary costs idle delivery, never the operator their agent. `input` is the session's EXISTING PTY input writer\n/// (reused; only the translation child is created). Extracted verbatim from\n/// `dispatch_spawn` so a live-update restart (`dispatch_adapter_apply`, W3d) can\n/// rebuild a session's translation child from the NEW binary without disturbing\n/// the PTY / output log / session (brain-parity).\n// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\n// [impl->REQ-ADAPTER-LIVE-UPDATE]\nfn build_translation(\n    argv: &[String],\n    endpoint_id: &str,\n    input: &Arc<InputWriter>,\n    respawns: Arc<AtomicU32>,\n    log: &Arc<Mutex<OutputLog>>,\n) -> Option<Arc<Translation>> {\n    let (cmd_tx, cmd_rx) = channel::<KeyCmd>();\n    match TranslationChild::spawn(argv, cmd_tx) {\n        Ok(child) => {\n            let child = Arc::new(child);\n            // First line: identity (content-free of any PTY bytes).\n            let _ = child.send(&ToBinary::Init {\n                endpoint_id: endpoint_id.to_string(),\n                node: spt_store::hostlabel::os_hostname().unwrap_or_default(),\n            });\n            let floor = Arc::new(Mutex::new(InjectFloor::default()));\n            let faulted = Arc::new(AtomicBool::new(false));\n            let (event_tx, event_rx) = channel::<String>();\n            let w_input = Arc::clone(input);\n            let w_child = Arc::clone(&child);\n            let w_floor = Arc::clone(&floor);\n            let w_faulted = Arc::clone(&faulted);\n            let w_respawns = Arc::clone(&respawns);\n            let w_log = Arc::clone(log);\n            let w_endpoint = endpoint_id.to_string();\n            // B6 (REQ-HAZARD-INJECT-WORKER-POISON): run the worker under a panic\n            // guard. A panic inside the worker (a residual unwrap, a poisoned lock the\n            // recovery missed, anything) must NOT silently kill the thread and orphan\n            // the translation child — that drops `event_rx` WITHOUT reaping the binary,\n            // so a live pid + failed `event_tx.send` reports \"worker-gone\" forever. On\n            // a panic, FAULT (terminate the child + set faulted) — the same terminal\n            // state the worker's own fault paths reach — so idle inbound cleanly spools.\n            let sup_child = Arc::clone(&child);\n            let sup_faulted = Arc::clone(&faulted);\n            let sup_endpoint = endpoint_id.to_string();\n            thread::spawn(move || {\n                run_inject_worker_supervised(\n                    sup_child,\n                    sup_faulted,\n                    sup_endpoint,\n                    std::panic::AssertUnwindSafe(move || {\n                        run_inject_worker(\n                            w_input, w_child, w_floor, event_rx, cmd_rx, w_faulted, w_respawns,\n                            w_log, w_endpoint,\n                        );\n                    }),\n                );\n            });\n            Some(Arc::new(Translation {\n                floor,\n                event_tx,\n                child,\n                faulted,\n            }))\n        }\n        Err(e) => {\n            spt_proto::emit_line_err!(\n                \"TRANSLATION_SPAWN_FAILED:{argv:?}: {e} — no idle PTY delivery; inbound spools (declare/repair the binary)\"\n            );\n            None\n        }\n    }\n}\n\n/// The resident translation binary path declared by the manifest at\n/// `<install_dir>/manifest.toml` (`[message-idle-translation-binary].path`), or\n/// `None` if there is no such manifest / declaration. Read fresh from disk so a\n/// live-update restart picks up a MOVED/renamed binary (verdict flag-5: never\n/// reuse the stale cached `SpawnReq.translation_binary`). (W3d)\n// [impl->REQ-ADAPTER-LIVE-UPDATE]\n// [impl->REQ-TRANSLATE-COMMAND]\nfn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n    let decl = manifest.message_idle_translation_binary.as_ref()?;\n    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n    // deprecated `path` to a spawnable argv — adapter-static keys substituted, the\n    // program token resolved against the install dir (same as the initial spawn in\n    // harnesshost) so the W3d live-update RESPAWN runs the real binary, not a\n    // verbatim path Command::new fails to find (which would spool idle inbound).\n    spt_runtime::runtime::resolve_translation_argv(\n        decl,\n        Some(install_dir),\n        Some(manifest.adapter.name.as_str()),\n    )\n}\n\n/// Run the inject worker under a panic guard (B6, REQ-HAZARD-INJECT-WORKER-POISON):\n/// `body` is the [`run_inject_worker`] call. If it PANICS, the guard FAULTs the\n/// translation binary — `fault_translation` terminates the child (no orphaned live\n/// pid) and sets `faulted` (idle inbound then spools, never a silent \"worker-gone\"\n/// force-native refusal against a still-running binary). Without this, a worker\n/// panic drops `event_rx` while the child keeps running: every later inject sees\n/// `event_tx.send` fail and reports delivered=false forever. `child` + `faulted` are\n/// the guard's OWN clones (the `body` moves its own into `run_inject_worker`).\n// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\n\n[Showing lines 4689-4798 of 16852. Use :4799 to continue]"}],"details":{"truncation":{"content":"/// the \"never park forever\" bound applied to injection.\r\n/// Bring up a translation binary at `path` for `endpoint_id`: spawn the\r\n/// stdin→stdout filter, send the identity `Init`, start the inject worker that\r\n/// owns the atomic PTY apply, and return the `Arc<Translation>`. A spawn failure\r\n/// FAILS CLOSED to SPOOLING (`None`, logged): `None` now MEANS the inbound spools\r\n/// (idle delivery is translation-binary-only, never a raw PTY inject) — a missing\r\n/// binary costs idle delivery, never the operator their agent. `input` is the session's EXISTING PTY input writer\r\n/// (reused; only the translation child is created). Extracted verbatim from\r\n/// `dispatch_spawn` so a live-update restart (`dispatch_adapter_apply`, W3d) can\r\n/// rebuild a session's translation child from the NEW binary without disturbing\r\n/// the PTY / output log / session (brain-parity).\r\n// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n// [impl->REQ-ADAPTER-LIVE-UPDATE]\r\nfn build_translation(\r\n    argv: &[String],\r\n    endpoint_id: &str,\r\n    input: &Arc<InputWriter>,\r\n    respawns: Arc<AtomicU32>,\r\n    log: &Arc<Mutex<OutputLog>>,\r\n) -> Option<Arc<Translation>> {\r\n    let (cmd_tx, cmd_rx) = channel::<KeyCmd>();\r\n    match TranslationChild::spawn(argv, cmd_tx) {\r\n        Ok(child) => {\r\n            let child = Arc::new(child);\r\n            // First line: identity (content-free of any PTY bytes).\r\n            let _ = child.send(&ToBinary::Init {\r\n                endpoint_id: endpoint_id.to_string(),\r\n                node: spt_store::hostlabel::os_hostname().unwrap_or_default(),\r\n            });\r\n            let floor = Arc::new(Mutex::new(InjectFloor::default()));\r\n            let faulted = Arc::new(AtomicBool::new(false));\r\n            let (event_tx, event_rx) = channel::<String>();\r\n            let w_input = Arc::clone(input);\r\n            let w_child = Arc::clone(&child);\r\n            let w_floor = Arc::clone(&floor);\r\n            let w_faulted = Arc::clone(&faulted);\r\n            let w_respawns = Arc::clone(&respawns);\r\n            let w_log = Arc::clone(log);\r\n            let w_endpoint = endpoint_id.to_string();\r\n            // B6 (REQ-HAZARD-INJECT-WORKER-POISON): run the worker under a panic\r\n            // guard. A panic inside the worker (a residual unwrap, a poisoned lock the\r\n            // recovery missed, anything) must NOT silently kill the thread and orphan\r\n            // the translation child — that drops `event_rx` WITHOUT reaping the binary,\r\n            // so a live pid + failed `event_tx.send` reports \"worker-gone\" forever. On\r\n            // a panic, FAULT (terminate the child + set faulted) — the same terminal\r\n            // state the worker's own fault paths reach — so idle inbound cleanly spools.\r\n            let sup_child = Arc::clone(&child);\r\n            let sup_faulted = Arc::clone(&faulted);\r\n            let sup_endpoint = endpoint_id.to_string();\r\n            thread::spawn(move || {\r\n                run_inject_worker_supervised(\r\n                    sup_child,\r\n                    sup_faulted,\r\n                    sup_endpoint,\r\n                    std::panic::AssertUnwindSafe(move || {\r\n                        run_inject_worker(\r\n                            w_input, w_child, w_floor, event_rx, cmd_rx, w_faulted, w_respawns,\r\n                            w_log, w_endpoint,\r\n                        );\r\n                    }),\r\n                );\r\n            });\r\n            Some(Arc::new(Translation {\r\n                floor,\r\n                event_tx,\r\n                child,\r\n                faulted,\r\n            }))\r\n        }\r\n        Err(e) => {\r\n            spt_proto::emit_line_err!(\r\n                \"TRANSLATION_SPAWN_FAILED:{argv:?}: {e} — no idle PTY delivery; inbound spools (declare/repair the binary)\"\r\n            );\r\n            None\r\n        }\r\n    }\r\n}\r\n\r\n/// The resident translation binary path declared by the manifest at\r\n/// `<install_dir>/manifest.toml` (`[message-idle-translation-binary].path`), or\r\n/// `None` if there is no such manifest / declaration. Read fresh from disk so a\r\n/// live-update restart picks up a MOVED/renamed binary (verdict flag-5: never\r\n/// reuse the stale cached `SpawnReq.translation_binary`). (W3d)\r\n// [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n// [impl->REQ-TRANSLATE-COMMAND]\r\nfn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\r\n    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\r\n    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\r\n    let decl = manifest.message_idle_translation_binary.as_ref()?;\r\n    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\r\n    // deprecated `path` to a spawnable argv — adapter-static keys substituted, the\r\n    // program token resolved against the install dir (same as the initial spawn in\r\n    // harnesshost) so the W3d live-update RESPAWN runs the real binary, not a\r\n    // verbatim path Command::new fails to find (which would spool idle inbound).\r\n    spt_runtime::runtime::resolve_translation_argv(\r\n        decl,\r\n        Some(install_dir),\r\n        Some(manifest.adapter.name.as_str()),\r\n    )\r\n}\r\n\r\n/// Run the inject worker under a panic guard (B6, REQ-HAZARD-INJECT-WORKER-POISON):\r\n/// `body` is the [`run_inject_worker`] call. If it PANICS, the guard FAULTs the\r\n/// translation binary — `fault_translation` terminates the child (no orphaned live\r\n/// pid) and sets `faulted` (idle inbound then spools, never a silent \"worker-gone\"\r\n/// force-native refusal against a still-running binary). Without this, a worker\r\n/// panic drops `event_rx` while the child keeps running: every later inject sees\r\n/// `event_tx.send` fail and reports delivered=false forever. `child` + `faulted` are\r\n/// the guard's OWN clones (the `body` moves its own into `run_inject_worker`).\r\n// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\r","truncated":true,"truncatedBy":"lines","totalLines":12164,"totalBytes":5888,"outputLines":110,"outputBytes":5888,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":16852,"displayContent":{"text":"/// the \"never park forever\" bound applied to injection.\r\n/// Bring up a translation binary at `path` for `endpoint_id`: spawn the\r\n/// stdin→stdout filter, send the identity `Init`, start the inject worker that\r\n/// owns the atomic PTY apply, and return the `Arc<Translation>`. A spawn failure\r\n/// FAILS CLOSED to SPOOLING (`None`, logged): `None` now MEANS the inbound spools\r\n/// (idle delivery is translation-binary-only, never a raw PTY inject) — a missing\r\n/// binary costs idle delivery, never the operator their agent. `input` is the session's EXISTING PTY input writer\r\n/// (reused; only the translation child is created). Extracted verbatim from\r\n/// `dispatch_spawn` so a live-update restart (`dispatch_adapter_apply`, W3d) can\r\n/// rebuild a session's translation child from the NEW binary without disturbing\r\n/// the PTY / output log / session (brain-parity).\r\n// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n// [impl->REQ-ADAPTER-LIVE-UPDATE]\r\nfn build_translation(\r\n    argv: &[String],\r\n    endpoint_id: &str,\r\n    input: &Arc<InputWriter>,\r\n    respawns: Arc<AtomicU32>,\r\n    log: &Arc<Mutex<OutputLog>>,\r\n) -> Option<Arc<Translation>> {\r\n    let (cmd_tx, cmd_rx) = channel::<KeyCmd>();\r\n    match TranslationChild::spawn(argv, cmd_tx) {\r\n        Ok(child) => {\r\n            let child = Arc::new(child);\r\n            // First line: identity (content-free of any PTY bytes).\r\n            let _ = child.send(&ToBinary::Init {\r\n                endpoint_id: endpoint_id.to_string(),\r\n                node: spt_store::hostlabel::os_hostname().unwrap_or_default(),\r\n            });\r\n            let floor = Arc::new(Mutex::new(InjectFloor::default()));\r\n            let faulted = Arc::new(AtomicBool::new(false));\r\n            let (event_tx, event_rx) = channel::<String>();\r\n            let w_input = Arc::clone(input);\r\n            let w_child = Arc::clone(&child);\r\n            let w_floor = Arc::clone(&floor);\r\n            let w_faulted = Arc::clone(&faulted);\r\n            let w_respawns = Arc::clone(&respawns);\r\n            let w_log = Arc::clone(log);\r\n            let w_endpoint = endpoint_id.to_string();\r\n            // B6 (REQ-HAZARD-INJECT-WORKER-POISON): run the worker under a panic\r\n            // guard. A panic inside the worker (a residual unwrap, a poisoned lock the\r\n            // recovery missed, anything) must NOT silently kill the thread and orphan\r\n            // the translation child — that drops `event_rx` WITHOUT reaping the binary,\r\n            // so a live pid + failed `event_tx.send` reports \"worker-gone\" forever. On\r\n            // a panic, FAULT (terminate the child + set faulted) — the same terminal\r\n            // state the worker's own fault paths reach — so idle inbound cleanly spools.\r\n            let sup_child = Arc::clone(&child);\r\n            let sup_faulted = Arc::clone(&faulted);\r\n            let sup_endpoint = endpoint_id.to_string();\r\n            thread::spawn(move || {\r\n                run_inject_worker_supervised(\r\n                    sup_child,\r\n                    sup_faulted,\r\n                    sup_endpoint,\r\n                    std::panic::AssertUnwindSafe(move || {\r\n                        run_inject_worker(\r\n                            w_input, w_child, w_floor, event_rx, cmd_rx, w_faulted, w_respawns,\r\n                            w_log, w_endpoint,\r\n                        );\r\n                    }),\r\n                );\r\n            });\r\n            Some(Arc::new(Translation {\r\n                floor,\r\n                event_tx,\r\n                child,\r\n                faulted,\r\n            }))\r\n        }\r\n        Err(e) => {\r\n            spt_proto::emit_line_err!(\r\n                \"TRANSLATION_SPAWN_FAILED:{argv:?}: {e} — no idle PTY delivery; inbound spools (declare/repair the binary)\"\r\n            );\r\n            None\r\n        }\r\n    }\r\n}\r\n\r\n/// The resident translation binary path declared by the manifest at\r\n/// `<install_dir>/manifest.toml` (`[message-idle-translation-binary].path`), or\r\n/// `None` if there is no such manifest / declaration. Read fresh from disk so a\r\n/// live-update restart picks up a MOVED/renamed binary (verdict flag-5: never\r\n/// reuse the stale cached `SpawnReq.translation_binary`). (W3d)\r\n// [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n// [impl->REQ-TRANSLATE-COMMAND]\r\nfn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\r\n    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\r\n    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\r\n    let decl = manifest.message_idle_translation_binary.as_ref()?;\r\n    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\r\n    // deprecated `path` to a spawnable argv — adapter-static keys substituted, the\r\n    // program token resolved against the install dir (same as the initial spawn in\r\n    // harnesshost) so the W3d live-update RESPAWN runs the real binary, not a\r\n    // verbatim path Command::new fails to find (which would spool idle inbound).\r\n    spt_runtime::runtime::resolve_translation_argv(\r\n        decl,\r\n        Some(install_dir),\r\n        Some(manifest.adapter.name.as_str()),\r\n    )\r\n}\r\n\r\n/// Run the inject worker under a panic guard (B6, REQ-HAZARD-INJECT-WORKER-POISON):\r\n/// `body` is the [`run_inject_worker`] call. If it PANICS, the guard FAULTs the\r\n/// translation binary — `fault_translation` terminates the child (no orphaned live\r\n/// pid) and sets `faulted` (idle inbound then spools, never a silent \"worker-gone\"\r\n/// force-native refusal against a still-running binary). Without this, a worker\r\n/// panic drops `event_rx` while the child keeps running: every later inject sees\r\n/// `event_tx.send` fail and reports delivered=false forever. `child` + `faulted` are\r\n/// the guard's OWN clones (the `body` moves its own into `run_inject_worker`).\r\n// [impl->REQ-HAZARD-INJECT-WORKER-POISON]\r","startLine":4689,"lineNumbers":[4689,4690,4691,4692,4693,4694,4695,4696,4697,4698,4699,4700,4701,4702,4703,4704,4705,4706,4707,4708,4709,4710,4711,4712,4713,4714,4715,4716,4717,4718,4719,4720,4721,4722,4723,4724,4725,4726,4727,4728,4729,4730,4731,4732,4733,4734,4735,4736,4737,4738,4739,4740,4741,4742,4743,4744,4745,4746,4747,4748,4749,4750,4751,4752,4753,4754,4755,4756,4757,4758,4759,4760,4761,4762,4763,4764,4765,4766,4767,4768,4769,4770,4771,4772,4773,4774,4775,4776,4777,4778,4779,4780,4781,4782,4783,4784,4785,4786,4787,4788,4789,4790,4791,4792,4793,4794,4795,4796,4797,4798]},"fileSize":877537,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":16852,"totalBytes":5888,"outputLines":110,"outputBytes":5888,"shownRange":{"start":4689,"end":4798},"nextOffset":4799}}},"isError":false,"timestamp":1789394625378}}
{"type":"message","id":"db934101","parentId":"9f82f3b2","timestamp":"2026-09-14T14:03:45.450Z","message":{"role":"toolResult","toolCallId":"call_Utd4gzd3nv2mqoCbkxGswORd|fc_0a63d1e8a5b46c9c016aa7fec1369887d193a4987835cd3160","toolName":"grep","content":[{"type":"text","text":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## broker.rs\n 4047|                    \"writer-exit\",\n*4048|                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 4049|                );\n 4050|                return;\n 4051|            }\n...\n 4090|                    \"writer-exit\",\n*4091|                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n 4092|                );\n 4093|                return;\n 4094|            }\n...\n 7592|        spt_proto::emit_line_err!(\n*7593|            \"SEAL_CEREMONY_MINTED: ceremony={ceremony_id} minter={minter} token={} kind=fido2\",\n 7594|            record.token\n 7595|        );\n 7596|        (\n...\n 7737|            \"SEAL_CEREMONY_MINTED: ceremony={ceremony_id} minter={minter} token={} \\\n*7738|             kind=totp enrolled_e=1\",\n 7739|            record.token\n 7740|        );\n 7741|        (\n...\n 10535|                let frame = read_frame(&mut wire).unwrap();\n*10536|                if frame.kind == crate::msg::KIND_USER_INPUT_PATHS {\n 10537|                    break serde_json::from_value(frame.payload).unwrap();\n 10538|                }\n 10539|            };\n...\n 13013|        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\n*13014|        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\n 13015|        let ev: crate::msg::OutputEvent =\n 13016|            serde_json::from_value(frame.payload).expect(\"output payload\");\n 13017|        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\n...\n 13524|        let dir = tempfile::tempdir().unwrap();\n*13525|        let manifest = \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 13526|                        min_spt_core_version=\\\"1\\\"\\n\\\n 13527|                        [message-idle-translation-binary]\\npath = \\\"some/bin\\\"\\n\";\n 13528|        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n...\n 13537|        let dir2 = tempfile::tempdir().unwrap();\n*13538|        let manifest2 = \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 13539|                         min_spt_core_version=\\\"1\\\"\\n\\\n 13540|                         [message-idle-translation-binary]\\n\\\n 13541|                         command = \\\"{adapter_dir}/claude-spt translate\\\"\\n\";\n...\n 13562|        let dir = tempfile::tempdir().unwrap();\n*13563|        let manifest = \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 13564|                        min_spt_core_version=\\\"1\\\"\\n\";\n 13565|        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 13566|        assert_eq!(\n## conn.rs\n 74|/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n*75|/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n 76|/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n 77|/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n 78|// [impl->REQ-CONN-POISON-ATTRIBUTION]\n*79|static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n 80|\n 81|/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n 82|/// startup ([`init_log_anchor`], both broker and brain entries) so every\n...\n 197|        self.emit(format_args!(\n*198|            \"CONN_LIFECYCLE_SUMMARY: scope=broker-ipc {} interval_ms={} starts={starts} closes={closes} \\\n 199|             [REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\",\n 200|            log_stamp(), elapsed.as_millis()\n 201|        ));\n...\n 236|struct Inner {\n*237|    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 238|    id: u64,\n 239|    /// Accreted attribution facts (subscriber role, endpoint/session where\n 240|    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n...\n 331|    /// Render the once-per-conn retirement record — the F-039 token split\n*332|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 333|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 334|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 335|    /// already-dead counterpart, the offline-peer dial churn) emits the\n*336|    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n 337|    /// SAME attribution fields — quieter class, never thinner (the retired\n 338|    /// class is what discriminates fresh-carrier churn from interleave\n 339|    /// artifact in the field). Behavior is identical either way: poison,\n...\n 344|            format!(\n*345|                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 346|                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 347|                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 348|                self.attribution(),\n...\n 356|            format!(\n*357|                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n 358|                 broker conn poisoned + retired, never reused (same retire behavior \\\n 359|                 as POISONED — only the failure class differs) \\\n 360|                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n...\n 373|        self.lifecycle_log.emit(format_args!(\n*374|            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n 375|            self.attribution()\n 376|        ));\n 377|    }\n...\n 388|            // First poison of this conn: loud, once. The token is class-split\n*389|            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n*390|            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 391|            self.lifecycle_log.emit(format_args!(\n 392|                \"{}\",\n 393|                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n...\n 534|        let inner = Arc::new(Inner {\n*535|            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n 536|            label: Mutex::new(String::new()),\n 537|            first_write: OnceLock::new(),\n 538|            lifecycle_log,\n...\n 887|        }\n*888|        assert!(lines[1].contains(\"CONN_WRITE_RETIRED:\"));\n 889|        assert!(lines[1].contains(\"BrokenPipe\"));\n 890|        assert!(lines[2].contains(\"poisoned=true timed_out=false\"));\n 891|    }\n...\n 893|    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*894|    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n*895|    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 896|    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 897|    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n 898|    // monotonic stamps. Quieter class, never thinner.\n...\n 907|        assert!(\n*908|            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n 909|            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 910|        );\n 911|        let retired = conn\n...\n 914|        assert!(\n*915|            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n 916|            \"organic class emits the DISTINCT retired token: {retired}\"\n 917|        );\n 918|        assert!("}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs, .worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":26,"fileCount":2,"files":[".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs",".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","count":9},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs","count":17}],"truncated":false,"displayContent":"# .worktrees/297-enforcement-shape/crates/spt-daemon/src/\n## broker.rs\n  4047│                    \"writer-exit\",\n *4048│                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n  4049│                );\n  4050│                return;\n  4051│            }\n      │...\n  4090│                    \"writer-exit\",\n *4091│                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n  4092│                );\n  4093│                return;\n  4094│            }\n      │...\n  7592│        spt_proto::emit_line_err!(\n *7593│            \"SEAL_CEREMONY_MINTED: ceremony={ceremony_id} minter={minter} token={} kind=fido2\",\n  7594│            record.token\n  7595│        );\n  7596│        (\n      │...\n  7737│            \"SEAL_CEREMONY_MINTED: ceremony={ceremony_id} minter={minter} token={} \\\n *7738│             kind=totp enrolled_e=1\",\n  7739│            record.token\n  7740│        );\n  7741│        (\n      │...\n 10535│                let frame = read_frame(&mut wire).unwrap();\n*10536│                if frame.kind == crate::msg::KIND_USER_INPUT_PATHS {\n 10537│                    break serde_json::from_value(frame.payload).unwrap();\n 10538│                }\n 10539│            };\n      │...\n 13013│        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\n*13014│        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\n 13015│        let ev: crate::msg::OutputEvent =\n 13016│            serde_json::from_value(frame.payload).expect(\"output payload\");\n 13017│        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\n      │...\n 13524│        let dir = tempfile::tempdir().unwrap();\n*13525│        let manifest = \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 13526│                        min_spt_core_version=\\\"1\\\"\\n\\\n 13527│                        [message-idle-translation-binary]\\npath = \\\"some/bin\\\"\\n\";\n 13528│        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n      │...\n 13537│        let dir2 = tempfile::tempdir().unwrap();\n*13538│        let manifest2 = \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 13539│                         min_spt_core_version=\\\"1\\\"\\n\\\n 13540│                         [message-idle-translation-binary]\\n\\\n 13541│                         command = \\\"{adapter_dir}/claude-spt translate\\\"\\n\";\n      │...\n 13562│        let dir = tempfile::tempdir().unwrap();\n*13563│        let manifest = \"[adapter]\\nname=\\\"cc\\\"\\nkind=\\\"harness\\\"\\nversion=\\\"1\\\"\\n\\\n 13564│                        min_spt_core_version=\\\"1\\\"\\n\";\n 13565│        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 13566│        assert_eq!(\n## conn.rs\n  74│/// [`BrokerConn::new`]. Before this the only conn identity was `Arc::ptr_eq`,\n *75│/// which does not survive a log line: per-line CONN_WRITE_POISONED churn was\n  76│/// UNDECIDABLE between fresh-carrier churn and stderr interleave (the F-039\n  77│/// RCA's terminal fork). Every lifecycle record carries `conn=<id>`.\n  78│// [impl->REQ-CONN-POISON-ATTRIBUTION]\n *79│static CONN_ID_SEQ: AtomicU64 = AtomicU64::new(0);\n  80│\n  81│/// The shared monotonic anchor for `mono_ms` stamps. Set explicitly at daemon\n  82│/// startup ([`init_log_anchor`], both broker and brain entries) so every\n    │...\n 197│        self.emit(format_args!(\n*198│            \"CONN_LIFECYCLE_SUMMARY: scope=broker-ipc {} interval_ms={} starts={starts} closes={closes} \\\n 199│             [REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED]\",\n 200│            log_stamp(), elapsed.as_millis()\n 201│        ));\n    │...\n 236│struct Inner {\n*237│    /// Stable per-physical-conn id ([`CONN_ID_SEQ`]) — on every lifecycle record.\n 238│    id: u64,\n 239│    /// Accreted attribution facts (subscriber role, endpoint/session where\n 240│    /// known — [`BrokerConn::describe`]). Bounded by [`LABEL_CAP`].\n    │...\n 331│    /// Render the once-per-conn retirement record — the F-039 token split\n*332│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 333│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 334│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 335│    /// already-dead counterpart, the offline-peer dial churn) emits the\n*336│    /// distinct `CONN_WRITE_RETIRED` token carrying the error kind and the\n 337│    /// SAME attribution fields — quieter class, never thinner (the retired\n 338│    /// class is what discriminates fresh-carrier churn from interleave\n 339│    /// artifact in the field). Behavior is identical either way: poison,\n    │...\n 344│            format!(\n*345│                \"CONN_WRITE_POISONED: {} — physical broker conn retired: a write \\\n 346│                 exceeded its {}ms bound; in-flight I/O aborted, conn never reused \\\n 347│                 [REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\",\n 348│                self.attribution(),\n    │...\n 356│            format!(\n*357│                \"CONN_WRITE_RETIRED: {} — organic write failure ({cause}); physical \\\n 358│                 broker conn poisoned + retired, never reused (same retire behavior \\\n 359│                 as POISONED — only the failure class differs) \\\n 360│                 [REQ-CONN-POISON-DIAL-SCOPE]\",\n    │...\n 373│        self.lifecycle_log.emit(format_args!(\n*374│            \"CONN_LIFECYCLE: {} event={event}{sep}{extra} [REQ-CONN-POISON-ATTRIBUTION]\",\n 375│            self.attribution()\n 376│        ));\n 377│    }\n    │...\n 388│            // First poison of this conn: loud, once. The token is class-split\n*389│            // (F-039 leg a): deadline → CONN_WRITE_POISONED (the wedge\n*390│            // observable), organic fast-fail → CONN_WRITE_RETIRED.\n 391│            self.lifecycle_log.emit(format_args!(\n 392│                \"{}\",\n 393│                self.render_retirement(self.timed_out.load(Ordering::Acquire), cause)\n    │...\n 534│        let inner = Arc::new(Inner {\n*535│            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n 536│            label: Mutex::new(String::new()),\n 537│            first_write: OnceLock::new(),\n 538│            lifecycle_log,\n    │...\n 887│        }\n*888│        assert!(lines[1].contains(\"CONN_WRITE_RETIRED:\"));\n 889│        assert!(lines[1].contains(\"BrokenPipe\"));\n 890│        assert!(lines[2].contains(\"poisoned=true timed_out=false\"));\n 891│    }\n    │...\n 893│    // [unit->REQ-CONN-POISON-DIAL-SCOPE] the F-039 leg-(a) token split: the loud\n*894│    // CONN_WRITE_POISONED token is RESERVED for the deadline (timed_out) class;\n*895│    // an organic write failure renders the distinct CONN_WRITE_RETIRED token\n 896│    // carrying the error kind — and (doyle amendment 1) the SAME attribution\n 897│    // fields as POISONED: conn id, accreted role/endpoint/session facts, wall +\n 898│    // monotonic stamps. Quieter class, never thinner.\n    │...\n 907│        assert!(\n*908│            poisoned.starts_with(\"CONN_WRITE_POISONED:\"),\n 909│            \"deadline class keeps the loud wedge-observable token: {poisoned}\"\n 910│        );\n 911│        let retired = conn\n    │...\n 914│        assert!(\n*915│            retired.starts_with(\"CONN_WRITE_RETIRED:\"),\n 916│            \"organic class emits the DISTINCT retired token: {retired}\"\n 917│        );\n 918│        assert!("},"isError":false,"timestamp":1789394625449}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3SOjkbCrn9HbAvwhVOS1An2v|fc_0a63d1e8a5b46c9c016aa7fecb4a1c87d19cab16d400484082","toolName":"grep","startedAt":"2026-09-14T14:03:55.356Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs"},"intent":"Locating hostname implementation"},"id":"d0269707","parentId":"db934101","timestamp":"2026-09-14T14:03:55.356Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CYFWsD1Zs9JLEjh3zD1gza8g|fc_0a63d1e8a5b46c9c016aa7fecb4a2887d1af1a5d76a1d259c9","toolName":"read","startedAt":"2026-09-14T14:03:55.356Z","args":{"path":".worktrees/297-enforcement-shape/CONTEXT.md:370-405"},"intent":"Reading control domain model"},"id":"4a6daf96","parentId":"d0269707","timestamp":"2026-09-14T14:03:55.356Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1fH650tRfB9aJNSZQUFgu1wp|fc_0a63d1e8a5b46c9c016aa7fecb4a3087d1ba7f651d2a580aeb","toolName":"read","startedAt":"2026-09-14T14:03:55.356Z","args":{"path":".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md:19-30"},"intent":"Reading process isolation decision"},"id":"906a7cf7","parentId":"4a6daf96","timestamp":"2026-09-14T14:03:55.356Z"}
{"type":"message","id":"69064cc3","parentId":"906a7cf7","timestamp":"2026-09-14T14:03:55.356Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reading host path**"},{"type":"toolCall","id":"call_3SOjkbCrn9HbAvwhVOS1An2v|fc_0a63d1e8a5b46c9c016aa7fecb4a1c87d19cab16d400484082","name":"grep","arguments":{"i":"Locating hostname implementation","pattern":"pub fn os_hostname|fn os_hostname","path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs","case":true,"gitignore":false,"skip":null},"intent":"Locating hostname implementation"},{"type":"toolCall","id":"call_CYFWsD1Zs9JLEjh3zD1gza8g|fc_0a63d1e8a5b46c9c016aa7fecb4a2887d1af1a5d76a1d259c9","name":"read","arguments":{"i":"Reading control domain model","path":".worktrees/297-enforcement-shape/CONTEXT.md:370-405"},"intent":"Reading control domain model"},{"type":"toolCall","id":"call_1fH650tRfB9aJNSZQUFgu1wp|fc_0a63d1e8a5b46c9c016aa7fecb4a3087d1ba7f651d2a580aeb","name":"read","arguments":{"i":"Reading process isolation decision","path":".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md:19-30"},"intent":"Reading process isolation decision"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":4777,"output":184,"cacheRead":65536,"cacheWrite":0,"totalTokens":70497,"reasoningTokens":15,"cost":{"input":0.04777000000000001,"output":0.0092,"cacheRead":0.065536,"cacheWrite":0,"total":0.122506}},"stopReason":"toolUse","timestamp":1789394625527,"responseId":"resp_0a63d1e8a5b46c9c016aa7fec1e67087d1b1c7f6c7510480b2","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7fec3b2c487d1beeba8a4f3e6e8c2","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_7LyJ9SMFCePWKsetSM1Sjt1s8auC0YhcBKTtODMmeaVI9cpiX3UBQxftReST_nf-xrflUFkGhKKkUIHWjVFSac_WCgQo5RTjEGJezGNPn-M2X-0odeQvTm6jvxKrHfT5D_WhWE6MfD89eHLeKzBpwOBe9qq5DQBEw-GZ2TkNX2W51Xuk_slCVHDtE8dy7kMrvFNWq3cv0a2BW2k_OBXXGts_DLq1jzapeQaB_AEzAhzfmCZj8tA9_3Go6OpMJkQZztGljmxbNBvSqzYWosrC-eZygQSMK13Fl-BN3Va3O8h1kAbamGIFL68SdsD5qDRGhyNAI_77j9BWwknGoYAHb_nOky0neRm6tXUs-r7GvfvP7pU7a7nTbLFOtTMqsP_0I3L7JMHBQaMaP2xS5YHaOgZ4tgldP5u35I9_HB3cbcL7F7Lv0o1FcQ3ET3yFP-TzwB8hmRyWa40_q78gFyT2MdWjnbgUEGuTsbR-rku8cSf0y63r5T-A8FWBHueCIN26q00NICm6FqmIXnZGc9o8Nb1FcPAeYM5fCTLUuN-0FWszyX1t57nkgT-X6jo4ifcKchKCnN4pnsnR9nxTuligZw24-hj4uy3LdVhoPvxC_5Di2U4YCNbesE5scz7lxyqv5mrrPs2KhxNndCh5W7LOXy7B_KV30Yo0n7juACb8Q9kM_inPVQom9agcdKdjzpCSAm2vk-NFo4xqycxouSV6GsYdb84cTwybINGU1W6akp43zGZgSbER_a8Fxn8fQBKhRXFrI6TWHN1j6kZZAqn9t2ODtgDyo_eSxUbfxAmoS4-W3gtV_-66QZo4PHNjQsLyKaxlcfbSeQqI2-Pr0u3r5U8JmMcNkyedNotm6bv4X58n9EZkijj2ZYdD_ryDmwe1vFKUXWdjV3oAMRJhw0dR0bqzs3RYQ1O1z68O4siriY1Ids19A2JPFJVWHlbkRmUHbJq5sg1NQhdcCOKh7M7U6kO5i4_UtbLCLy9o8fGeWrjyglF9RhSyDH2xKch7kcoGLA-s4ZORQ2Iz87c07bf4I2tY6qXooXjwRBAPNnxqKptC61lEi_nHO6OWZ5w0gSJ_O-SWXg8EFt8_TGcOO0ZqTYa5GK2jgK_6NCsJ4IkYgjcfUL51O09SDAVFClV3NPfHOQdzEZPQI5j0i0xpkA5NwSmwnbmR2MVqOV22whL3QOpRf6mcTyREPiPVg6Upuk70j3VPu1cHBnuKVrwHsVvj5tyWPVAE6sfm75qVj-J1P6eZhM4BEavhTlUV85o-e4hkt_xbN9JWpUCWBsx-ndDG_On_0p_f1GB89bGy-48HMUI_VN1FGLicp7zQ25eE6uc6ImYzip0me_5jImw3eStXkxhw==","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"summary":[{"type":"summary_text","text":"**Reading host path**"}],"metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fecb4a1c87d19cab16d400484082","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating hostname implementation\",\"pattern\":\"pub fn os_hostname|fn os_hostname\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_3SOjkbCrn9HbAvwhVOS1An2v","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"grep","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fecb4a2887d1af1a5d76a1d259c9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading control domain model\",\"path\":\".worktrees/297-enforcement-shape/CONTEXT.md:370-405\"}","call_id":"call_CYFWsD1Zs9JLEjh3zD1gza8g","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"read","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fecb4a3087d1ba7f651d2a580aeb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading process isolation decision\",\"path\":\".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md:19-30\"}","call_id":"call_1fH650tRfB9aJNSZQUFgu1wp","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"read","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}}]},"duration":9827.068000003695,"ttft":2197.8968000113964,"completedAt":1789394635355,"contextSnapshot":{"promptTokens":70313,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"f66ca372","parentId":"69064cc3","timestamp":"2026-09-14T14:03:55.360Z","message":{"role":"toolResult","toolCallId":"call_3SOjkbCrn9HbAvwhVOS1An2v|fc_0a63d1e8a5b46c9c016aa7fecb4a1c87d19cab16d400484082","toolName":"grep","content":[{"type":"text","text":" 23|/// prefix, never a lie.\n*24|pub fn os_hostname() -> Option<String> {\n 25|    let name = detect()?;\n 26|    let name = name.trim();\n 27|    (!name.is_empty()).then(|| name.to_string())"}],"details":{"scopePath":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-store\\src\\hostlabel.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":1,"fileCount":1,"files":[".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs"],"fileMatches":[{"path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs","count":1}],"truncated":false,"displayContent":" 23│/// prefix, never a lie.\n*24│pub fn os_hostname() -> Option<String> {\n 25│    let name = detect()?;\n 26│    let name = name.trim();\n 27│    (!name.is_empty()).then(|| name.to_string())"},"isError":false,"timestamp":1789394635360}}
{"type":"message","id":"a6fed639","parentId":"f66ca372","timestamp":"2026-09-14T14:03:55.365Z","message":{"role":"toolResult","toolCallId":"call_1fH650tRfB9aJNSZQUFgu1wp|fc_0a63d1e8a5b46c9c016aa7fecb4a3087d1ba7f651d2a580aeb","toolName":"read","content":[{"type":"text","text":"## Context\n…\n- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n…\n- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.\n\n## Decision\n\n**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\n\n1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\n2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical ex…\n3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`** Only phase-significant loops convert — the idempotent pump cadences need none.\n4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update is new-brain × old-broker, so a CI-real old-broker × new-brain compat test across the verb surface is required (else KH-2.3 returns).\n5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\n6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable), or auto-rollback silently breaks on the first schema migration.\n7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\n8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\n\n## Consequences\n\n…\n- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred \"future polish\" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).\n\n[Showing lines 18-33 of 37. Use :34 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"\r\n## Decision\r\n\r\n**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\r\n\r\n1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\r\n2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical ex…\n3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`** Only phase-significant loops convert — the idempotent pump cadences need none.\r\n4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update is new-brain × old-broker, so a CI-real old-broker × new-brain compat test across the verb surface is required (else KH-2.3 returns).\r\n5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\r\n6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable), or auto-rollback silently breaks on the first schema migration.\r\n7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\r\n8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\r\n\r\n## Consequences\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":20,"totalBytes":5897,"outputLines":16,"outputBytes":5897,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":37,"displayContent":{"text":"## Context\n…\n- The brain cannot restart onto a new binary without killing the in-process broker thread, which would close every PTY, orphan every harness child, and drop every listening socket. **The no-endpoint-drop self-update pillar is therefore silently unrealized.**\n…\n- **REQ-DAEMON-2 and REQ-UPD-3 carry `int` evidence that proves only the in-process handoff shape** (`tests/update.rs`, `brain_swap.rs`, the M3b-B9 daemon E2E) — i.e. the regression is masked in the requirement registry: the tests pass while proving the wrong thing.\n\r\n## Decision\r\n\r\n**Restore the ADR-0004 two-process model.** The brain runs new code by restarting onto the swapped binary while the broker process — holding every continuity-bearing resource — survives. The design decisions (grill Q2–Q8, hardened by six verification amendments `[V1]`–`[V6]`):\r\n\r\n1. **Supervision (Q2).** The broker is the always-up per-machine anchor (one per `SPT_HOME`, present even with zero endpoints). It owns the seed-control lock + liveness (`ensure_running`/`is_running` target) and supervises/spawns the brain as its child.\r\n2. **Update trigger (Q3).** `apply` swaps the binary on disk, then signals the brain to snapshot + self-exit; the broker auto-respawns from the executable path (now the new binary); the new brain re-attaches. Reuses the existing snapshot→drop→re-attach primitive; an update is a *planned* crash on the path the broker already recovers from. **`[AMENDED 2026-06-11, v0.4.2]`** \"the executable path\" silently assumed path-string semantics. The Linux apply renames the running `spt` → `spt.old-N`; a per-spawn `std::env::current_exe()` (`readlink(/proc/self/exe)`, inode-tracking) then **follows the rename to `.old-N`**, so the broker respawned the brain onto the OLD bytes while recording `applied` (caught live on kitsubito, v0.4.1 roll). Fix: capture the canonical ex…\n3. **Loop-timing continuity (Q4).** Durable absolute-deadline state on disk, not a handoff snapshot. Periodic loops persist `(anchor, interval)` once on a fresh/crash start and derive `next_fire` functionally (no per-fire writes); an update restart keeps deriving (phase preserved), a crash restart resets the anchor. One-shot deadlines persist at creation and survive crash + update. **`[V3]`** The one-shot *rule* is fixed here; its *machinery* is built with the alarm port (no untested dead code). **`[V4]`** Only phase-significant loops convert — the idempotent pump cadences need none.\r\n4. **IPC boundary (Q5).** ADR-0004 §B adjudicates: net bring-up → broker (already broker-owned via `broker.rs:175` `OnceLock` — near-free), digest hub → broker / parse → brain, seed-lock → broker; every brain→broker call becomes a versioned IPC verb (no shared `Arc`). **Exception:** shellwake watcher children stay brain-side, re-reconciled from disk on start (rare updates make the window tolerable). Broker enlargement accepted (ADR-0004:63 precedent). **`[V6]`** Steady state after any update is new-brain × old-broker, so a CI-real old-broker × new-brain compat test across the verb surface is required (else KH-2.3 returns).\r\n5. **Multi-session handoff (Q6).** The broker becomes cursor-of-record per session; the new brain re-attaches **all** sessions in resume mode. Output is at-least-once (matches the SPIKE-05 terminal-stream contract); input/effects stay exactly-once via the broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* retires (no brain→brain channel exists under the self-exit trigger).\r\n6. **Failure atomicity (Q7).** Bounded-retry → auto-rollback to the last-known-good binary, gated on a brain `ready` signal (reuse the `peerloop.rs:805` supervise-backoff + healthy-run). Quarantine the failed version; loud notif. The applied record becomes **two-phase** (`applied-pending` → `applied` on ready, or `rolled-back` on failure) — fixing today's optimistic `record_applied` before boot (`applyhost.rs:176`). **`[V1]`** Forward invariant minted: a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable), or auto-rollback silently breaks on the first schema migration.\r\n7. **Generation custody (Q2/`[V2]`).** Retiring `BrainState` would orphan the KH-2.4 generation counter; the **broker** owns it, increments on every spawn, and hands `{generation, start-reason}` to the brain at spawn via a versioned argv/hello field (KH-2.3 compat). The same channel carries Q4's update-vs-crash discriminator.\r\n8. **Cross-platform uniformity (Q8).** The broker spawns the brain as a child process (`Command::spawn`) + socket IPC — no `exec`, so no Windows/Unix divergence. ConPTY/forkpty handles never leave the broker.\r\n\r\n## Consequences\r\n\r\n…\n- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred \"future polish\" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).","startLine":7,"lineNumbers":[7,null,14,null,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,null,37]},"fileSize":9995,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\docs\\adr\\0018-broker-brain-process-isolation-restoration.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":37,"totalBytes":5897,"outputLines":16,"outputBytes":5897,"shownRange":{"start":18,"end":33},"nextOffset":34},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789394635365}}
{"type":"message","id":"4a6dea1c","parentId":"a6fed639","timestamp":"2026-09-14T14:03:55.440Z","message":{"role":"toolResult","toolCallId":"call_CYFWsD1Zs9JLEjh3zD1gza8g|fc_0a63d1e8a5b46c9c016aa7fecb4a2887d1af1a5d76a1d259c9","toolName":"read","content":[{"type":"text","text":"#### Shell model (detailed)\n…\n\n<!-- [doc->REQ-CONSENT-3] -->\n**per-capability approval gates** (ratified 2026-06-11, Gateway grill): the same `require_approval` enum may ride **individual capability entries** in a shell manifest — gating the dangerous *operation*, not just the spawn. Same grant store, same interactive escalation, same floor semantics. A capability may declare a **class key** so grants are scoped finer than the capability itself: the first consumer is the usbip shell's `attach`, granted per **(owner endpoint × device class × node)** — a remembered HID-attach grant never authorizes a storage-class attach. Spawn gates govern *existence*; capability gates govern *acts*.\n\n**instance cap (`max_instances_per_owner` + `over_cap`, manifest):** an optional ceiling on how many instances of this shell adapter **one owner endpoint** may hold. The count is **all existing instances** — online *and* offline, every non-torn-down perch (`shell teardown` frees a slot) — so offline persistent shells cannot be stockpiled to evade it. Unset ⇒ unlimited. At the cap, **`over_cap`** decides: `reject` (**default**) refuses the spawn outright; `approve` requires per-spawn approval for each instance beyond the cap (allow-once each — it does **not** permanently raise the cap; a bigger ceiling is a config change, not a grant). Serves the consent model's **runaway** concern, and **composes with `require_approval`**: that governs spawns *under* the cap…\n\n**shell instance aliasing:** every instance has an immutable canonical id **`<adapter>-<n>`** (`GameRobot-0`, `GameRobot-1`, …) — which itself encodes the providing adapter — plus an optional **alias**, a friendly owner-unique label (`TempleKeeper`) set at spawn (`shell spawn GameRobot --alias TempleKeeper`) or later (`shell rename <ref> <alias>`). Alias and canonical id are interchangeable for addressing (`shell cmd TempleKeeper …`). The alias is a *display/address* overlay only: `adapter_name` always rides the perch, so `shell list` still shows the underlying type (`TempleKeeper → GameRobot, online`) — aliasing never obscures what a shell is.\n\n**Agent awareness.** Available shells *and* the agent's own shell instances (with online/offline state) are injected into the agent's context (SessionStart additionalContext / `$LIVE start`-equiv + hidden-context versions) — the agent ideally just \"knows.\" A `shell list` command also exists.\n\n**Discovery scope (what \"available shells\" resolves to):** the set surfaced to an agent (injected + `shell list`) is **(1)** its **own instances** — alias · canonical `<adapter>-<n>` · status — always; plus **(2) instantiable adapters**: shell adapters **registered on the agent's own node** with `broadcast ∈ {subnet, same-node}`, and shell adapters on *other* subnet nodes with `broadcast = subnet`. `broadcast = none` shells are **never** surfaced proactively — spawnable only by explicit user/agent instruction (if registered locally / reachable). Discovering an *other-node* shell does **not** imply free instantiation: spawning it rides the deferred **instantiate-anywhere** gate. (Discovery is gated by the same reach gates as everything else — an agent only se…\n\n**Cross-node instance resolution** (ruled 2026-07-24, #70 grill — governs the cross-node drive leg): an unqualified shell ref resolves **local-first** — if the owner holds a matching instance on the *current* node, that one is used, even when same-ref instances exist elsewhere. Resolution **refuses as ambiguous only** when the ref matches instances on more than one node **and none is local**; the qualified form (`ref@node`) is the escape hatch and always exact. Never a silent cross-node pick.\n\n**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline), and an optional `alias` — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.\n\n**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt endpoint shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content…\n\n#### Shell sleep/wake (offline ↔ online)\n\nA shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:\n\n- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers \"owner already up.\" **Node-qualified once instances span nodes** (ruled 2026-07-24, #70 grill): when an owner comes online at a given node, only the **latest** instances qualified to that *endpoint@node* pairing come online with it — a stale instance of the same shell left on another node does not auto-wake alongside. A **once-per-daemon-generation boot sweep** restores every down persistent instance whose owner is online, after a machine reboot **or a daemon-only restart**. Birth-safe liveness protects both an online binary and a live launch awaiting bind. Machine boot time, recorded launch age, and missing launch stamps do **not** gate res…\n  **Daemons boot before endpoints do, so the sweep is not the only trigger:** the reconcile loop runs that **same per-owner restore decision** on each owner **offline→online edge**. The edge is seeded before the sweep; a live launch awaiting bind prevents duplicate launch if both triggers reach it. Failed launches are attempted once per owner-online event, not every tick while the owner stays online. A force-kill does not revoke persistence: use `teardown` or a nonpersistent manifest to keep the instance down. <!-- [doc->REQ-SHELL-OWNER-ONLINE-RESTORE] -->\n- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs while the shell is offline and eligible. Its sole job is to fire a wake. It runs on the **shell's node**, where the platform wake-event originates (e.g. interacting with a shut-down avatar). Covers \"owner is down, wake it from outside.\"\n\n**Online/offline are mutually-exclusive processes:** a live shell binary (including a launch awaiting bind) excludes its wake-watcher. A recorded-offline instance is watcher-eligible when it is persistent, carries no corpse, or its corpse's launch predates machine boot. **Only nonpersistent instances retain the same-boot force-kill freeze:** their same-boot or undatable corpses arm no watcher, preserving the deploy/overwrite case. A persistent corpse bypasses that freeze even without a launch stamp or boot oracle (releases#287). Missing birth evidence retains the existing fail-toward-alive process probe; it is not itself a restore refusal.\n\n**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.\n\n**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —\n- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;\n- owner **suspended** → revive the owner, then online the shell;\n- owner **active** elsewhere → attach/online the shell;\n- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).\n\nThe same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.\n\n**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):\nThe user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking t…\n_Avoid_: \"screen share\"; resize-per-viewer; silent controller displacement.\n…\n\n\n[Showing lines 369-408 of 1267. Use :409 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"\r\n<!-- [doc->REQ-CONSENT-3] -->\r\n**per-capability approval gates** (ratified 2026-06-11, Gateway grill): the same `require_approval` enum may ride **individual capability entries** in a shell manifest — gating the dangerous *operation*, not just the spawn. Same grant store, same interactive escalation, same floor semantics. A capability may declare a **class key** so grants are scoped finer than the capability itself: the first consumer is the usbip shell's `attach`, granted per **(owner endpoint × device class × node)** — a remembered HID-attach grant never authorizes a storage-class attach. Spawn gates govern *existence*; capability gates govern *acts*.\r\n\r\n**instance cap (`max_instances_per_owner` + `over_cap`, manifest):** an optional ceiling on how many instances of this shell adapter **one owner endpoint** may hold. The count is **all existing instances** — online *and* offline, every non-torn-down perch (`shell teardown` frees a slot) — so offline persistent shells cannot be stockpiled to evade it. Unset ⇒ unlimited. At the cap, **`over_cap`** decides: `reject` (**default**) refuses the spawn outright; `approve` requires per-spawn approval for each instance beyond the cap (allow-once each — it does **not** permanently raise the cap; a bigger ceiling is a config change, not a grant). Serves the consent model's **runaway** concern, and **composes with `require_approval`**: that governs spawns *under* the cap…\n\r\n**shell instance aliasing:** every instance has an immutable canonical id **`<adapter>-<n>`** (`GameRobot-0`, `GameRobot-1`, …) — which itself encodes the providing adapter — plus an optional **alias**, a friendly owner-unique label (`TempleKeeper`) set at spawn (`shell spawn GameRobot --alias TempleKeeper`) or later (`shell rename <ref> <alias>`). Alias and canonical id are interchangeable for addressing (`shell cmd TempleKeeper …`). The alias is a *display/address* overlay only: `adapter_name` always rides the perch, so `shell list` still shows the underlying type (`TempleKeeper → GameRobot, online`) — aliasing never obscures what a shell is.\r\n\r\n**Agent awareness.** Available shells *and* the agent's own shell instances (with online/offline state) are injected into the agent's context (SessionStart additionalContext / `$LIVE start`-equiv + hidden-context versions) — the agent ideally just \"knows.\" A `shell list` command also exists.\r\n\r\n**Discovery scope (what \"available shells\" resolves to):** the set surfaced to an agent (injected + `shell list`) is **(1)** its **own instances** — alias · canonical `<adapter>-<n>` · status — always; plus **(2) instantiable adapters**: shell adapters **registered on the agent's own node** with `broadcast ∈ {subnet, same-node}`, and shell adapters on *other* subnet nodes with `broadcast = subnet`. `broadcast = none` shells are **never** surfaced proactively — spawnable only by explicit user/agent instruction (if registered locally / reachable). Discovering an *other-node* shell does **not** imply free instantiation: spawning it rides the deferred **instantiate-anywhere** gate. (Discovery is gated by the same reach gates as everything else — an agent only se…\n\r\n**Cross-node instance resolution** (ruled 2026-07-24, #70 grill — governs the cross-node drive leg): an unqualified shell ref resolves **local-first** — if the owner holds a matching instance on the *current* node, that one is used, even when same-ref instances exist elsewhere. Resolution **refuses as ambiguous only** when the ref matches instances on more than one node **and none is local**; the qualified form (`ref@node`) is the escape hatch and always exact. Never a silent cross-node pick.\r\n\r\n**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline), and an optional `alias` — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.\r\n\r\n**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt endpoint shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content…\n\r\n#### Shell sleep/wake (offline ↔ online)\r\n\r\nA shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:\r\n\r\n- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers \"owner already up.\" **Node-qualified once instances span nodes** (ruled 2026-07-24, #70 grill): when an owner comes online at a given node, only the **latest** instances qualified to that *endpoint@node* pairing come online with it — a stale instance of the same shell left on another node does not auto-wake alongside. A **once-per-daemon-generation boot sweep** restores every down persistent instance whose owner is online, after a machine reboot **or a daemon-only restart**. Birth-safe liveness protects both an online binary and a live launch awaiting bind. Machine boot time, recorded launch age, and missing launch stamps do **not** gate res…\n  **Daemons boot before endpoints do, so the sweep is not the only trigger:** the reconcile loop runs that **same per-owner restore decision** on each owner **offline→online edge**. The edge is seeded before the sweep; a live launch awaiting bind prevents duplicate launch if both triggers reach it. Failed launches are attempted once per owner-online event, not every tick while the owner stays online. A force-kill does not revoke persistence: use `teardown` or a nonpersistent manifest to keep the instance down. <!-- [doc->REQ-SHELL-OWNER-ONLINE-RESTORE] -->\r\n- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs while the shell is offline and eligible. Its sole job is to fire a wake. It runs on the **shell's node**, where the platform wake-event originates (e.g. interacting with a shut-down avatar). Covers \"owner is down, wake it from outside.\"\r\n\r\n**Online/offline are mutually-exclusive processes:** a live shell binary (including a launch awaiting bind) excludes its wake-watcher. A recorded-offline instance is watcher-eligible when it is persistent, carries no corpse, or its corpse's launch predates machine boot. **Only nonpersistent instances retain the same-boot force-kill freeze:** their same-boot or undatable corpses arm no watcher, preserving the deploy/overwrite case. A persistent corpse bypasses that freeze even without a launch stamp or boot oracle (releases#287). Missing birth evidence retains the existing fail-toward-alive process probe; it is not itself a restore refusal.\r\n\r\n**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.\r\n\r\n**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —\r\n- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;\r\n- owner **suspended** → revive the owner, then online the shell;\r\n- owner **active** elsewhere → attach/online the shell;\r\n- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).\r\n\r\nThe same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.\r\n\r\n**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):\r\nThe user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking t…","truncated":true,"truncatedBy":"lines","totalLines":899,"totalBytes":9932,"outputLines":40,"outputBytes":9932,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1267,"displayContent":{"text":"#### Shell model (detailed)\n…\n\r\n<!-- [doc->REQ-CONSENT-3] -->\r\n**per-capability approval gates** (ratified 2026-06-11, Gateway grill): the same `require_approval` enum may ride **individual capability entries** in a shell manifest — gating the dangerous *operation*, not just the spawn. Same grant store, same interactive escalation, same floor semantics. A capability may declare a **class key** so grants are scoped finer than the capability itself: the first consumer is the usbip shell's `attach`, granted per **(owner endpoint × device class × node)** — a remembered HID-attach grant never authorizes a storage-class attach. Spawn gates govern *existence*; capability gates govern *acts*.\r\n\r\n**instance cap (`max_instances_per_owner` + `over_cap`, manifest):** an optional ceiling on how many instances of this shell adapter **one owner endpoint** may hold. The count is **all existing instances** — online *and* offline, every non-torn-down perch (`shell teardown` frees a slot) — so offline persistent shells cannot be stockpiled to evade it. Unset ⇒ unlimited. At the cap, **`over_cap`** decides: `reject` (**default**) refuses the spawn outright; `approve` requires per-spawn approval for each instance beyond the cap (allow-once each — it does **not** permanently raise the cap; a bigger ceiling is a config change, not a grant). Serves the consent model's **runaway** concern, and **composes with `require_approval`**: that governs spawns *under* the cap…\n\r\n**shell instance aliasing:** every instance has an immutable canonical id **`<adapter>-<n>`** (`GameRobot-0`, `GameRobot-1`, …) — which itself encodes the providing adapter — plus an optional **alias**, a friendly owner-unique label (`TempleKeeper`) set at spawn (`shell spawn GameRobot --alias TempleKeeper`) or later (`shell rename <ref> <alias>`). Alias and canonical id are interchangeable for addressing (`shell cmd TempleKeeper …`). The alias is a *display/address* overlay only: `adapter_name` always rides the perch, so `shell list` still shows the underlying type (`TempleKeeper → GameRobot, online`) — aliasing never obscures what a shell is.\r\n\r\n**Agent awareness.** Available shells *and* the agent's own shell instances (with online/offline state) are injected into the agent's context (SessionStart additionalContext / `$LIVE start`-equiv + hidden-context versions) — the agent ideally just \"knows.\" A `shell list` command also exists.\r\n\r\n**Discovery scope (what \"available shells\" resolves to):** the set surfaced to an agent (injected + `shell list`) is **(1)** its **own instances** — alias · canonical `<adapter>-<n>` · status — always; plus **(2) instantiable adapters**: shell adapters **registered on the agent's own node** with `broadcast ∈ {subnet, same-node}`, and shell adapters on *other* subnet nodes with `broadcast = subnet`. `broadcast = none` shells are **never** surfaced proactively — spawnable only by explicit user/agent instruction (if registered locally / reachable). Discovering an *other-node* shell does **not** imply free instantiation: spawning it rides the deferred **instantiate-anywhere** gate. (Discovery is gated by the same reach gates as everything else — an agent only se…\n\r\n**Cross-node instance resolution** (ruled 2026-07-24, #70 grill — governs the cross-node drive leg): an unqualified shell ref resolves **local-first** — if the owner holds a matching instance on the *current* node, that one is used, even when same-ref instances exist elsewhere. Resolution **refuses as ambiguous only** when the ref matches instances on more than one node **and none is local**; the qualified form (`ref@node`) is the escape hatch and always exact. Never a silent cross-node pick.\r\n\r\n**Not in the subnet registry.** Shell perches are private to the agent↔shell link, not a general messaging surface — they are nested under the owner (`perches/<endpoint_id>/shells/<adapter>-<n>/`) and not registry-advertised like agent endpoints. The perch `info.json` carries only `type=Shell`, `owner`, `adapter_name`, `status` (online|offline), and an optional `alias` — the capability set is resolved from the shell adapter manifest by `adapter_name`, not duplicated on the perch.\r\n\r\n**Shell-relevant commands:** agent/user surface — `spt shell spawn <shell_adapter> [--id]`, `shell list`, `shell relink <id>`, `shell teardown <id>`, `shell cmd <id> <op…>` (or ordinary `send` of a typed command payload), and **`spt endpoint shutdown`** (an agent gracefully shuts down *its own* endpoint → suspended; fires the transition echo-commune; cascades its persistent shells offline). Machinery `api` surface — `api bind` (type=Shell), `api poll` (relay delivery), `api emit --type <sensory_type> <payload>` (sensory back to the owner; owner known from bind), and **`api owner-shutdown`** (a shell directly suspends its linked owner, bypassing agent comms — gated by `can_shutdown` in the shell manifest; the channel is this machinery command, not the content…\n\r\n#### Shell sleep/wake (offline ↔ online)\r\n\r\nA shell automatically goes **offline when its owner endpoint goes offline**. Two complementary manifest options bring it (and its owner) back:\r\n\r\n- **`persistent`** (bool): the shell is automatically brought **online whenever its owner endpoint is online**. Covers \"owner already up.\" **Node-qualified once instances span nodes** (ruled 2026-07-24, #70 grill): when an owner comes online at a given node, only the **latest** instances qualified to that *endpoint@node* pairing come online with it — a stale instance of the same shell left on another node does not auto-wake alongside. A **once-per-daemon-generation boot sweep** restores every down persistent instance whose owner is online, after a machine reboot **or a daemon-only restart**. Birth-safe liveness protects both an online binary and a live launch awaiting bind. Machine boot time, recorded launch age, and missing launch stamps do **not** gate res…\n  **Daemons boot before endpoints do, so the sweep is not the only trigger:** the reconcile loop runs that **same per-owner restore decision** on each owner **offline→online edge**. The edge is seeded before the sweep; a live launch awaiting bind prevents duplicate launch if both triggers reach it. Failed launches are attempted once per owner-online event, not every tick while the owner stays online. A force-kill does not revoke persistence: use `teardown` or a nonpersistent manifest to keep the instance down. <!-- [doc->REQ-SHELL-OWNER-ONLINE-RESTORE] -->\r\n- **`wake_command`** (template): a long-running **wake-watcher** process spt-core runs while the shell is offline and eligible. Its sole job is to fire a wake. It runs on the **shell's node**, where the platform wake-event originates (e.g. interacting with a shut-down avatar). Covers \"owner is down, wake it from outside.\"\r\n\r\n**Online/offline are mutually-exclusive processes:** a live shell binary (including a launch awaiting bind) excludes its wake-watcher. A recorded-offline instance is watcher-eligible when it is persistent, carries no corpse, or its corpse's launch predates machine boot. **Only nonpersistent instances retain the same-boot force-kill freeze:** their same-boot or undatable corpses arm no watcher, preserving the deploy/overwrite case. A persistent corpse bypasses that freeze even without a launch stamp or boot oracle (releases#287). Missing birth evidence retains the existing fail-toward-alive process probe; it is not itself a restore refusal.\r\n\r\n**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.\r\n\r\n**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —\r\n- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;\r\n- owner **suspended** → revive the owner, then online the shell;\r\n- owner **active** elsewhere → attach/online the shell;\r\n- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).\r\n\r\nThe same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.\r\n\r\n**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):\r\nThe user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking t…\n_Avoid_: \"screen share\"; resize-per-viewer; silent controller displacement.\n…\n","startLine":335,"lineNumbers":[335,null,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,null,528]},"fileSize":351663,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\CONTEXT.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1267,"totalBytes":9932,"outputLines":40,"outputBytes":9932,"shownRange":{"start":369,"end":408},"nextOffset":409},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789394635439}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TMMUn2YElbIFgBaX2suuONJQ|fc_0a63d1e8a5b46c9c016aa7fed2593887d1b03781c7d0c5ed67","toolName":"read","startedAt":"2026-09-14T14:04:02.494Z","args":{"path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs:19-91"},"intent":"Reading hostname platform calls"},"id":"96620109","parentId":"4a6dea1c","timestamp":"2026-09-14T14:04:02.494Z"}
{"type":"message","id":"586f0eb2","parentId":"96620109","timestamp":"2026-09-14T14:04:02.494Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reading hostname small**"},{"type":"toolCall","id":"call_TMMUn2YElbIFgBaX2suuONJQ|fc_0a63d1e8a5b46c9c016aa7fed2593887d1b03781c7d0c5ed67","name":"read","arguments":{"i":"Reading hostname platform calls","path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs:19-91"},"intent":"Reading hostname platform calls"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":3946,"output":56,"cacheRead":70144,"cacheWrite":0,"totalTokens":74146,"reasoningTokens":8,"cost":{"input":0.03946,"output":0.0028,"cacheRead":0.070144,"cacheWrite":0,"total":0.112404}},"stopReason":"toolUse","timestamp":1789394635469,"responseId":"resp_0a63d1e8a5b46c9c016aa7fecbaddc87d1aa96e18edacf4bbf","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7fece82c887d1829805418768c2d6","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_7StzLIGjLKltl4HOw795TYt-WN263TN2xGUBbRwRp9ebjaXyiyL3Btew2ufarjuaLxY3MPgoaEhZyUB3od_-qX4oA2jjMHNcWa7DJm9o6v3MYzUeX30mzBVsZFs81zzwsRDUS5AqrH-4KEfAieyIBldr1rSLN0yPAEf2MQpTUVNBpwaAFihQCmkD2yGLmbO_JGQaRl9d-PMuf-qyCNPZhXl4ng2klpobDGmrDFXlwKXpKL4TnJEYaEcoKw9n2Om1v17kW8lzb_LZneYdAyT3O8thhuU874fUSWhoySNvrD9g-rGdFauZMaYouU1xY9LvKnBT_r029U9eOVQJ0GjFaPeZ8WbGKUY2k5QPlWKCn10bpaPhiDEng5yXt6iAfd-wH4QS9z1YkyJERhjyyckIWMXUhc5jrD8X0SZgM3X3zafR3P25RsGDQdsOK5sLWdTL4E1ZdJ-fsNziCdxb5hcDn6XjcsKvsQWHkeC9WLFvnQfgeftPXvpKQggtaotbbzNg3tIkpYdyuHdAXLZvfrY5apwVXvOCQI2NM_CSRwjAXZt78gR6Gv3vc54Im_MsfgV1cmKP3MJWnf64DumhF73ChrGt6G3LVhKB2lXy2STHOhwG_ZZeYuHxYRiwOSpWa3IHVGIWI1g48SHxY0_1pRQ8clXBEJiBtqxh4o8xLaS0IGVV1Jvb8zhHQfPf7UK0oLlQqnDj9MSZ59siA-lDH7ZauMINaieQfIoyK-OaQiM7yJlCeObww9rBpfSjj-FhFXzqmHlxSdZmZUc0gpetaqKQPbz8Ihxr_QsKYMCcpwyIxkikEHyhwZxYb1jd4h9QBcxsdwWwmmlmFeoYgjZOsYZZF_o2113BCZyR2xn6PHTDCCfuunoqfnoO3HQBmWiIYHxZNGuX-EOk72efSUjybfRf86KSJpx3EFKlT_rWoLLDwsFQaanQ72VR0YGC7H-0BOBP5MaT5mwvvfgK5t7oP2uPyVFVRj5lVTMiv8qVfugtRk70mDzSCLd0quwd6H7wcfF-p8JltSzZdMYEKeeJnf73bprjGD85TxWMZPrIID5mANdivLjg7I9yUFuS3ZeeJ37DJOEipd2HMvqfjMuRvgX0R5NsmYKZESghBm4ppOrUw_d35k0MWel4zo1jAsCcZxZayvWep7ShDXdGkt8mOpCygZRwKHINVWg0WHCbCvVxi_sSURZIjAc5oshFIc2jIf-5yVCCZ1zNRkmqZ8iOXB0IbrickVMbQ3bmX6xWAQ8sI5Cbj3KGqFOG2noIFNnE-W_RZLQ0iI1BuMewdZkzFYOa85sAPUUdiR7L0M5PWBMvZEtcA=","internal_chat_message_metadata_passthrough":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"summary":[{"type":"summary_text","text":"**Reading hostname small**"}],"metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fed2593887d1b03781c7d0c5ed67","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hostname platform calls\",\"path\":\".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs:19-91\"}","call_id":"call_TMMUn2YElbIFgBaX2suuONJQ","internal_chat_message_metadata_passthrough":{"create_time":1789394635.871884,"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"},"name":"read","metadata":{"turn_id":"02992209-00ed-4a3f-8b22-3c368dc4486a"}}]},"duration":7023.454499989748,"ttft":3066.210299998522,"completedAt":1789394642493,"contextSnapshot":{"promptTokens":74090,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"000ffeda","parentId":"586f0eb2","timestamp":"2026-09-14T14:04:02.503Z","message":{"role":"toolResult","toolCallId":"call_TMMUn2YElbIFgBaX2suuONJQ|fc_0a63d1e8a5b46c9c016aa7fed2593887d1b03781c7d0c5ed67","toolName":"read","content":[{"type":"text","text":"pub fn is_reserved_web_facet(label: &str) -> bool {\n    [\"docs\", \"f\", \"a\", \"m\", \"bin\", \"install\"]\n…\n        .any(|facet| label.eq_ignore_ascii_case(facet))\n}\n\n/// This machine's OS hostname, if the OS will say. `None` (rather than a\n/// placeholder) when it won't — an absent label renders as the bare key\n/// prefix, never a lie.\npub fn os_hostname() -> Option<String> {\n    let name = detect()?;\n    let name = name.trim();\n    (!name.is_empty()).then(|| name.to_string())\n}\n\n/// Resolve the `{node}` manifest-fill value (REQ-MANIFEST-NODE-KEY): the node's\n/// ADVERTISED label. `in_mem` is the daemon's startup-read label (the value it\n/// stamps on every registry row it advertises, `RegistryHost.node_label`) — passed\n/// through when present + non-empty so `{node}` == advertised == rendered. Falls\n/// back to a FRESH [`os_hostname`] ONLY when no in-mem label is known (the CLI-side\n/// self-spawn, which has no daemon handle — it already sources its own self-label\n/// this way; identical to the advertised value except across a mid-daemon-life\n/// hostname change). `None` when neither is available → the caller MUST leave\n/// `{node}` unfilled so a manifest referencing it fails LOUD, never an empty token.\n// [impl->REQ-MANIFEST-NODE-KEY]\npub fn node_fill_label(in_mem: Option<&str>) -> Option<String> {\n    in_mem\n        .map(str::trim)\n        .filter(|s| !s.is_empty())\n        .map(str::to_string)\n        .or_else(os_hostname)\n}\n\n#[cfg(windows)]\nfn detect() -> Option<String> {\n    // Set by the OS for every process; the NetBIOS-style uppercase name\n    // (e.g. `HFENDULEAM`) — exactly what views should render.\n    std::env::var(\"COMPUTERNAME\").ok()\n}\n\n#[cfg(unix)]\nfn detect() -> Option<String> {\n    // `gethostname(2)` — no env dependence (HOSTNAME is shell-managed and\n    // absent in non-interactive contexts).\n    let mut buf = [0u8; 256];\n    let rc = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };\n    if rc != 0 {\n        return None;\n    }\n    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());\n    String::from_utf8(buf[..len].to_vec()).ok()\n}\n\n#[cfg(not(any(unix, windows)))]\nfn detect() -> Option<String> {\n    None\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-SUBNET-3] supported platforms produce a non-empty,\n    // whitespace-trimmed label (the actual value is the runner's hostname —\n    // environment-dependent, so only the shape is asserted).\n    #[test]\n    fn hostname_is_nonempty_and_trimmed_on_supported_platforms() {\n        if cfg!(any(unix, windows)) {\n            let label = os_hostname().expect(\"supported OS yields a hostname\");\n            assert!(!label.is_empty());\n            assert_eq!(label, label.trim());\n        }\n    }\n\n    // [unit->REQ-MANIFEST-NODE-KEY] the {node} fill resolver prefers the in-mem\n    // advertised label (the daemon's startup value, so fill == advertised) and falls\n    // back to the OS hostname ONLY when no in-mem label is known (the CLI self-spawn).\n    #[test]\n    fn node_key_absent_label_falls_back_to_hostname() {\n        // A present, non-empty in-mem label passes through verbatim (trimmed).\n…\n    }\n}\n\n[Showing lines 18-94 of 110. Use :95 to continue]"}],"details":{"truncation":{"content":"        .any(|facet| label.eq_ignore_ascii_case(facet))\r\n}\r\n\r\n/// This machine's OS hostname, if the OS will say. `None` (rather than a\r\n/// placeholder) when it won't — an absent label renders as the bare key\r\n/// prefix, never a lie.\r\npub fn os_hostname() -> Option<String> {\r\n    let name = detect()?;\r\n    let name = name.trim();\r\n    (!name.is_empty()).then(|| name.to_string())\r\n}\r\n\r\n/// Resolve the `{node}` manifest-fill value (REQ-MANIFEST-NODE-KEY): the node's\r\n/// ADVERTISED label. `in_mem` is the daemon's startup-read label (the value it\r\n/// stamps on every registry row it advertises, `RegistryHost.node_label`) — passed\r\n/// through when present + non-empty so `{node}` == advertised == rendered. Falls\r\n/// back to a FRESH [`os_hostname`] ONLY when no in-mem label is known (the CLI-side\r\n/// self-spawn, which has no daemon handle — it already sources its own self-label\r\n/// this way; identical to the advertised value except across a mid-daemon-life\r\n/// hostname change). `None` when neither is available → the caller MUST leave\r\n/// `{node}` unfilled so a manifest referencing it fails LOUD, never an empty token.\r\n// [impl->REQ-MANIFEST-NODE-KEY]\r\npub fn node_fill_label(in_mem: Option<&str>) -> Option<String> {\r\n    in_mem\r\n        .map(str::trim)\r\n        .filter(|s| !s.is_empty())\r\n        .map(str::to_string)\r\n        .or_else(os_hostname)\r\n}\r\n\r\n#[cfg(windows)]\r\nfn detect() -> Option<String> {\r\n    // Set by the OS for every process; the NetBIOS-style uppercase name\r\n    // (e.g. `HFENDULEAM`) — exactly what views should render.\r\n    std::env::var(\"COMPUTERNAME\").ok()\r\n}\r\n\r\n#[cfg(unix)]\r\nfn detect() -> Option<String> {\r\n    // `gethostname(2)` — no env dependence (HOSTNAME is shell-managed and\r\n    // absent in non-interactive contexts).\r\n    let mut buf = [0u8; 256];\r\n    let rc = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };\r\n    if rc != 0 {\r\n        return None;\r\n    }\r\n    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());\r\n    String::from_utf8(buf[..len].to_vec()).ok()\r\n}\r\n\r\n#[cfg(not(any(unix, windows)))]\r\nfn detect() -> Option<String> {\r\n    None\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-SUBNET-3] supported platforms produce a non-empty,\r\n    // whitespace-trimmed label (the actual value is the runner's hostname —\r\n    // environment-dependent, so only the shape is asserted).\r\n    #[test]\r\n    fn hostname_is_nonempty_and_trimmed_on_supported_platforms() {\r\n        if cfg!(any(unix, windows)) {\r\n            let label = os_hostname().expect(\"supported OS yields a hostname\");\r\n            assert!(!label.is_empty());\r\n            assert_eq!(label, label.trim());\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-MANIFEST-NODE-KEY] the {node} fill resolver prefers the in-mem\r\n    // advertised label (the daemon's startup value, so fill == advertised) and falls\r\n    // back to the OS hostname ONLY when no in-mem label is known (the CLI self-spawn).\r\n    #[test]\r\n    fn node_key_absent_label_falls_back_to_hostname() {\r\n        // A present, non-empty in-mem label passes through verbatim (trimmed).\r","truncated":true,"truncatedBy":"lines","totalLines":93,"totalBytes":3144,"outputLines":77,"outputBytes":3144,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":110,"displayContent":{"text":"pub fn is_reserved_web_facet(label: &str) -> bool {\n    [\"docs\", \"f\", \"a\", \"m\", \"bin\", \"install\"]\n…\n        .any(|facet| label.eq_ignore_ascii_case(facet))\r\n}\r\n\r\n/// This machine's OS hostname, if the OS will say. `None` (rather than a\r\n/// placeholder) when it won't — an absent label renders as the bare key\r\n/// prefix, never a lie.\r\npub fn os_hostname() -> Option<String> {\r\n    let name = detect()?;\r\n    let name = name.trim();\r\n    (!name.is_empty()).then(|| name.to_string())\r\n}\r\n\r\n/// Resolve the `{node}` manifest-fill value (REQ-MANIFEST-NODE-KEY): the node's\r\n/// ADVERTISED label. `in_mem` is the daemon's startup-read label (the value it\r\n/// stamps on every registry row it advertises, `RegistryHost.node_label`) — passed\r\n/// through when present + non-empty so `{node}` == advertised == rendered. Falls\r\n/// back to a FRESH [`os_hostname`] ONLY when no in-mem label is known (the CLI-side\r\n/// self-spawn, which has no daemon handle — it already sources its own self-label\r\n/// this way; identical to the advertised value except across a mid-daemon-life\r\n/// hostname change). `None` when neither is available → the caller MUST leave\r\n/// `{node}` unfilled so a manifest referencing it fails LOUD, never an empty token.\r\n// [impl->REQ-MANIFEST-NODE-KEY]\r\npub fn node_fill_label(in_mem: Option<&str>) -> Option<String> {\r\n    in_mem\r\n        .map(str::trim)\r\n        .filter(|s| !s.is_empty())\r\n        .map(str::to_string)\r\n        .or_else(os_hostname)\r\n}\r\n\r\n#[cfg(windows)]\r\nfn detect() -> Option<String> {\r\n    // Set by the OS for every process; the NetBIOS-style uppercase name\r\n    // (e.g. `HFENDULEAM`) — exactly what views should render.\r\n    std::env::var(\"COMPUTERNAME\").ok()\r\n}\r\n\r\n#[cfg(unix)]\r\nfn detect() -> Option<String> {\r\n    // `gethostname(2)` — no env dependence (HOSTNAME is shell-managed and\r\n    // absent in non-interactive contexts).\r\n    let mut buf = [0u8; 256];\r\n    let rc = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };\r\n    if rc != 0 {\r\n        return None;\r\n    }\r\n    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());\r\n    String::from_utf8(buf[..len].to_vec()).ok()\r\n}\r\n\r\n#[cfg(not(any(unix, windows)))]\r\nfn detect() -> Option<String> {\r\n    None\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-SUBNET-3] supported platforms produce a non-empty,\r\n    // whitespace-trimmed label (the actual value is the runner's hostname —\r\n    // environment-dependent, so only the shape is asserted).\r\n    #[test]\r\n    fn hostname_is_nonempty_and_trimmed_on_supported_platforms() {\r\n        if cfg!(any(unix, windows)) {\r\n            let label = os_hostname().expect(\"supported OS yields a hostname\");\r\n            assert!(!label.is_empty());\r\n            assert_eq!(label, label.trim());\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-MANIFEST-NODE-KEY] the {node} fill resolver prefers the in-mem\r\n    // advertised label (the daemon's startup value, so fill == advertised) and falls\r\n    // back to the OS hostname ONLY when no in-mem label is known (the CLI self-spawn).\r\n    #[test]\r\n    fn node_key_absent_label_falls_back_to_hostname() {\r\n        // A present, non-empty in-mem label passes through verbatim (trimmed).\r\n…\n    }\n}","startLine":15,"lineNumbers":[15,16,null,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,null,109,110]},"fileSize":4797,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\297-enforcement-shape\\crates\\spt-store\\src\\hostlabel.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":110,"totalBytes":3144,"outputLines":77,"outputBytes":3144,"shownRange":{"start":18,"end":94},"nextOffset":95}}},"isError":false,"timestamp":1789394642502}}
{"type":"message","id":"753bcb8e","parentId":"000ffeda","timestamp":"2026-09-14T14:04:02.512Z","message":{"role":"user","content":"Current interruptible wait interrupted: IRC message from parent agent `Main`.\n\nParent IRC message:\n\nMain read fixture110-133: attach_ipc_deadline default2s, override SPT_ATTACH_IPC_DEADLINE_MS; comment says CI explicitly30000 on shared boxes for request_attach scheduler starvation, but helper reused by spawn_xlate_session. Consumer scrub leaves only ephemeral override, so baseline cohort MUST retain2s, no silent30s injection. Both red test bodies finish2.04s; g1 waiter_armed892ms, g7 1328ms. Need actual next blocking operation before reply.","attribution":"agent","timestamp":1789394635852,"steering":true}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_dWuxla3klsYLsdsK8G7cW2bv|fc_0a63d1e8a5b46c9c016aa7fed6fc8487d1be3397e1ae4251af","toolName":"hub","startedAt":"2026-09-14T14:04:11.157Z","intent":"Answering next blocking operation"},"id":"9cf66f00","parentId":"753bcb8e","timestamp":"2026-09-14T14:04:11.157Z"}
{"type":"message","id":"c7149e1d","parentId":"9cf66f00","timestamp":"2026-09-14T14:04:11.158Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_dWuxla3klsYLsdsK8G7cW2bv|fc_0a63d1e8a5b46c9c016aa7fed6fc8487d1be3397e1ae4251af","name":"hub","arguments":{"i":"Answering next blocking operation","op":"send","to":"Main","message":"After waiter_armed line output: (1) same spawn_phase closure recover(wake_inflight)8119 then returns; (2) build_translation4709 -> TranslationChild::spawn translation247-291, first substantive potentially blocking OS op command.spawn266 (pipes/process create), then reader thread spawn274; (3) Windows os_hostname is only COMPUTERNAME env read hostlabel48-51, no DNS/process; (4) Init child.send broker4714 uses synchronous stdin lock/write_all/flush translation311-320, ignored error; (5) inject-worker thread spawn broker4738, no ready wait. No journal or NetHost call in this route. Source-backed strongest candidate: accumulated startup >2s, already ~45%/~66% spent before second process creation; exact blocked syscall unproven."},"intent":"Answering next blocking operation"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1180,"output":228,"cacheRead":73984,"cacheWrite":0,"totalTokens":75392,"reasoningTokens":28,"cost":{"input":0.011800000000000001,"output":0.0114,"cacheRead":0.073984,"cacheWrite":0,"total":0.09718399999999999}},"stopReason":"toolUse","timestamp":1789394642510,"responseId":"resp_0a63d1e8a5b46c9c016aa7fed2c8e487d1add3e41f3a2212ca","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7fed44c7c87d1a0af0c7dc0a2cf09","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_7Wpx6QS2OPLzz4-HTfI9qWkOqOI_f6tl-76F7TellqsfGPrnAJdodrXqWi2Sg68Qomq98mzhnsWSlW5PBB_ps-DcrH6caASsS125r582yhNnUAJg2nLamndBCR2i2gPs-MextZ_swO-RxT1zXMX8-Z8pmvAcNQwKDs9CVuN4_DRbp8WD0Inda1YT-hizN5VwlpUx-2VVEnAzZgiGyUfO0c2Y54nCe1M3a_rAeEQ20HDaQh28IGcA73TrZUnX9ljxwUWIWgFVykHVBwNgsmQvMH6aMWlXSi-p3-tOWBk_Z6GBQfZ19dAWOSec20XovvyVvSMOqjnhqwXk1MvqlhP5lWm1LiHF6xjjVagDNw5vaz_l6Er9aaqHD8U-D2SDt54mjNPdqQtR1jUifzylZq-bYn-3L_k3eYSFYiXxLB2UZVmmxVDlTqx8JmZoUcSiI7t1G253iJyfZ1NR1Y4GwJhIg0pfPz11fu1Y3eD4A9xWjx3qRdbMZd8m_d2vRqgHOj7SXWNI5hu17TDjS0wmllU8VLle3kjCy5KzhbLZvUSm28ZOFSUwkb7N3DkbHKBpVNPTtGl98_xL61k0hTajxDm5tiX2jO5rgsS678iOVgWe2MzV0kwTGO9Wd3s5o8l9Rk4y-GBxdXR8oEukyS1X6V31zi2K2lRSQig-Ti_Ls9gELCu5GcyOYwQAdNll6TYTip2_Mal0UJtjZgIXknC0LkF2GnXDowYgPTdeIK2ST_j6bN0T2eNnUV1UK99vSRtO5pvNDt1HnenKcUwK_zx59KbFcqclpjUkRy8X8nX8xlNiOCV2qY_Ck1VbgR3xIu55mPlTQ4by6J6q4c-33kcRTWbtL9Xkvl7486fgXtgytpbUNhEHzmS1XJxNKJmoWRD2cXmP5PF76TaV_PdbXGhlsgCvUhZZzIxp7k8zVStTEV5xBRAJsDpAOZSLeSQR61CgUSEkw2WRIVvKhG-QBqqGMWirfJiN8zOnLI_xRZJ1Rp0jKU6hPH-7_neKpeawZbHTsA3cAifVI8t9FDLLMv2A8mqOq526D8iN_k2WB8xD3Wp9XRR_M7AMHphI3O6h80RK0pBE7bsjOGB60e6ZBgyZfmuu59UEaK2ja8iBGHqPWGZZj1XhKumlsMTQBmHRk193sAiPtZLhzSW9QvMC56mmRVR8mfE6-oEeWlazAVjkJkp-bMA2xKaYTPYzWsaEBJriSEppalYk66RODch8soy8GFb5d1WHbYXh6SmZ06KW7tc7o3_o3TpJY2kwBlR3hAj-PEUMOb0Zjf_vvybMr6jtXsvlbCbRsNgzlkslNESG6fjc3kqeChwC25HX4EbpqnZf6STR2qupMmDxY211CzDP90abuRHzMrBXLjcBFX4eUlXq8Ql9Lbgc9bwq6ruBcjNBItSm5bJK_9TE7twYJcf8wvOrpV8crFaUP33JbU8M6ilV2Sr_Q=","internal_chat_message_metadata_passthrough":{"turn_id":"fe25fe33-abd7-4906-9792-91e060e64ee7"},"summary":[],"metadata":{"turn_id":"fe25fe33-abd7-4906-9792-91e060e64ee7"}},{"id":"fc_0a63d1e8a5b46c9c016aa7fed6fc8487d1be3397e1ae4251af","type":"function_call","status":"completed","arguments":"{\"i\":\"Answering next blocking operation\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"After waiter_armed line output: (1) same spawn_phase closure recover(wake_inflight)8119 then returns; (2) build_translation4709 -> TranslationChild::spawn translation247-291, first substantive potentially blocking OS op command.spawn266 (pipes/process create), then reader thread spawn274; (3) Windows os_hostname is only COMPUTERNAME env read hostlabel48-51, no DNS/process; (4) Init child.send broker4714 uses synchronous stdin lock/write_all/flush translation311-320, ignored error; (5) inject-worker thread spawn broker4738, no ready wait. No journal or NetHost call in this route. Source-backed strongest candidate: accumulated startup >2s, already ~45%/~66% spent before second process creation; exact blocked syscall unproven.\"}","call_id":"call_dWuxla3klsYLsdsK8G7cW2bv","internal_chat_message_metadata_passthrough":{"create_time":1789394643.10327,"turn_id":"fe25fe33-abd7-4906-9792-91e060e64ee7"},"name":"hub","metadata":{"turn_id":"fe25fe33-abd7-4906-9792-91e060e64ee7"}}]},"duration":8646.432500004768,"ttft":1813.0108000040054,"completedAt":1789394651157,"contextSnapshot":{"promptTokens":75164,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"17146b20","parentId":"c7149e1d","timestamp":"2026-09-14T14:04:11.159Z","message":{"role":"toolResult","toolCallId":"call_dWuxla3klsYLsdsK8G7cW2bv|fc_0a63d1e8a5b46c9c016aa7fed6fc8487d1be3397e1ae4251af","toolName":"hub","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"SpawnReplyTrace","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1789394651158}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DoRQUUtyWEaxZ7Cdz4Sj3XIy|fc_0a63d1e8a5b46c9c016aa7fedd823087d1a5741064451e0e8b","toolName":"yield","startedAt":"2026-09-14T14:06:09.192Z"},"id":"39f50c86","parentId":"17146b20","timestamp":"2026-09-14T14:06:09.192Z"}
{"type":"message","id":"567da017","parentId":"39f50c86","timestamp":"2026-09-14T14:06:09.192Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_DoRQUUtyWEaxZ7Cdz4Sj3XIy|fc_0a63d1e8a5b46c9c016aa7fedd823087d1a5741064451e0e8b","name":"yield","arguments":{"type":null,"data":{"summary":"The exact candidate bounds the spawn reply read correctly, but does not bound or cancel the broker's spawn operation. Both reported reds stop in the interval after waiter_armed and before translation_ready, whose substantive work is synchronous translation-process creation and its initial stdin write—not NetHost or journal startup. The 2s fixture budget covers PTY creation plus translation startup, leaving approximately 1.108s (g1) and 0.672s (g7) after waiter_armed. Existing evidence favors accumulated Windows startup latency over a direct #302 runtime/journal deadlock, without identifying the blocked syscall.","files":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs","description":"Pump construction 499-519; spawn request/reply loop 830-851; fixed deadline 1041-1043; Spawned decode 1208-1215; request writes 2555-2561; Split timeout semantics 2582-2621."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","description":"Per-connection synchronous dispatch 6052-6110; dispatch_spawn 6487-6492; phase closure 8112-8128; PTY creation and subscriber setup 8361-8487; waiter_armed 8672; translation startup 4702-4765 and 8682-8694; table insertion/reply 8696-8740."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/translation.rs","description":"Translation child Command::spawn and reader-thread creation 247-299; synchronous Init stdin mutex/write_all/flush 311-320. No ready handshake."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs","description":"Broker reply writes have their own gate-plus-write deadline 610-686; retirement tokens distinguish deadline poison from organic I/O failure 331-360."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs","description":"SpawnReq 498-556 carries neither timeout nor cancellation/request ID; Spawned 560-566 contains session ID and optional PID."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs","description":"Envelope 125-145 contains protocol_version, kind, payload—not request/reply correlation ID."},{"path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs","description":"os_hostname 24-28 uses COMPUTERNAME environment lookup on Windows 46-51; no network lookup or hostname subprocess."},{"path":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs","description":"emit_line_err 187-193 synchronously takes stderr's lock, writes the line, and ignores the result; relevant to interpreting the last printed phase."},{"path":".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md","description":"6.9 at429-432 documents output discarded while waiting for Spawned; 7.6 at501-528 specifies Split and fixed per-call deadlines; 7.8 at538-542 describes separate broker QUIC operation bounds."},{"path":".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Decisions 1,4,5,8 at23-30 define broker resource ownership, IPC boundary, and journal custody; process survival is independent of the requesting brain."},{"path":".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md","description":"Decisions 4-6 at62-81 define stamp convergence, atomic spawn-policy distinction, and lifecycle authority; fresh creation is not an attach/controller success claim."},{"path":".worktrees/297-enforcement-shape/CONTEXT.md","description":"Broker/brain ownership at35-39; remote-attach controller/viewer model near403-408."}],"architecture":"A Split Brain writes a Spawn envelope, then waits on its reader-thread channel with one absolute reply deadline. A dedicated broker connection handler executes spawn synchronously: gate → PTY → input/controller/drain/waiter → translation process plus Init → session table insertion → bounded broker connection write of Spawned. The request does not propagate its deadline into this operation. NetHost and EffectJournal are broker-owned but absent from this direct spawn/translation-startup call chain.","report":"## Scope and evidence status\nRead-only source investigation of the named candidate worktree; no edits, processes, validation, tests, debugger attachment, or credential reads. Incident timings and fixture contract below are Main-provided evidence, not independently re-executed findings. Navigation used exact-worktree grep/read only; no root-source LSP references were substituted. Main reported that worktree LSP references are empty while root references point to different source, so reference-search completeness must not be inferred from those results.\n\n## Precise causal route\n1. `Brain::cold_start_pump` (`brain.rs:499-519`) connects/handshakes, splits the connection, and stores `Some(io_timeout)`. `split_with_reader` (`375-404`) runs blocking `read_frame` on a dedicated native thread and forwards complete frame results to an unbounded MPSC channel. `PUMP_IPC_READER: spawned` proves thread entry, not successful reading of any reply.\n2. `spawn_session` (`604-606`) calls `spawn_session_pid` (`830-851`). It sends `KIND_SPAWN` first, then computes `call_deadline()` once (`836`; definition `1041-1043`). Each loop iteration uses the same deadline. Interleaved Output/Exit/other events cannot renew it. The send itself uses synchronous `write_frame` (`2555-2561`) outside that read budget; connect and handshake also precede it. Thus this is a bounded reply-wait, not a complete RPC-duration guarantee.\n3. `read_frame_until` (`2582-2621`) on Split calls `frames.recv_timeout(deadline.saturating_duration_since(now))`. Empty-at-expiry returns `TimedOut` with the exact string `brain IPC read deadline elapsed` (`2605-2607`). Whole plus Some(deadline) instead returns Unsupported (`2586-2594`), excluding silent Whole-carrier unbounded-read behavior for this reported error. One nuance: zero remaining time still drains already-queued frames; there is no absolute rejection of frames queued at/after the boundary. Therefore the code bounds waiting, not all possible CPU time spent draining an indefinitely nonempty queue.\n4. `Broker::handle_conn` (`6052-6110`) reads the request, synchronously calls `dispatch_spawn` on that connection's handler (`6092-6095`), then records the successful session subscription. `dispatch_spawn` decodes and calls `dispatch_spawn_policy` (`6487-6492`). There is no spawn operation deadline or cancellation token in `SpawnReq` (`msg.rs:498-556`). A client read timeout does not interrupt this dispatch; handler EOF cleanup is not reached until dispatch returns and the read loop observes failure.\n5. The reported phases establish that the gate was claimed immediately and PTY creation plus birth-identity sampling completed (`broker.rs:8391-8415`). The child already exists before `pty_created`. Input writer, automatic placeholder controller, output drain and native exit-waiter are then started (`8417-8550`). Waiting for child death happens on its separate waiter thread, not the spawn handler. `controller-attach` is not Spawned and does not imply translation startup or table insertion completed.\n6. After printed `waiter_armed` (`8672`), the next substantive startup call is `build_translation` (`8682-8694`). It creates a second process through `TranslationChild::spawn` (`broker.rs:4710`, `translation.rs:247-299`): configure piped stdin/stdout and inherited stderr; call synchronous `Command::spawn` (`266`); extract pipe handles; create a stdout reader thread (`274`). No timeout wraps process creation or thread creation.\n7. `build_translation` next obtains Windows hostname via COMPUTERNAME environment lookup (`hostlabel.rs:24-28,46-51`), then synchronously sends one `ToBinary::Init` (`broker.rs:4714-4717`). `TranslationChild::send` serializes JSON, locks stdin, performs two `write_all` calls and `flush` (`translation.rs:311-320`), with no timeout. The Init error is ignored. At this initial call the child has not yet been shared with the inject worker, so competing startup callers taking this child's stdin mutex are not demonstrated; pipe/OS write delay is the relevant possible wait, not presumed existing-worker mutex contention.\n8. `build_translation` allocates worker state and starts the native inject-worker thread (`broker.rs:4718-4759`), then returns immediately. It does NOT wait for a translation ready response, first stdout command, Init acknowledgement, or inject commit. Missing/unspawnable binary logs `TRANSLATION_SPAWN_FAILED` and returns None (`4760-4765`); that failure ordinarily degrades delivery but continues spawning, rather than returning this timeout directly.\n9. `translation_ready` is emitted (`8696`), then `sessions` is locked and the HostedSession inserted (`8698-8716`); `bringups_in_flight` is locked/cleared (`8723`, helper `8070-8072`); `row_inserted` is emitted (`8728`). Spawned is serialized and sent (`8730-8738`). `send_frame` discards the result of `BrokerConn::write` (`10258-10260`). That write has its own absolute gate-plus-OS-write deadline (`conn.rs:610-686`) whose default is 15s (`broker.rs:226-241`), independently overrideable; it is not capped to this client's remaining 2s.\n\n## Last-phase precision and every remaining wait class\nThe visible `waiter_armed` line localizes an interval, not one syscall. The phase closure prints FIRST (`8113-8116`), then locks `wake_inflight` and updates its claim (`8119-8127`). Therefore the still-unfinished tail of the waiter_armed phase itself is a possible mutex wait before build_translation starts. The next phase also takes stderr's synchronous lock before writing (`emit.rs:187-193`). Absence of translation_ready is therefore consistent with: claim-lock tail; OS/process or thread creation; Init stdin write; scheduling delay anywhere in that interval; or blocking/lost phase emission. Ordinary remaining waits after successful translation are session-table and in-flight-ledger mutexes, phase claim mutexes, logging locks/I/O, and the bounded serialized broker reply write. Those later sites would normally be preceded by translation_ready or row_inserted; they do not naturally explain the supplied last-phase cutoff if the log is complete.\n\n## Budget mismatch proven versus inferred\nMain read fixture lines110-133: `attach_ipc_deadline()` defaults to exactly 2s and accepts `SPT_ATTACH_IPC_DEADLINE_MS`; its comment mentions explicit CI 30000ms for shared-box request_attach starvation. The same helper is reused by spawn_xlate_session. Baseline must remain 2s; silently setting 30s would change the admitted experiment.\n\nSource proves this 2s read budget covers both first PTY setup and second translation-process startup, because Spawned is produced only after both. Main's timings: g1 waiter_armed892ms leaves about1108ms; g7 waiter_armed1328ms leaves about672ms relative to broker phase zero (not perfectly synchronized to the client's post-send clock). Both test bodies finish around2.04s. ConPTY alone had already returned before expiry; 'ConPTY remained stuck' is not the reported state. Accumulated ordinary startup latency exhausting the shared budget remains plausible.\n\nThere is no 2s broker spawn limit. The 120s pty_creating /30s other-phase ceilings (`broker.rs:309-343`) are duplicate-wake wedge detectors, explicitly NOT operation performance budgets and NOT a cancellation timer. They do not protect this request. The 5s inject-commit bound (`622`) is subsequent worker choreography, not startup acknowledgement. The default15s broker write bound only starts when a frame write is attempted. KH7.8's10s broker QUIC-operation timeout belongs to net operations, not this spawn path.\n\n## Correlation, poison, and late replies\n`Envelope` has only protocol_version/kind/payload (`frame.rs:125-145`); `Spawned` has session_id/pid (`msg.rs:560-566`). The spawn wait accepts any Spawned event, with no request-ID matching (`brain.rs:837-845`). Reusing the same Brain after a timed-out spawn could therefore consume a late Spawned as the reply to a subsequent request. That is a real API misuse hazard but not an established cause here: Main describes a newly constructed Brain in each helper and immediate panic on the first failed spawn. A late reply is the consequence of broker overrun, not an explanation for why this first wait lacked its reply.\n\nThe timeout branch does not itself latch an internal Brain poison flag, cancel the broker, or abort the reader. 'Poison' is the caller/supervisor policy described in KH7.6 and the API comments: abandon/restart rather than retry on the same carrier. The dedicated reader retains framing ownership, so caller timeout does not itself abandon a partially read frame. A missing/late complete reply and an incomplete/blocked transport read are indistinguishable to recv_timeout, but invalid decoded payload/framing errors would ordinarily surface a different error. Broker-side connection poison is separate and logged as `CONN_WRITE_POISONED` or `CONN_WRITE_RETIRED` (`conn.rs:331-360`). Pure Spawned-write failure would normally occur after row_inserted, which is absent in both supplied tails.\n\n## Ranked falsifiable explanations\n1. **Accumulated normal Windows startup / scheduling cost exhausts a too-short spawn reply budget.** Strongest fit: first process consumes45%/66% of budget, followed by unbounded synchronous creation of another process; next phase missing; timing ends at the client deadline. Prediction: existing full broker tails from equivalent failures eventually show translation_ready and row_inserted after2s, or an independently admitted longer-observation capture shows that continuation without intervention. A permanent last-phase stall with healthy scheduling falsifies mere slow completion. No claim that a wider timeout is the fix.\n2. **Translation startup specifically wedges or takes exceptional latency, rather than merely accumulating ordinary overhead.** Candidate sites Command::spawn, reader/inject thread creation, or initial stdin write. Prediction: a passive stack/process snapshot captured during the interval identifies CreateProcess/pipe/thread creation or WriteFile, and translation_ready remains absent while unrelated broker requests still progress. A child existing with Init already observed and handler beyond these calls falsifies this branch. Initial small Init to a fresh pipe makes a persistent full-pipe explanation weaker than process creation absent contrary evidence; no ready-handshake wait exists to blame.\n3. **Local shared-lock or log-sink/scheduler stall inside phase boundaries.** waiter_armed printed before wake_inflight locking; phase logging itself is synchronous. Prediction: a passive stack shows claim mutex or stderr lock/write, or correlated phase records across otherwise unrelated sessions stall together. If a complete sink records smooth unrelated phases and stack shows Command::spawn, this explanation loses support. This can share machine-level resource pressure with NetHost without being a NetHost-owned await/deadlock.\n4. **Reply transport/reader scheduling or malformed late frame correlation.** Architecturally possible but poor match to absent translation_ready/row_inserted. Prediction: full logs must first recover row_inserted before timeout, together with relevant same-connection writer retirement, pump-reader failure/starvation, or complete reply delivery to the channel that the caller did not reach. If the broker never exits translation startup before expiry, transport/correlation is downstream and cannot be the primary stall. Cold-started single-call helpers falsify the prior-late-Spawned variant.\n5. **Direct #302 NetHost2 runtime/journal deadlock.** Lowest direct-cause support: the investigated call graph invokes neither NetHost nor EffectJournal. Prediction required for promotion: an actual ownership chain from this spawn handler's blocked resource to a NetHost runtime/journal owner, not just simultaneous net presence. A stalled net reactor counter alone proves net trouble, not spawn causality; translation/process-stack evidence with no shared owner falsifies this specific direct linkage. Shared host CPU/thread/process-creation pressure remains a possible indirect relationship.\n\n## Next useful existing evidence/probe\nFirst consume the complete per-endpoint broker tail, not just the panic window: search the same labels for translation_ready, row_inserted, row_removed, TRANSLATION_SPAWN_FAILED, and PUMP_IPC_READER: exited; correlate CONN_WRITE_POISONED/RETIRED by conn attribution. translation_ready after expiry separates delayed startup from reply delivery; translation_ready before expiry but row_inserted absent moves attention to table/ledger/phase locks; row_inserted before expiry moves attention to connection writer/reader. row_removed before insertion indicates early child exit, not proof that Spawned was sent. None of these existing phase records distinguishes Command::spawn from Init WriteFile. The next discriminating passive probe, if separately authorized, is a timed thread-stack/process snapshot during the waiter_armed→translation_ready interval plus translation-child existence; this assignment performed no attachment or execution. Preserve the baseline2s contract and report any observation-cohort deadline change explicitly.\n\nDomain/ADR reading reinforces the distinction: broker resources survive brain failure (CONTEXT35-39; ADR0018 decisions1/4/5/8), controller ownership is independent of completed process startup (CONTEXT remote-attach model; ADR0044), and authoritative session-table existence precedes a truthful Spawned reply (ADR0041 policies). Neither a controller-attach line nor a client timeout establishes a registered live session or its absence."},"error":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":406,"output":3863,"cacheRead":75008,"cacheWrite":0,"totalTokens":79277,"cost":{"input":0.00406,"output":0.19315000000000002,"cacheRead":0.07500799999999999,"cacheWrite":0,"total":0.272218}},"stopReason":"toolUse","timestamp":1789394651163,"responseId":"resp_0a63d1e8a5b46c9c016aa7fedbc51c87d1bed2389111c2decb","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7fedd823087d1a5741064451e0e8b","type":"function_call","status":"completed","arguments":"{\"type\":null,\"data\":{\"summary\":\"The exact candidate bounds the spawn reply read correctly, but does not bound or cancel the broker's spawn operation. Both reported reds stop in the interval after waiter_armed and before translation_ready, whose substantive work is synchronous translation-process creation and its initial stdin write—not NetHost or journal startup. The 2s fixture budget covers PTY creation plus translation startup, leaving approximately 1.108s (g1) and 0.672s (g7) after waiter_armed. Existing evidence favors accumulated Windows startup latency over a direct #302 runtime/journal deadlock, without identifying the blocked syscall.\",\"files\":[{\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs\",\"description\":\"Pump construction 499-519; spawn request/reply loop 830-851; fixed deadline 1041-1043; Spawned decode 1208-1215; request writes 2555-2561; Split timeout semantics 2582-2621.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs\",\"description\":\"Per-connection synchronous dispatch 6052-6110; dispatch_spawn 6487-6492; phase closure 8112-8128; PTY creation and subscriber setup 8361-8487; waiter_armed 8672; translation startup 4702-4765 and 8682-8694; table insertion/reply 8696-8740.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/translation.rs\",\"description\":\"Translation child Command::spawn and reader-thread creation 247-299; synchronous Init stdin mutex/write_all/flush 311-320. No ready handshake.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs\",\"description\":\"Broker reply writes have their own gate-plus-write deadline 610-686; retirement tokens distinguish deadline poison from organic I/O failure 331-360.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs\",\"description\":\"SpawnReq 498-556 carries neither timeout nor cancellation/request ID; Spawned 560-566 contains session ID and optional PID.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs\",\"description\":\"Envelope 125-145 contains protocol_version, kind, payload—not request/reply correlation ID.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs\",\"description\":\"os_hostname 24-28 uses COMPUTERNAME environment lookup on Windows 46-51; no network lookup or hostname subprocess.\"},{\"path\":\".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs\",\"description\":\"emit_line_err 187-193 synchronously takes stderr's lock, writes the line, and ignores the result; relevant to interpreting the last printed phase.\"},{\"path\":\".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md\",\"description\":\"6.9 at429-432 documents output discarded while waiting for Spawned; 7.6 at501-528 specifies Split and fixed per-call deadlines; 7.8 at538-542 describes separate broker QUIC operation bounds.\"},{\"path\":\".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md\",\"description\":\"Decisions 1,4,5,8 at23-30 define broker resource ownership, IPC boundary, and journal custody; process survival is independent of the requesting brain.\"},{\"path\":\".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md\",\"description\":\"Decisions 4-6 at62-81 define stamp convergence, atomic spawn-policy distinction, and lifecycle authority; fresh creation is not an attach/controller success claim.\"},{\"path\":\".worktrees/297-enforcement-shape/CONTEXT.md\",\"description\":\"Broker/brain ownership at35-39; remote-attach controller/viewer model near403-408.\"}],\"architecture\":\"A Split Brain writes a Spawn envelope, then waits on its reader-thread channel with one absolute reply deadline. A dedicated broker connection handler executes spawn synchronously: gate → PTY → input/controller/drain/waiter → translation process plus Init → session table insertion → bounded broker connection write of Spawned. The request does not propagate its deadline into this operation. NetHost and EffectJournal are broker-owned but absent from this direct spawn/translation-startup call chain.\",\"report\":\"## Scope and evidence status\\nRead-only source investigation of the named candidate worktree; no edits, processes, validation, tests, debugger attachment, or credential reads. Incident timings and fixture contract below are Main-provided evidence, not independently re-executed findings. Navigation used exact-worktree grep/read only; no root-source LSP references were substituted. Main reported that worktree LSP references are empty while root references point to different source, so reference-search completeness must not be inferred from those results.\\n\\n## Precise causal route\\n1. `Brain::cold_start_pump` (`brain.rs:499-519`) connects/handshakes, splits the connection, and stores `Some(io_timeout)`. `split_with_reader` (`375-404`) runs blocking `read_frame` on a dedicated native thread and forwards complete frame results to an unbounded MPSC channel. `PUMP_IPC_READER: spawned` proves thread entry, not successful reading of any reply.\\n2. `spawn_session` (`604-606`) calls `spawn_session_pid` (`830-851`). It sends `KIND_SPAWN` first, then computes `call_deadline()` once (`836`; definition `1041-1043`). Each loop iteration uses the same deadline. Interleaved Output/Exit/other events cannot renew it. The send itself uses synchronous `write_frame` (`2555-2561`) outside that read budget; connect and handshake also precede it. Thus this is a bounded reply-wait, not a complete RPC-duration guarantee.\\n3. `read_frame_until` (`2582-2621`) on Split calls `frames.recv_timeout(deadline.saturating_duration_since(now))`. Empty-at-expiry returns `TimedOut` with the exact string `brain IPC read deadline elapsed` (`2605-2607`). Whole plus Some(deadline) instead returns Unsupported (`2586-2594`), excluding silent Whole-carrier unbounded-read behavior for this reported error. One nuance: zero remaining time still drains already-queued frames; there is no absolute rejection of frames queued at/after the boundary. Therefore the code bounds waiting, not all possible CPU time spent draining an indefinitely nonempty queue.\\n4. `Broker::handle_conn` (`6052-6110`) reads the request, synchronously calls `dispatch_spawn` on that connection's handler (`6092-6095`), then records the successful session subscription. `dispatch_spawn` decodes and calls `dispatch_spawn_policy` (`6487-6492`). There is no spawn operation deadline or cancellation token in `SpawnReq` (`msg.rs:498-556`). A client read timeout does not interrupt this dispatch; handler EOF cleanup is not reached until dispatch returns and the read loop observes failure.\\n5. The reported phases establish that the gate was claimed immediately and PTY creation plus birth-identity sampling completed (`broker.rs:8391-8415`). The child already exists before `pty_created`. Input writer, automatic placeholder controller, output drain and native exit-waiter are then started (`8417-8550`). Waiting for child death happens on its separate waiter thread, not the spawn handler. `controller-attach` is not Spawned and does not imply translation startup or table insertion completed.\\n6. After printed `waiter_armed` (`8672`), the next substantive startup call is `build_translation` (`8682-8694`). It creates a second process through `TranslationChild::spawn` (`broker.rs:4710`, `translation.rs:247-299`): configure piped stdin/stdout and inherited stderr; call synchronous `Command::spawn` (`266`); extract pipe handles; create a stdout reader thread (`274`). No timeout wraps process creation or thread creation.\\n7. `build_translation` next obtains Windows hostname via COMPUTERNAME environment lookup (`hostlabel.rs:24-28,46-51`), then synchronously sends one `ToBinary::Init` (`broker.rs:4714-4717`). `TranslationChild::send` serializes JSON, locks stdin, performs two `write_all` calls and `flush` (`translation.rs:311-320`), with no timeout. The Init error is ignored. At this initial call the child has not yet been shared with the inject worker, so competing startup callers taking this child's stdin mutex are not demonstrated; pipe/OS write delay is the relevant possible wait, not presumed existing-worker mutex contention.\\n8. `build_translation` allocates worker state and starts the native inject-worker thread (`broker.rs:4718-4759`), then returns immediately. It does NOT wait for a translation ready response, first stdout command, Init acknowledgement, or inject commit. Missing/unspawnable binary logs `TRANSLATION_SPAWN_FAILED` and returns None (`4760-4765`); that failure ordinarily degrades delivery but continues spawning, rather than returning this timeout directly.\\n9. `translation_ready` is emitted (`8696`), then `sessions` is locked and the HostedSession inserted (`8698-8716`); `bringups_in_flight` is locked/cleared (`8723`, helper `8070-8072`); `row_inserted` is emitted (`8728`). Spawned is serialized and sent (`8730-8738`). `send_frame` discards the result of `BrokerConn::write` (`10258-10260`). That write has its own absolute gate-plus-OS-write deadline (`conn.rs:610-686`) whose default is 15s (`broker.rs:226-241`), independently overrideable; it is not capped to this client's remaining 2s.\\n\\n## Last-phase precision and every remaining wait class\\nThe visible `waiter_armed` line localizes an interval, not one syscall. The phase closure prints FIRST (`8113-8116`), then locks `wake_inflight` and updates its claim (`8119-8127`). Therefore the still-unfinished tail of the waiter_armed phase itself is a possible mutex wait before build_translation starts. The next phase also takes stderr's synchronous lock before writing (`emit.rs:187-193`). Absence of translation_ready is therefore consistent with: claim-lock tail; OS/process or thread creation; Init stdin write; scheduling delay anywhere in that interval; or blocking/lost phase emission. Ordinary remaining waits after successful translation are session-table and in-flight-ledger mutexes, phase claim mutexes, logging locks/I/O, and the bounded serialized broker reply write. Those later sites would normally be preceded by translation_ready or row_inserted; they do not naturally explain the supplied last-phase cutoff if the log is complete.\\n\\n## Budget mismatch proven versus inferred\\nMain read fixture lines110-133: `attach_ipc_deadline()` defaults to exactly 2s and accepts `SPT_ATTACH_IPC_DEADLINE_MS`; its comment mentions explicit CI 30000ms for shared-box request_attach starvation. The same helper is reused by spawn_xlate_session. Baseline must remain 2s; silently setting 30s would change the admitted experiment.\\n\\nSource proves this 2s read budget covers both first PTY setup and second translation-process startup, because Spawned is produced only after both. Main's timings: g1 waiter_armed892ms leaves about1108ms; g7 waiter_armed1328ms leaves about672ms relative to broker phase zero (not perfectly synchronized to the client's post-send clock). Both test bodies finish around2.04s. ConPTY alone had already returned before expiry; 'ConPTY remained stuck' is not the reported state. Accumulated ordinary startup latency exhausting the shared budget remains plausible.\\n\\nThere is no 2s broker spawn limit. The 120s pty_creating /30s other-phase ceilings (`broker.rs:309-343`) are duplicate-wake wedge detectors, explicitly NOT operation performance budgets and NOT a cancellation timer. They do not protect this request. The 5s inject-commit bound (`622`) is subsequent worker choreography, not startup acknowledgement. The default15s broker write bound only starts when a frame write is attempted. KH7.8's10s broker QUIC-operation timeout belongs to net operations, not this spawn path.\\n\\n## Correlation, poison, and late replies\\n`Envelope` has only protocol_version/kind/payload (`frame.rs:125-145`); `Spawned` has session_id/pid (`msg.rs:560-566`). The spawn wait accepts any Spawned event, with no request-ID matching (`brain.rs:837-845`). Reusing the same Brain after a timed-out spawn could therefore consume a late Spawned as the reply to a subsequent request. That is a real API misuse hazard but not an established cause here: Main describes a newly constructed Brain in each helper and immediate panic on the first failed spawn. A late reply is the consequence of broker overrun, not an explanation for why this first wait lacked its reply.\\n\\nThe timeout branch does not itself latch an internal Brain poison flag, cancel the broker, or abort the reader. 'Poison' is the caller/supervisor policy described in KH7.6 and the API comments: abandon/restart rather than retry on the same carrier. The dedicated reader retains framing ownership, so caller timeout does not itself abandon a partially read frame. A missing/late complete reply and an incomplete/blocked transport read are indistinguishable to recv_timeout, but invalid decoded payload/framing errors would ordinarily surface a different error. Broker-side connection poison is separate and logged as `CONN_WRITE_POISONED` or `CONN_WRITE_RETIRED` (`conn.rs:331-360`). Pure Spawned-write failure would normally occur after row_inserted, which is absent in both supplied tails.\\n\\n## Ranked falsifiable explanations\\n1. **Accumulated normal Windows startup / scheduling cost exhausts a too-short spawn reply budget.** Strongest fit: first process consumes45%/66% of budget, followed by unbounded synchronous creation of another process; next phase missing; timing ends at the client deadline. Prediction: existing full broker tails from equivalent failures eventually show translation_ready and row_inserted after2s, or an independently admitted longer-observation capture shows that continuation without intervention. A permanent last-phase stall with healthy scheduling falsifies mere slow completion. No claim that a wider timeout is the fix.\\n2. **Translation startup specifically wedges or takes exceptional latency, rather than merely accumulating ordinary overhead.** Candidate sites Command::spawn, reader/inject thread creation, or initial stdin write. Prediction: a passive stack/process snapshot captured during the interval identifies CreateProcess/pipe/thread creation or WriteFile, and translation_ready remains absent while unrelated broker requests still progress. A child existing with Init already observed and handler beyond these calls falsifies this branch. Initial small Init to a fresh pipe makes a persistent full-pipe explanation weaker than process creation absent contrary evidence; no ready-handshake wait exists to blame.\\n3. **Local shared-lock or log-sink/scheduler stall inside phase boundaries.** waiter_armed printed before wake_inflight locking; phase logging itself is synchronous. Prediction: a passive stack shows claim mutex or stderr lock/write, or correlated phase records across otherwise unrelated sessions stall together. If a complete sink records smooth unrelated phases and stack shows Command::spawn, this explanation loses support. This can share machine-level resource pressure with NetHost without being a NetHost-owned await/deadlock.\\n4. **Reply transport/reader scheduling or malformed late frame correlation.** Architecturally possible but poor match to absent translation_ready/row_inserted. Prediction: full logs must first recover row_inserted before timeout, together with relevant same-connection writer retirement, pump-reader failure/starvation, or complete reply delivery to the channel that the caller did not reach. If the broker never exits translation startup before expiry, transport/correlation is downstream and cannot be the primary stall. Cold-started single-call helpers falsify the prior-late-Spawned variant.\\n5. **Direct #302 NetHost2 runtime/journal deadlock.** Lowest direct-cause support: the investigated call graph invokes neither NetHost nor EffectJournal. Prediction required for promotion: an actual ownership chain from this spawn handler's blocked resource to a NetHost runtime/journal owner, not just simultaneous net presence. A stalled net reactor counter alone proves net trouble, not spawn causality; translation/process-stack evidence with no shared owner falsifies this specific direct linkage. Shared host CPU/thread/process-creation pressure remains a possible indirect relationship.\\n\\n## Next useful existing evidence/probe\\nFirst consume the complete per-endpoint broker tail, not just the panic window: search the same labels for translation_ready, row_inserted, row_removed, TRANSLATION_SPAWN_FAILED, and PUMP_IPC_READER: exited; correlate CONN_WRITE_POISONED/RETIRED by conn attribution. translation_ready after expiry separates delayed startup from reply delivery; translation_ready before expiry but row_inserted absent moves attention to table/ledger/phase locks; row_inserted before expiry moves attention to connection writer/reader. row_removed before insertion indicates early child exit, not proof that Spawned was sent. None of these existing phase records distinguishes Command::spawn from Init WriteFile. The next discriminating passive probe, if separately authorized, is a timed thread-stack/process snapshot during the waiter_armed→translation_ready interval plus translation-child existence; this assignment performed no attachment or execution. Preserve the baseline2s contract and report any observation-cohort deadline change explicitly.\\n\\nDomain/ADR reading reinforces the distinction: broker resources survive brain failure (CONTEXT35-39; ADR0018 decisions1/4/5/8), controller ownership is independent of completed process startup (CONTEXT remote-attach model; ADR0044), and authoritative session-table existence precedes a truthful Spawned reply (ADR0041 policies). Neither a controller-attach line nor a client timeout establishes a registered live session or its absence.\"},\"error\":null}","call_id":"call_DoRQUUtyWEaxZ7Cdz4Sj3XIy","internal_chat_message_metadata_passthrough":{"create_time":1789394651.996935,"turn_id":"fe25fe33-abd7-4906-9792-91e060e64ee7"},"name":"yield","metadata":{"turn_id":"fe25fe33-abd7-4906-9792-91e060e64ee7"}}]},"duration":118026.98620000482,"ttft":2368.739500001073,"completedAt":1789394769191,"contextSnapshot":{"promptTokens":75414,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"aeb4016f","parentId":"567da017","timestamp":"2026-09-14T14:06:09.197Z","message":{"role":"toolResult","toolCallId":"call_DoRQUUtyWEaxZ7Cdz4Sj3XIy|fc_0a63d1e8a5b46c9c016aa7fedd823087d1a5741064451e0e8b","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"The exact candidate bounds the spawn reply read correctly, but does not bound or cancel the broker's spawn operation. Both reported reds stop in the interval after waiter_armed and before translation_ready, whose substantive work is synchronous translation-process creation and its initial stdin write—not NetHost or journal startup. The 2s fixture budget covers PTY creation plus translation startup, leaving approximately 1.108s (g1) and 0.672s (g7) after waiter_armed. Existing evidence favors accumulated Windows startup latency over a direct #302 runtime/journal deadlock, without identifying the blocked syscall.","files":[{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/brain.rs","description":"Pump construction 499-519; spawn request/reply loop 830-851; fixed deadline 1041-1043; Spawned decode 1208-1215; request writes 2555-2561; Split timeout semantics 2582-2621."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/broker.rs","description":"Per-connection synchronous dispatch 6052-6110; dispatch_spawn 6487-6492; phase closure 8112-8128; PTY creation and subscriber setup 8361-8487; waiter_armed 8672; translation startup 4702-4765 and 8682-8694; table insertion/reply 8696-8740."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/translation.rs","description":"Translation child Command::spawn and reader-thread creation 247-299; synchronous Init stdin mutex/write_all/flush 311-320. No ready handshake."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/conn.rs","description":"Broker reply writes have their own gate-plus-write deadline 610-686; retirement tokens distinguish deadline poison from organic I/O failure 331-360."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/msg.rs","description":"SpawnReq 498-556 carries neither timeout nor cancellation/request ID; Spawned 560-566 contains session ID and optional PID."},{"path":".worktrees/297-enforcement-shape/crates/spt-daemon/src/frame.rs","description":"Envelope 125-145 contains protocol_version, kind, payload—not request/reply correlation ID."},{"path":".worktrees/297-enforcement-shape/crates/spt-store/src/hostlabel.rs","description":"os_hostname 24-28 uses COMPUTERNAME environment lookup on Windows 46-51; no network lookup or hostname subprocess."},{"path":".worktrees/297-enforcement-shape/crates/spt-proto/src/emit.rs","description":"emit_line_err 187-193 synchronously takes stderr's lock, writes the line, and ignores the result; relevant to interpreting the last printed phase."},{"path":".worktrees/297-enforcement-shape/docs/KNOWN-HAZARDS.md","description":"6.9 at429-432 documents output discarded while waiting for Spawned; 7.6 at501-528 specifies Split and fixed per-call deadlines; 7.8 at538-542 describes separate broker QUIC operation bounds."},{"path":".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md","description":"Decisions 1,4,5,8 at23-30 define broker resource ownership, IPC boundary, and journal custody; process survival is independent of the requesting brain."},{"path":".worktrees/297-enforcement-shape/docs/adr/0041-endpoint-lifecycle-truth.md","description":"Decisions 4-6 at62-81 define stamp convergence, atomic spawn-policy distinction, and lifecycle authority; fresh creation is not an attach/controller success claim."},{"path":".worktrees/297-enforcement-shape/CONTEXT.md","description":"Broker/brain ownership at35-39; remote-attach controller/viewer model near403-408."}],"architecture":"A Split Brain writes a Spawn envelope, then waits on its reader-thread channel with one absolute reply deadline. A dedicated broker connection handler executes spawn synchronously: gate → PTY → input/controller/drain/waiter → translation process plus Init → session table insertion → bounded broker connection write of Spawned. The request does not propagate its deadline into this operation. NetHost and EffectJournal are broker-owned but absent from this direct spawn/translation-startup call chain.","report":"## Scope and evidence status\nRead-only source investigation of the named candidate worktree; no edits, processes, validation, tests, debugger attachment, or credential reads. Incident timings and fixture contract below are Main-provided evidence, not independently re-executed findings. Navigation used exact-worktree grep/read only; no root-source LSP references were substituted. Main reported that worktree LSP references are empty while root references point to different source, so reference-search completeness must not be inferred from those results.\n\n## Precise causal route\n1. `Brain::cold_start_pump` (`brain.rs:499-519`) connects/handshakes, splits the connection, and stores `Some(io_timeout)`. `split_with_reader` (`375-404`) runs blocking `read_frame` on a dedicated native thread and forwards complete frame results to an unbounded MPSC channel. `PUMP_IPC_READER: spawned` proves thread entry, not successful reading of any reply.\n2. `spawn_session` (`604-606`) calls `spawn_session_pid` (`830-851`). It sends `KIND_SPAWN` first, then computes `call_deadline()` once (`836`; definition `1041-1043`). Each loop iteration uses the same deadline. Interleaved Output/Exit/other events cannot renew it. The send itself uses synchronous `write_frame` (`2555-2561`) outside that read budget; connect and handshake also precede it. Thus this is a bounded reply-wait, not a complete RPC-duration guarantee.\n3. `read_frame_until` (`2582-2621`) on Split calls `frames.recv_timeout(deadline.saturating_duration_since(now))`. Empty-at-expiry returns `TimedOut` with the exact string `brain IPC read deadline elapsed` (`2605-2607`). Whole plus Some(deadline) instead returns Unsupported (`2586-2594`), excluding silent Whole-carrier unbounded-read behavior for this reported error. One nuance: zero remaining time still drains already-queued frames; there is no absolute rejection of frames queued at/after the boundary. Therefore the code bounds waiting, not all possible CPU time spent draining an indefinitely nonempty queue.\n4. `Broker::handle_conn` (`6052-6110`) reads the request, synchronously calls `dispatch_spawn` on that connection's handler (`6092-6095`), then records the successful session subscription. `dispatch_spawn` decodes and calls `dispatch_spawn_policy` (`6487-6492`). There is no spawn operation deadline or cancellation token in `SpawnReq` (`msg.rs:498-556`). A client read timeout does not interrupt this dispatch; handler EOF cleanup is not reached until dispatch returns and the read loop observes failure.\n5. The reported phases establish that the gate was claimed immediately and PTY creation plus birth-identity sampling completed (`broker.rs:8391-8415`). The child already exists before `pty_created`. Input writer, automatic placeholder controller, output drain and native exit-waiter are then started (`8417-8550`). Waiting for child death happens on its separate waiter thread, not the spawn handler. `controller-attach` is not Spawned and does not imply translation startup or table insertion completed.\n6. After printed `waiter_armed` (`8672`), the next substantive startup call is `build_translation` (`8682-8694`). It creates a second process through `TranslationChild::spawn` (`broker.rs:4710`, `translation.rs:247-299`): configure piped stdin/stdout and inherited stderr; call synchronous `Command::spawn` (`266`); extract pipe handles; create a stdout reader thread (`274`). No timeout wraps process creation or thread creation.\n7. `build_translation` next obtains Windows hostname via COMPUTERNAME environment lookup (`hostlabel.rs:24-28,46-51`), then synchronously sends one `ToBinary::Init` (`broker.rs:4714-4717`). `TranslationChild::send` serializes JSON, locks stdin, performs two `write_all` calls and `flush` (`translation.rs:311-320`), with no timeout. The Init error is ignored. At this initial call the child has not yet been shared with the inject worker, so competing startup callers taking this child's stdin mutex are not demonstrated; pipe/OS write delay is the relevant possible wait, not presumed existing-worker mutex contention.\n8. `build_translation` allocates worker state and starts the native inject-worker thread (`broker.rs:4718-4759`), then returns immediately. It does NOT wait for a translation ready response, first stdout command, Init acknowledgement, or inject commit. Missing/unspawnable binary logs `TRANSLATION_SPAWN_FAILED` and returns None (`4760-4765`); that failure ordinarily degrades delivery but continues spawning, rather than returning this timeout directly.\n9. `translation_ready` is emitted (`8696`), then `sessions` is locked and the HostedSession inserted (`8698-8716`); `bringups_in_flight` is locked/cleared (`8723`, helper `8070-8072`); `row_inserted` is emitted (`8728`). Spawned is serialized and sent (`8730-8738`). `send_frame` discards the result of `BrokerConn::write` (`10258-10260`). That write has its own absolute gate-plus-OS-write deadline (`conn.rs:610-686`) whose default is 15s (`broker.rs:226-241`), independently overrideable; it is not capped to this client's remaining 2s.\n\n## Last-phase precision and every remaining wait class\nThe visible `waiter_armed` line localizes an interval, not one syscall. The phase closure prints FIRST (`8113-8116`), then locks `wake_inflight` and updates its claim (`8119-8127`). Therefore the still-unfinished tail of the waiter_armed phase itself is a possible mutex wait before build_translation starts. The next phase also takes stderr's synchronous lock before writing (`emit.rs:187-193`). Absence of translation_ready is therefore consistent with: claim-lock tail; OS/process or thread creation; Init stdin write; scheduling delay anywhere in that interval; or blocking/lost phase emission. Ordinary remaining waits after successful translation are session-table and in-flight-ledger mutexes, phase claim mutexes, logging locks/I/O, and the bounded serialized broker reply write. Those later sites would normally be preceded by translation_ready or row_inserted; they do not naturally explain the supplied last-phase cutoff if the log is complete.\n\n## Budget mismatch proven versus inferred\nMain read fixture lines110-133: `attach_ipc_deadline()` defaults to exactly 2s and accepts `SPT_ATTACH_IPC_DEADLINE_MS`; its comment mentions explicit CI 30000ms for shared-box request_attach starvation. The same helper is reused by spawn_xlate_session. Baseline must remain 2s; silently setting 30s would change the admitted experiment.\n\nSource proves this 2s read budget covers both first PTY setup and second translation-process startup, because Spawned is produced only after both. Main's timings: g1 waiter_armed892ms leaves about1108ms; g7 waiter_armed1328ms leaves about672ms relative to broker phase zero (not perfectly synchronized to the client's post-send clock). Both test bodies finish around2.04s. ConPTY alone had already returned before expiry; 'ConPTY remained stuck' is not the reported state. Accumulated ordinary startup latency exhausting the shared budget remains plausible.\n\nThere is no 2s broker spawn limit. The 120s pty_creating /30s other-phase ceilings (`broker.rs:309-343`) are duplicate-wake wedge detectors, explicitly NOT operation performance budgets and NOT a cancellation timer. They do not protect this request. The 5s inject-commit bound (`622`) is subsequent worker choreography, not startup acknowledgement. The default15s broker write bound only starts when a frame write is attempted. KH7.8's10s broker QUIC-operation timeout belongs to net operations, not this spawn path.\n\n## Correlation, poison, and late replies\n`Envelope` has only protocol_version/kind/payload (`frame.rs:125-145`); `Spawned` has session_id/pid (`msg.rs:560-566`). The spawn wait accepts any Spawned event, with no request-ID matching (`brain.rs:837-845`). Reusing the same Brain after a timed-out spawn could therefore consume a late Spawned as the reply to a subsequent request. That is a real API misuse hazard but not an established cause here: Main describes a newly constructed Brain in each helper and immediate panic on the first failed spawn. A late reply is the consequence of broker overrun, not an explanation for why this first wait lacked its reply.\n\nThe timeout branch does not itself latch an internal Brain poison flag, cancel the broker, or abort the reader. 'Poison' is the caller/supervisor policy described in KH7.6 and the API comments: abandon/restart rather than retry on the same carrier. The dedicated reader retains framing ownership, so caller timeout does not itself abandon a partially read frame. A missing/late complete reply and an incomplete/blocked transport read are indistinguishable to recv_timeout, but invalid decoded payload/framing errors would ordinarily surface a different error. Broker-side connection poison is separate and logged as `CONN_WRITE_POISONED` or `CONN_WRITE_RETIRED` (`conn.rs:331-360`). Pure Spawned-write failure would normally occur after row_inserted, which is absent in both supplied tails.\n\n## Ranked falsifiable explanations\n1. **Accumulated normal Windows startup / scheduling cost exhausts a too-short spawn reply budget.** Strongest fit: first process consumes45%/66% of budget, followed by unbounded synchronous creation of another process; next phase missing; timing ends at the client deadline. Prediction: existing full broker tails from equivalent failures eventually show translation_ready and row_inserted after2s, or an independently admitted longer-observation capture shows that continuation without intervention. A permanent last-phase stall with healthy scheduling falsifies mere slow completion. No claim that a wider timeout is the fix.\n2. **Translation startup specifically wedges or takes exceptional latency, rather than merely accumulating ordinary overhead.** Candidate sites Command::spawn, reader/inject thread creation, or initial stdin write. Prediction: a passive stack/process snapshot captured during the interval identifies CreateProcess/pipe/thread creation or WriteFile, and translation_ready remains absent while unrelated broker requests still progress. A child existing with Init already observed and handler beyond these calls falsifies this branch. Initial small Init to a fresh pipe makes a persistent full-pipe explanation weaker than process creation absent contrary evidence; no ready-handshake wait exists to blame.\n3. **Local shared-lock or log-sink/scheduler stall inside phase boundaries.** waiter_armed printed before wake_inflight locking; phase logging itself is synchronous. Prediction: a passive stack shows claim mutex or stderr lock/write, or correlated phase records across otherwise unrelated sessions stall together. If a complete sink records smooth unrelated phases and stack shows Command::spawn, this explanation loses support. This can share machine-level resource pressure with NetHost without being a NetHost-owned await/deadlock.\n4. **Reply transport/reader scheduling or malformed late frame correlation.** Architecturally possible but poor match to absent translation_ready/row_inserted. Prediction: full logs must first recover row_inserted before timeout, together with relevant same-connection writer retirement, pump-reader failure/starvation, or complete reply delivery to the channel that the caller did not reach. If the broker never exits translation startup before expiry, transport/correlation is downstream and cannot be the primary stall. Cold-started single-call helpers falsify the prior-late-Spawned variant.\n5. **Direct #302 NetHost2 runtime/journal deadlock.** Lowest direct-cause support: the investigated call graph invokes neither NetHost nor EffectJournal. Prediction required for promotion: an actual ownership chain from this spawn handler's blocked resource to a NetHost runtime/journal owner, not just simultaneous net presence. A stalled net reactor counter alone proves net trouble, not spawn causality; translation/process-stack evidence with no shared owner falsifies this specific direct linkage. Shared host CPU/thread/process-creation pressure remains a possible indirect relationship.\n\n## Next useful existing evidence/probe\nFirst consume the complete per-endpoint broker tail, not just the panic window: search the same labels for translation_ready, row_inserted, row_removed, TRANSLATION_SPAWN_FAILED, and PUMP_IPC_READER: exited; correlate CONN_WRITE_POISONED/RETIRED by conn attribution. translation_ready after expiry separates delayed startup from reply delivery; translation_ready before expiry but row_inserted absent moves attention to table/ledger/phase locks; row_inserted before expiry moves attention to connection writer/reader. row_removed before insertion indicates early child exit, not proof that Spawned was sent. None of these existing phase records distinguishes Command::spawn from Init WriteFile. The next discriminating passive probe, if separately authorized, is a timed thread-stack/process snapshot during the waiter_armed→translation_ready interval plus translation-child existence; this assignment performed no attachment or execution. Preserve the baseline2s contract and report any observation-cohort deadline change explicitly.\n\nDomain/ADR reading reinforces the distinction: broker resources survive brain failure (CONTEXT35-39; ADR0018 decisions1/4/5/8), controller ownership is independent of completed process startup (CONTEXT remote-attach model; ADR0044), and authoritative session-table existence precedes a truthful Spawned reply (ADR0041 policies). Neither a controller-attach line nor a client timeout establishes a registered live session or its absence."},"status":"success"},"isError":false,"timestamp":1789394769193}}
{"type":"message","id":"d7a1259b","parentId":"aeb4016f","timestamp":"2026-09-14T14:06:09.205Z","message":{"role":"user","content":"Current interruptible wait interrupted: IRC message from parent agent `Main`.\n\nParent IRC message:\n\nConfirmed exact fixture126-133 default Duration::from_secs(2), parses SPT_ATTACH_IPC_DEADLINE_MS override;110-125 commentsCI30000 for sharedbox request_attach. Consumer scrub removedoverride. Thank you for phase print-before-wake_inflight caveat; include it.","attribution":"agent","timestamp":1789394651300,"steering":true}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_oHx2dtpbXx1hjKuJzcFsWFs4|fc_0a63d1e8a5b46c9c016aa7ff54ab0c87d1952e982eeb5bae90","toolName":"hub","startedAt":"2026-09-14T14:06:15.448Z","intent":"Confirming incorporated budget evidence"},"id":"400501ff","parentId":"d7a1259b","timestamp":"2026-09-14T14:06:15.448Z"}
{"type":"message","id":"7cc00f09","parentId":"400501ff","timestamp":"2026-09-14T14:06:15.449Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_oHx2dtpbXx1hjKuJzcFsWFs4|fc_0a63d1e8a5b46c9c016aa7ff54ab0c87d1952e982eeb5bae90","name":"hub","arguments":{"i":"Confirming incorporated budget evidence","op":"send","to":"Main","message":"Included confirmed2s default/override scrub, CI30000 comment scope, and print-before-wake_inflight caveat in terminal structured report. Report also distinguishes no internal Brain poison/cancel from caller poison policy; late reply can miscorrelate only on reused same carrier, not evidenced in these cold-start first-call reds."},"intent":"Confirming incorporated budget evidence"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":4143,"output":99,"cacheRead":75264,"cacheWrite":0,"totalTokens":79506,"cost":{"input":0.04143,"output":0.00495,"cacheRead":0.075264,"cacheWrite":0,"total":0.121644}},"stopReason":"toolUse","timestamp":1789394769203,"responseId":"resp_0a63d1e8a5b46c9c016aa7ff518c7c87d19a23a1b70cb50317","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0a63d1e8a5b46c9c016aa7ff54ab0c87d1952e982eeb5bae90","type":"function_call","status":"completed","arguments":"{\"i\":\"Confirming incorporated budget evidence\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Included confirmed2s default/override scrub, CI30000 comment scope, and print-before-wake_inflight caveat in terminal structured report. Report also distinguishes no internal Brain poison/cancel from caller poison policy; late reply can miscorrelate only on reused same carrier, not evidenced in these cold-start first-call reds.\"}","call_id":"call_oHx2dtpbXx1hjKuJzcFsWFs4","internal_chat_message_metadata_passthrough":{"create_time":1789394769.860843,"turn_id":"420c74df-c1a7-401b-81d6-98f5cf1f3215"},"name":"hub","metadata":{"turn_id":"420c74df-c1a7-401b-81d6-98f5cf1f3215"}}]},"duration":6243.738299995661,"ttft":3485.2865999937057,"completedAt":1789394775448,"contextSnapshot":{"promptTokens":79407,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"message","id":"523e1322","parentId":"7cc00f09","timestamp":"2026-09-14T14:06:15.449Z","message":{"role":"toolResult","toolCallId":"call_oHx2dtpbXx1hjKuJzcFsWFs4|fc_0a63d1e8a5b46c9c016aa7ff54ab0c87d1952e982eeb5bae90","toolName":"hub","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"SpawnReplyTrace","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1789394775449}}
{"type":"message","id":"d82ea9c6","parentId":"523e1322","timestamp":"2026-09-14T14:06:21.464Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**No further work remains.**"},{"type":"text","text":"Completed structured report submitted. Confirmed 2s default and override scrub are incorporated, including the `waiter_armed` print-before-lock caveat.","textSignature":"{\"v\":1,\"id\":\"msg_0a63d1e8a5b46c9c016aa7ff5d1c1087d1abfb88f4eb93cf12\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":296,"output":47,"cacheRead":79232,"cacheWrite":0,"totalTokens":79575,"reasoningTokens":10,"cost":{"input":0.0029600000000000004,"output":0.00235,"cacheRead":0.079232,"cacheWrite":0,"total":0.08454199999999999}},"stopReason":"stop","timestamp":1789394775458,"responseId":"resp_0a63d1e8a5b46c9c016aa7ff57a9d887d193efe90f803126be","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0a63d1e8a5b46c9c016aa7ff5b39e487d194b695393d2d7689","type":"reasoning","content":[],"encrypted_content":"gAAAAABqp_9dLVFvh5QGLtBCvti_fwlWUj-GY02eHKju7ys2LHb2Hryb1LwPYeYQ5nBZUv7NdI72RBvjVOnqR_MDdNSxbSsodT8GnvlSbTInhAFDIeLx7BL2qE8UDOfRuos8wCgyjeYz-4nVdT1RMT7fKijr38ErY2hkKaj0vvt_l5zYPVyCzkn0iGLMKTaIZGYJ1QBNvNMElE_1xwEhrhEYPrT-gpLQb-C-WxuxlDQ06ky0O3HQRmz0NjxSlAdKuIOHu7yCu9AFng67TNf6mkqoovzdF-Zv-JM8YZWDX6f6yWcpZsL_bexNCFvkVYySVMIGGJi_4swYRNDvLecfNAWCPjyXiZywMvdJfW3fq6I2sT09LEOrr7gdsw5YTMTwYKpAJRKGL0XLrn3hN7mx_wosIFtPDNxyGTgkqbvLQT3XuQFtUmuwP7Lg9V6wqXtuMpvO0RrNpkwX6VPFKmwpOt2JfnhgVIcMgHVMVqQGgNjLPiyUyCWIwAGObzffspkff4mj0BPWAthII07WBKJTbjADhf9d5-36ipPL6cL3y6V6ScWhTHMt_SKZVGok1s5rtOkNEkcK-_yn63FPuvLEO4lgY79P31a0fzU-aao3BvmcJkESvM-lFG2eQPn4BGY_YZWS0b5mysBd5KxqQXiWGxABZH8uitoRzwsGSs89Qev4dKlzS_KNTs2Y4bfNrpTgQ7lkK5_CFEShi_Rv9r4fb2a7ADPBjpCNRppxyY3IArmseyT2fPjdb4AVE_txsDaWB-0ECsf-QUEOKb4OtbmW8calZH3lJ8jyladgKFB_ApownHyhTj1AARIi5yLfQ6u4nDGOeSIgDMl9W-SDePQg5EIcP-t2dkUIgOufwn6PIY-PepBXmphVXPC_cAOHRDf39CyXekD1NWmjFnjhnVUrdgoGVURfhdACuZZN4hNEmbMSn0kkqllNMkUpP-DUC5FB2mZJzX-xbuXL7-pw-zIxE4b7E-5zkB7GkKBGi9XeJfEvrYeDe9VFtVdCgOrzZ9h7msPC3ciynisT-SF0JjmrOWTitt6mnMP6Wk5xkukrZ0B6xhNrUIS9x_DOEqR9NwCj0FACCq1t7ekTKxl6oREdUR1CNqQqInb7lIomJliAQA5-FEHdGlLkRY60xKx9SX0AorHTEgLCeZ8waaH55kUEBYGjz5oNYTQAUiixHulHn0Ni4NjmQp7SjT67BeD3gwfV-jhEi0AxwRZPSOUx8yuxJRMW8Q25CbBXpslWAiDL08SU4H2wX7CI_ezC4R4TUp_zmFKzMspYr7qXfuR8aeI_hJhF8UMZ6igR7qQn3WXrMuALgvVQJEWPP2s=","internal_chat_message_metadata_passthrough":{"turn_id":"420c74df-c1a7-401b-81d6-98f5cf1f3215"},"summary":[{"type":"summary_text","text":"**No further work remains.**"}],"metadata":{"turn_id":"420c74df-c1a7-401b-81d6-98f5cf1f3215"}},{"id":"msg_0a63d1e8a5b46c9c016aa7ff5d1c1087d1abfb88f4eb93cf12","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Completed structured report submitted. Confirmed 2s default and override scrub are incorporated, including the `waiter_armed` print-before-lock caveat."}],"internal_chat_message_metadata_passthrough":{"create_time":1789394775.839273,"turn_id":"420c74df-c1a7-401b-81d6-98f5cf1f3215"},"phase":"final_answer","role":"assistant","metadata":{"turn_id":"420c74df-c1a7-401b-81d6-98f5cf1f3215"}}]},"duration":6005.079300001264,"ttft":3797.6720000058413,"completedAt":1789394781464,"contextSnapshot":{"promptTokens":79528,"nonMessageTokens":10601,"compactionEpoch":0}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-09-14T14:13:09.230Z"},"id":"71dc9efe","parentId":"d82ea9c6","timestamp":"2026-09-14T14:13:09.230Z"}
