{"type":"title","v":1,"title":"","updatedAt":"2026-09-16T01:41:03.187Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"01a0a7df-e452-7000-8fd1-99eae467b9dc","timestamp":"2026-09-16T01:41:03.187Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"f82b90be","parentId":null,"timestamp":"2026-09-16T01:41:04.052Z","model":"openai-codex/gpt-6-astra","resolvedModelIsFallback":false}
{"type":"thinking_level_change","id":"7accd031","parentId":"f82b90be","timestamp":"2026-09-16T01:41:04.053Z","thinkingLevel":"xhigh","configured":null}
{"type":"session_init","id":"61733ba1","parentId":"7accd031","timestamp":"2026-09-16T01:41:04.056Z","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- Bash: `bash`\n- Web Search: `web_search`\n- Submit Result: `yield`\n- Task: `task`\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- User says `parallel` or `parallelize` → MUST use `task` subagents; parallel tool calls insufficient.\n\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- `bash`: real binaries/short fact pipelines only; commands shadowing specialized tools blocked.\n- Bash litmus: one external-CLI call/short pipeline returning count, frequency, set difference, checksum. For merely moving, paging, trimming fetchable bytes: tool.\n\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# Delegation\nInline first. Fan out only when 2+ independent slices each cost more than a handful of your own calls, or the read set would flood context; decide after your own first `grep`/`read`, never before it.\n- NEVER open with a scout. Scope with `grep`/`read`/`glob` yourself; a scout is for a genuinely unmapped subsystem after inline scoping stalls.\n- NEVER delegate one slice. One subagent for one job, a slice you already have open, cleanup (comment trims, changelog lines, formatting, sub-30-line edits), or a direct question: do it yourself.\n- NEVER babysit. Spawn → keep working → read the result. Steering a lone agent through `hub` send/wait costs more than the work.\n## Delegation gates\n- **Own decomposition.** Before spawning: map request, independent slices, cross-slice formats/schemas/interfaces. Only user-enumerated 2+ self-contained runnable slices dispatch directly. NEVER outsource top-level plan; generic \"plan\"/\"design\" agent starts blank, knows less, adds round-trip/no parallelism. Slice-local design and requested competing plans/reviews allowed.\n- **Real concurrency.** Fan exactly to genuine decomposition, one `tasks[]` array. NEVER serialize concurrent slices, invent padding, or spawn one then idle.\n- **User intent.** Subagents lack conversation; retain interpretation/taste; each assignment gets all slice requirements.\n- **Cap:** At most 32 subagents concurrently; excess queues. `tasks[]` batch > 32 delays results: stay within cap.\n- **Dependencies only.** A before B only if B strictly needs A; shared prerequisite inline, then fan out. “Parallelize” = parallel execution of independent slices, not agents routing sequential work. Small missing piece: run parallel; B asks A via `hub`!\n\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\nFind bugs author wants fixed before merge.\n\n<procedure>\n1. Patch: `git diff` | `jj diff --git` | `gh pr diff <number>`\n2. Modified files: read full context.\n3. Each issue: incremental `yield`, `type: [\"findings\"]`.\n4. Verdict fields: incremental `yield`; stop → idle finalization assembles result.\n\nBash read-only: `git diff`, `git log`, `git show`, `jj diff --git`, `gh pr diff`. NEVER edit files or trigger builds.\n</procedure>\n\n<criteria>\nReport only issues meeting ALL:\n- **Provable impact** — specific affected code paths; no speculation.\n- **Actionable** — discrete fix, not vague \"consider improving X\".\n- **Unintentional** — clearly not deliberate design choice.\n- **Introduced in patch** — don't flag pre-existing bugs.\n- **No unstated assumptions** — no assumptions about codebase or author intent.\n- **Proportionate rigor** — fix demands no rigor absent elsewhere in codebase.\n</criteria>\n\n<cross-boundary>\nEvery patch-introduced type, variant, or value crossing a function or module boundary (event, message, command, frame, enum variant, queue item, IPC payload):\n1. Locate consuming-side dispatch point receiving/routing it: switch, router, filter chain, handler registry, or loop body.\n2. Confirm explicit branch or existing catch-all correctly forwards it.\n3. Report defect if silent drop, no-op, or discard; e.g., unmatched `if`/`switch` simply returns without processing.\n\nDispatch point often outside diff. MUST read it before concluding producing side correct. Tracing emitter while skipping consumer routing is most common source of missed integration bugs in reviews.\n</cross-boundary>\n\n<priority>\n|Level|Criteria|Example|\n|---|---|---|\n|P0|Blocks release/operations; universal (no input assumptions)|Data corruption, auth bypass|\n|P1|High; fix next cycle|Race condition under load|\n|P2|Medium; fix eventually|Edge case mishandling|\n|P3|Info; nice to have|Suboptimal but correct|\n</priority>\n\n<findings>\n- **Title**: e.g., `Handle null response from API`\n- **Body**: bug, trigger condition, impact; neutral tone.\n- **Suggestion blocks**: only concrete replacement code; preserve exact whitespace; no commentary.\n</findings>\n\n<example name=\"finding\">\n<title>Validate input length before buffer copy</title>\n<body>When `data.length > BUFFER_SIZE`, `memcpy` writes past buffer boundary. Occurs if API returns oversized payloads, causing heap corruption.</body>\n```suggestion\nif (data.length > BUFFER_SIZE) return -EINVAL;\nmemcpy(buf, data.ptr, data.length);\n```\n</example>\n\n<output>\nFinding: incremental `yield`, `type: [\"findings\"]`; `data`:\n- `title`: imperative, ≤80 chars.\n- `body`: one paragraph.\n- `priority`: 0-3.\n- `confidence`: 0.0-1.0.\n- `file_path`: affected-file path.\n- `line_start`, `line_end`: ≤10-line range; MUST overlap diff.\n\nVerdict fields: incremental `yield`:\n- `type: [\"overall_correctness\"]`: `\"correct\"` (no bugs/blockers) | `\"incorrect\"`.\n- `type: [\"explanation\"]`: plain-text 1-3-sentence verdict summary.\n- `type: [\"confidence\"]`: 0.0-1.0 confidence.\n\nDo not emit separate submit tool call or duplicate `findings` in another payload. After all sections, stop; idle finalization assembles result.\n\nNEVER output JSON or code blocks.\n\nCorrectness ignores non-blocking issues: style, docs, nits.\n</output>\n\n<critical>\nEvery finding MUST be patch-anchored and evidence-backed.\n</critical>\n\n§ Context\n# Goal\nDoyle Z5VJM2WP / #307 W1a authorizes #308 Windows completion on HFENDULEAM. Worktree .worktrees/308-registry-process-lock successfully rebased fefc4d64 onto main 289e3cdf. Only parent has removed obsolete PREP-ONLY comment from already-active doc/impl/unit/int requirement. User requires red-first nextest two-process regression with OS lock dropped, restored fail-closed unit + targeted serving/webserve_attachment_e2e, traceable-reqs 0.4.1, thin PR; no #309 or #49/#267 implementation yet.\n# Constraints\nBoth agents MUST skip all builds, tests, formatters, linters, pool commands, and producer execution. Main executes serially. No shared edits. Do not touch root or other worktrees. Source changes owned Main. Current tree has no target. Operator says runner idle; Hertz may run two isolated-home daemon probes concurrently, do not classify his known daemon as forbidden cargo or kill anything. Own target only; no unclassified target deletion.\n# Contract\nReviewer is read-only. Driver writer owns only .spt/preserved/308-registry-process-lock/windows-validation/run.py; implementation source never edited by driver. Parent will disable exactly lock.lock_exclusive()? in serving.rs and restore byte-exact after red. Driver supports named phases pool-claim, red, green, trace, pool-release (one execution each, unique receipts), invokes real binaries serially, logs native exits separately. Root path C:/Users/decid/Documents/projects/spt-core. Private test dirs outside Git. Jobs2, ci-windows nextest profile and golden three-knob env values from existing established driver. Main handles pool cleanup after proof and thin PR; retain all receipts.\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 `RegistryLockReview`. Currently visible peers:\n- `RegistryWindowsDriver` — task (sub, running)\n- `Main` — main (main, running)\n18 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    overall_correctness: \"correct\" | \"incorrect\";\n    explanation: string;\n    confidence: unknown;\n    findings?: Array<{\n    title: string;\n    body: string;\n    priority: unknown;\n    confidence: unknown;\n    file_path: string;\n    line_start: unknown;\n    line_end: unknown;\n  }>;\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\nRead-only review of rebased #308 draft: crates/spt-store/src/serving.rs lock_registry_at, crates/spt-daemon/src/servehost.rs apply_at + tests, livehost.rs reaper, store tests/fixtures/serving_registry_worker.rs and tests/serving_registry_two_process_int.rs, fixture writers in two webserve E2Es, requirement/doc scope. No code changes and no validation.\n# Change\nCheck Windows fs2 write-only sentinel correctness, stable sentinel through atomic rename, fail-closed before load, explicit save/no-op semantics, live lock scopes through unlink/publication, remaining writer gaps, deterministic OS-lock-disabled red witness. Confirm existing tests cover user dispatch (fail-closed + 3 process tests + targeted suite) and comments/activation current. Inspect diff via git diff 289e3cdf -- selected paths if helpful. Identify concrete blockers only with source evidence; distinguish original known coverage limits (storage API process regression vs exact production schedule) from defects. Do not suggest broad new APIs or scope extras. LSP is configured but not started; do not launch analyzer/build; text source review allowed.\n# Acceptance\nReturn actionable findings ranked severity or explicit no blocking findings, exact paths/lines. Skip all builds/lint/tests.","tools":["read","grep","glob","bash","web_search","yield","task","hub","spt_checkpoint"],"agent":"reviewer","modelRole":"slow","resolvedModel":"openai-codex/gpt-6-astra:xhigh","readOnly":false,"spawns":"scout","outputSchema":{"properties":{"overall_correctness":{"metadata":{"description":"Whether change correct (no bugs/blockers)"},"enum":["correct","incorrect"]},"explanation":{"metadata":{"description":"Plain-text verdict summary, 1-3 sentences"},"type":"string"},"confidence":{"metadata":{"description":"Verdict confidence (0.0-1.0)"},"type":"number"}},"optionalProperties":{"findings":{"metadata":{"description":"Populate via incremental yield sections under type: [\"findings\"]; don't repeat it in a final payload."},"elements":{"properties":{"title":{"metadata":{"description":"Imperative, ≤80 chars"},"type":"string"},"body":{"metadata":{"description":"One paragraph: bug, trigger, impact"},"type":"string"},"priority":{"metadata":{"description":"P0-P3: 0 blocks release, 1 fix next cycle, 2 fix eventually, 3 nice to have"},"type":"number"},"confidence":{"metadata":{"description":"Confidence it's real bug (0.0-1.0)"},"type":"number"},"file_path":{"metadata":{"description":"Path to affected file"},"type":"string"},"line_start":{"metadata":{"description":"First line (1-indexed)"},"type":"number"},"line_end":{"metadata":{"description":"Last line (1-indexed, ≤10 lines)"},"type":"number"}}}}}},"outputSchemaMode":"permissive"}
{"type":"message","id":"a5dd6dc4","parentId":"61733ba1","timestamp":"2026-09-16T01:41:04.123Z","message":{"role":"user","content":[{"type":"text","text":"Complete assignment thoroughly:\n\n# Target\nRead-only review of rebased #308 draft: crates/spt-store/src/serving.rs lock_registry_at, crates/spt-daemon/src/servehost.rs apply_at + tests, livehost.rs reaper, store tests/fixtures/serving_registry_worker.rs and tests/serving_registry_two_process_int.rs, fixture writers in two webserve E2Es, requirement/doc scope. No code changes and no validation.\n# Change\nCheck Windows fs2 write-only sentinel correctness, stable sentinel through atomic rename, fail-closed before load, explicit save/no-op semantics, live lock scopes through unlink/publication, remaining writer gaps, deterministic OS-lock-disabled red witness. Confirm existing tests cover user dispatch (fail-closed + 3 process tests + targeted suite) and comments/activation current. Inspect diff via git diff 289e3cdf -- selected paths if helpful. Identify concrete blockers only with source evidence; distinguish original known coverage limits (storage API process regression vs exact production schedule) from defects. Do not suggest broad new APIs or scope extras. LSP is configured but not started; do not launch analyzer/build; text source review allowed.\n# Acceptance\nReturn actionable findings ranked severity or explicit no blocking findings, exact paths/lines. Skip all builds/lint/tests."}],"attribution":"agent","timestamp":1789522864072}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MAkbbXo3xLACXsYqsOJh2PTk|fc_0cdf94810c888815016aa9f3b28b6087d1beaec7be6bd54d2a","toolName":"read","startedAt":"2026-09-16T01:41:07.194Z","args":{"path":"skill://code-review"},"intent":"Reading review skill instructions"},"id":"23658965","parentId":"a5dd6dc4","timestamp":"2026-09-16T01:41:07.194Z"}
{"type":"message","id":"b030cea8","parentId":"23658965","timestamp":"2026-09-16T01:41:07.194Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_MAkbbXo3xLACXsYqsOJh2PTk|fc_0cdf94810c888815016aa9f3b28b6087d1beaec7be6bd54d2a","name":"read","arguments":{"i":"Reading review skill instructions","path":"skill://code-review"},"intent":"Reading review skill instructions"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":13149,"output":27,"cacheRead":0,"cacheWrite":0,"totalTokens":13176,"cost":{"input":0.13149000000000002,"output":0.00135,"cacheRead":0,"cacheWrite":0,"total":0.13284}},"stopReason":"toolUse","timestamp":1789522864121,"responseId":"resp_0cdf94810c888815016aa9f3b0e8f087d1b86586c3f5a2a749","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cdf94810c888815016aa9f3b28b6087d1beaec7be6bd54d2a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading review skill instructions\",\"path\":\"skill://code-review\"}","call_id":"call_MAkbbXo3xLACXsYqsOJh2PTk","internal_chat_message_metadata_passthrough":{"create_time":1789522865.286728,"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":3070.664300000004,"ttft":2444.1198999999906,"completedAt":1789522867193,"contextSnapshot":{"promptTokens":13149,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"credential_pin","id":"0779f264","parentId":"b030cea8","timestamp":"2026-09-16T01:41:07.195Z","provider":"openai-codex","hash":"1a3c10d7bc41d381cab1511db604bc2637dfcfc63796a890d8b5ee152859e1cb"}
{"type":"message","id":"6ed51934","parentId":"0779f264","timestamp":"2026-09-16T01:41:07.197Z","message":{"role":"toolResult","toolCallId":"call_MAkbbXo3xLACXsYqsOJh2PTk|fc_0cdf94810c888815016aa9f3b28b6087d1beaec7be6bd54d2a","toolName":"read","content":[{"type":"text","text":"---\nname: code-review\ndescription: 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---\n\nTwo-axis review of the diff between `HEAD` and a fixed point the user supplies:\n\n- **Standards** — does the code conform to this repo's documented coding standards?\n- **Spec** — does the code faithfully implement the originating issue / spec?\n\nBoth axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings.\n\nThe issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing.\n\n## Process\n\n### 1. Pin the fixed point\n\nWhatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. If they didn't specify one, ask for it.\n\nCapture the diff command once: `git diff <fixed-point>...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log <fixed-point>..HEAD --oneline`.\n\nBefore going further, confirm the fixed point resolves (`git rev-parse <fixed-point>`) and the diff is non-empty. A bad ref or empty diff should fail here — not inside two parallel sub-agents.\n\n### 2. Identify the spec source\n\nLook for the originating spec, in this order:\n\n1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`.\n2. A path the user passed as an argument.\n3. A spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature.\n4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report \"no spec available\".\n\n### 3. Identify the standards sources\n\nAnything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`.\n\nOn top of whatever the repo documents, the Standards axis always carries the **smell baseline** below — a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it:\n\n- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell.\n- **Always a judgement call.** Each smell is a labelled heuristic (\"possible Feature Envy\"), never a hard violation — and, like any standard here, skip anything tooling already enforces.\n\nEach smell reads *what it is* → *how to fix*; match it against the diff:\n\n- **Mysterious Name** — a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky.\n- **Duplicated Code** — the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both.\n- **Feature Envy** — a method that reaches into another object's data more than its own. → move the method onto the data it envies.\n- **Data Clumps** — the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that.\n- **Primitive Obsession** — a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type.\n- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share.\n- **Shotgun Surgery** — one logical change forces scattered edits across many files in the diff. → gather what changes together into one module.\n- **Divergent Change** — one file or module is edited for several unrelated reasons. → split so each module changes for one reason.\n- **Speculative Generality** — abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows.\n- **Message Chains** — long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object.\n- **Middle Man** — a class or function that mostly just delegates onward. → cut it, call the real target direct.\n- **Refused Bequest** — a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition.\n\n### 4. Spawn both sub-agents in parallel\n\nSend a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both.\n\n**Standards sub-agent prompt** — include:\n\n- The full diff command and commit list.\n- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full — the sub-agent has no other access to it.\n- The brief: \"Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls — documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words.\"\n\n**Spec sub-agent prompt** — include:\n\n- The diff command and commit list.\n- The path or fetched contents of the spec.\n- The brief: \"Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words.\"\n\nIf the spec is missing, skip the Spec sub-agent and note this in the final report.\n\n### 5. Aggregate\n\nPresent the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate (see _Why two axes_).\n\nEnd with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes — that's the reranking the separation exists to prevent.\n\n## Why two axes\n\nA change can pass one axis and fail the other:\n\n- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.**\n- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.**\n\nReporting them separately stops one axis from masking the other."}],"details":{"resolvedPath":"C:\\Users\\decid\\.agents\\skills\\code-review\\SKILL.md","contentType":"text/markdown","totalLines":89,"displayContent":{"text":"---\r\nname: code-review\r\ndescription: 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\".\r\n---\r\n\r\nTwo-axis review of the diff between `HEAD` and a fixed point the user supplies:\r\n\r\n- **Standards** — does the code conform to this repo's documented coding standards?\r\n- **Spec** — does the code faithfully implement the originating issue / spec?\r\n\r\nBoth axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings.\r\n\r\nThe issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing.\r\n\r\n## Process\r\n\r\n### 1. Pin the fixed point\r\n\r\nWhatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. If they didn't specify one, ask for it.\r\n\r\nCapture the diff command once: `git diff <fixed-point>...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log <fixed-point>..HEAD --oneline`.\r\n\r\nBefore going further, confirm the fixed point resolves (`git rev-parse <fixed-point>`) and the diff is non-empty. A bad ref or empty diff should fail here — not inside two parallel sub-agents.\r\n\r\n### 2. Identify the spec source\r\n\r\nLook for the originating spec, in this order:\r\n\r\n1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`.\r\n2. A path the user passed as an argument.\r\n3. A spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature.\r\n4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report \"no spec available\".\r\n\r\n### 3. Identify the standards sources\r\n\r\nAnything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`.\r\n\r\nOn top of whatever the repo documents, the Standards axis always carries the **smell baseline** below — a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it:\r\n\r\n- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell.\r\n- **Always a judgement call.** Each smell is a labelled heuristic (\"possible Feature Envy\"), never a hard violation — and, like any standard here, skip anything tooling already enforces.\r\n\r\nEach smell reads *what it is* → *how to fix*; match it against the diff:\r\n\r\n- **Mysterious Name** — a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky.\r\n- **Duplicated Code** — the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both.\r\n- **Feature Envy** — a method that reaches into another object's data more than its own. → move the method onto the data it envies.\r\n- **Data Clumps** — the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that.\r\n- **Primitive Obsession** — a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type.\r\n- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share.\r\n- **Shotgun Surgery** — one logical change forces scattered edits across many files in the diff. → gather what changes together into one module.\r\n- **Divergent Change** — one file or module is edited for several unrelated reasons. → split so each module changes for one reason.\r\n- **Speculative Generality** — abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows.\r\n- **Message Chains** — long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object.\r\n- **Middle Man** — a class or function that mostly just delegates onward. → cut it, call the real target direct.\r\n- **Refused Bequest** — a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition.\r\n\r\n### 4. Spawn both sub-agents in parallel\r\n\r\nSend a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both.\r\n\r\n**Standards sub-agent prompt** — include:\r\n\r\n- The full diff command and commit list.\r\n- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full — the sub-agent has no other access to it.\r\n- The brief: \"Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls — documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words.\"\r\n\r\n**Spec sub-agent prompt** — include:\r\n\r\n- The diff command and commit list.\r\n- The path or fetched contents of the spec.\r\n- The brief: \"Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words.\"\r\n\r\nIf the spec is missing, skip the Spec sub-agent and note this in the final report.\r\n\r\n### 5. Aggregate\r\n\r\nPresent the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate (see _Why two axes_).\r\n\r\nEnd with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes — that's the reranking the separation exists to prevent.\r\n\r\n## Why two axes\r\n\r\nA change can pass one axis and fail the other:\r\n\r\n- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.**\r\n- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.**\r\n\r\nReporting them separately stops one axis from masking the other.\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]},"meta":{"source":{"type":"internal","value":"skill://code-review"}}},"isError":false,"timestamp":1789522867196}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cAghn1giRgcZQmm4tG8LJreC|fc_0cdf94810c888815016aa9f3b4827887d186eb6d022bcdca7f","toolName":"bash","startedAt":"2026-09-16T01:41:10.020Z","args":{"command":"git diff 289e3cdf --stat"},"intent":"Inspecting registry lock patch"},"id":"70b22056","parentId":"6ed51934","timestamp":"2026-09-16T01:41:10.020Z"}
{"type":"message","id":"7d8296bb","parentId":"70b22056","timestamp":"2026-09-16T01:41:10.020Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_cAghn1giRgcZQmm4tG8LJreC|fc_0cdf94810c888815016aa9f3b4827887d186eb6d022bcdca7f","name":"bash","arguments":{"i":"Inspecting registry lock patch","command":"git diff 289e3cdf --stat","cwd":"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock"},"intent":"Inspecting registry lock patch"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1659,"output":57,"cacheRead":13056,"cacheWrite":0,"totalTokens":14772,"cost":{"input":0.01659,"output":0.00285,"cacheRead":0.013056,"cacheWrite":0,"total":0.032496}},"stopReason":"toolUse","timestamp":1789522867200,"responseId":"resp_0cdf94810c888815016aa9f3b36c9087d1b59b129482873755","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cdf94810c888815016aa9f3b4827887d186eb6d022bcdca7f","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting registry lock patch\",\"command\":\"git diff 289e3cdf --stat\",\"cwd\":\"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock\"}","call_id":"call_cAghn1giRgcZQmm4tG8LJreC","internal_chat_message_metadata_passthrough":{"create_time":1789522867.591062,"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"bash","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":2818.1865000000107,"ttft":1334.589800000016,"completedAt":1789522870019,"contextSnapshot":{"promptTokens":14715,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"message","id":"caa523e3","parentId":"7d8296bb","timestamp":"2026-09-16T01:41:13.370Z","message":{"role":"toolResult","toolCallId":"call_cAghn1giRgcZQmm4tG8LJreC|fc_0cdf94810c888815016aa9f3b4827887d186eb6d022bcdca7f","toolName":"bash","content":[{"type":"text","text":" CHANGELOG.md                                       |  10 +\n crates/spt-daemon/src/livehost.rs                  |  96 +++++-----\n crates/spt-daemon/src/servehost.rs                 | 118 ++----------\n crates/spt-store/Cargo.toml                        |   5 +\n crates/spt-store/src/serving.rs                    |  36 +++-\n .../tests/fixtures/serving_registry_worker.rs      | 105 ++++++++++\n .../tests/serving_registry_two_process_int.rs      | 212 +++++++++++++++++++++\n crates/spt/tests/webserve_attachment_e2e.rs        |  39 ++--\n crates/spt/tests/webserve_cross_node_e2e.rs        |  36 ++--\n docs-site/src/serving/attachments.md               |  22 ++-\n docs-site/src/serving/overview.md                  |  11 +-\n docs/KNOWN-HAZARDS.md                              |   8 +\n docs/STORAGE.md                                    |  52 ++++-\n traceable-reqs.toml                                |   6 +\n 14 files changed, 557 insertions(+), 199 deletions(-)\n\n\nWall time: 3.35 seconds"}],"details":{"timeoutSeconds":300,"wallTimeMs":3349.038100000005},"isError":false,"timestamp":1789522873370}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JuDPBag20aiJDzP3nM2AzRVt|fc_0cdf94810c888815016aa9f3c3a4d487d1b704f6937a25eed9","toolName":"bash","startedAt":"2026-09-16T01:41:23.717Z","args":{"command":"git diff 289e3cdf -- crates/spt-store/src/serving.rs crates/spt-daemon/src/servehost.rs crates/spt-daemon/src/livehost.rs crates/spt-store/Cargo.toml"},"intent":"Reading registry implementation changes"},"id":"b3f33091","parentId":"caa523e3","timestamp":"2026-09-16T01:41:23.717Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_C51CkfwidOahejQ8Qr7Ylimr|fc_0cdf94810c888815016aa9f3c3a4e487d18808cf4d0a80979d","toolName":"bash","startedAt":"2026-09-16T01:41:23.717Z","args":{"command":"git diff 289e3cdf -- crates/spt-store/tests/fixtures/serving_registry_worker.rs crates/spt-store/tests/serving_registry_two_process_int.rs crates/spt/tests/webserve_attachment_e2e.rs crates/spt/tests/…"},"intent":"Reading registry regression changes"},"id":"db03382f","parentId":"b3f33091","timestamp":"2026-09-16T01:41:23.717Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eoHm3sgu7raXAeaXEowSnssK|fc_0cdf94810c888815016aa9f3c3a4e887d193882b6fb6a8dafd","toolName":"bash","startedAt":"2026-09-16T01:41:23.718Z","args":{"command":"git diff 289e3cdf -- CHANGELOG.md docs-site/src/serving/attachments.md docs-site/src/serving/overview.md docs/KNOWN-HAZARDS.md docs/STORAGE.md traceable-reqs.toml"},"intent":"Reading documented registry scope"},"id":"439687e8","parentId":"db03382f","timestamp":"2026-09-16T01:41:23.718Z"}
{"type":"message","id":"96d6165e","parentId":"439687e8","timestamp":"2026-09-16T01:41:23.718Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_JuDPBag20aiJDzP3nM2AzRVt|fc_0cdf94810c888815016aa9f3c3a4d487d1b704f6937a25eed9","name":"bash","arguments":{"i":"Reading registry implementation changes","command":"git diff 289e3cdf -- crates/spt-store/src/serving.rs crates/spt-daemon/src/servehost.rs crates/spt-daemon/src/livehost.rs crates/spt-store/Cargo.toml","cwd":"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock"},"intent":"Reading registry implementation changes"},{"type":"toolCall","id":"call_C51CkfwidOahejQ8Qr7Ylimr|fc_0cdf94810c888815016aa9f3c3a4e487d18808cf4d0a80979d","name":"bash","arguments":{"i":"Reading registry regression changes","command":"git diff 289e3cdf -- crates/spt-store/tests/fixtures/serving_registry_worker.rs crates/spt-store/tests/serving_registry_two_process_int.rs crates/spt/tests/webserve_attachment_e2e.rs crates/spt/tests/webserve_cross_node_e2e.rs","cwd":"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock"},"intent":"Reading registry regression changes"},{"type":"toolCall","id":"call_eoHm3sgu7raXAeaXEowSnssK|fc_0cdf94810c888815016aa9f3c3a4e887d193882b6fb6a8dafd","name":"bash","arguments":{"i":"Reading documented registry scope","command":"git diff 289e3cdf -- CHANGELOG.md docs-site/src/serving/attachments.md docs-site/src/serving/overview.md docs/KNOWN-HAZARDS.md docs/STORAGE.md traceable-reqs.toml","cwd":"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock"},"intent":"Reading documented registry scope"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":15026,"output":298,"cacheRead":0,"cacheWrite":0,"totalTokens":15324,"cost":{"input":0.15026,"output":0.0149,"cacheRead":0,"cacheWrite":0,"total":0.16516}},"stopReason":"toolUse","timestamp":1789522873396,"responseId":"resp_0cdf94810c888815016aa9f3b9a73087d1a107f37a2df0def5","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cdf94810c888815016aa9f3c3a4d487d1b704f6937a25eed9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry implementation changes\",\"command\":\"git diff 289e3cdf -- crates/spt-store/src/serving.rs crates/spt-daemon/src/servehost.rs crates/spt-daemon/src/livehost.rs crates/spt-store/Cargo.toml\",\"cwd\":\"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock\"}","call_id":"call_JuDPBag20aiJDzP3nM2AzRVt","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"bash","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3c3a4e487d18808cf4d0a80979d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry regression changes\",\"command\":\"git diff 289e3cdf -- crates/spt-store/tests/fixtures/serving_registry_worker.rs crates/spt-store/tests/serving_registry_two_process_int.rs crates/spt/tests/webserve_attachment_e2e.rs crates/spt/tests/webserve_cross_node_e2e.rs\",\"cwd\":\"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock\"}","call_id":"call_C51CkfwidOahejQ8Qr7Ylimr","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"bash","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3c3a4e887d193882b6fb6a8dafd","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading documented registry scope\",\"command\":\"git diff 289e3cdf -- CHANGELOG.md docs-site/src/serving/attachments.md docs-site/src/serving/overview.md docs/KNOWN-HAZARDS.md docs/STORAGE.md traceable-reqs.toml\",\"cwd\":\"C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock\"}","call_id":"call_eoHm3sgu7raXAeaXEowSnssK","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"bash","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":10319.183299999975,"ttft":10274.47590000002,"completedAt":1789522883715,"contextSnapshot":{"promptTokens":15026,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"message","id":"233be472","parentId":"96d6165e","timestamp":"2026-09-16T01:41:26.015Z","message":{"role":"toolResult","toolCallId":"call_eoHm3sgu7raXAeaXEowSnssK|fc_0cdf94810c888815016aa9f3c3a4e887d193882b6fb6a8dafd","toolName":"bash","content":[{"type":"text","text":"CHANGELOG.md | 10 ++++++++++\ndocs-site/src/serving/attachments.md | 22 +++++++++++++++++-----\ndocs-site/src/serving/overview.md | 11 ++++++++++-\ndocs/KNOWN-HAZARDS.md | 8 ++++++++\ndocs/STORAGE.md | 52 +++++++++++++++++++++++-\ntraceable-reqs.toml | 6 ++++++\n6 files changed, 99 insertions(+), 10 deletions(-)\n\n--- Changes ---\n\nFile: CHANGELOG.md\n  @@ -10,6 +10,16 @@ breaks something, or changes the observable behavior of existing surfaces\n  +## [Unreleased]\n  +\n  +### Fixed\n  +\n  +<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n  +- Serving registrations and their stable URL-name assignments no longer disappear\n  […4 changed lines elided…]\n\nFile: docs-site/src/serving/attachments.md\n  @@ -56,11 +56,23 @@ when the sender meant days is a deleted attachment.\n  -The daemon's pulse reaps what has expired, deleting the snapshot file and its\n  -registry entry together and logging how many it removed. An expired entry stops\n  -serving the moment it expires, whether or not the reaper has run yet: between\n  -expiry and the next tick the bytes are still on disk, and serving them would\n  -make the TTL a suggestion.\n  +The brain's pulse reaps expired registry entries and logs how many it removed.\n  […16 changed lines elided…]\n\nFile: docs-site/src/serving/overview.md\n  @@ -115,7 +115,8 @@ Served names remain stable for an entry's lifetime:\n  -The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n  +The broker and brain are separate processes that write\n  +`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n  @@ -125,6 +126,14 @@ registration and its enforcement belong to W2. Do not edit or remove this\n  +Writer coordination requires both processes to hold the\n  +same `serve/registry.lock` guard across reading and updating the registry.\n  +Atomic snapshot replacement alone does not prevent one writer from losing\n  +another's update. Both running writer implementations must be updated:\n  +a new brain with an old broker does not fully activate protection. HTTP readers remain\n  +lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n  […2 changed lines elided…]\n\nFile: docs/KNOWN-HAZARDS.md\n  @@ -459,6 +459,14 @@ Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 c\n  +### 6.14 Serving-registry writers must serialize across processes\n  +<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n  +- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own sna…[+193]\n  +- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit sav…[+330]\n  +- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-sa…[+160]\n  +- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemo…[+18]\n  […2 changed lines elided…]\n\nFile: docs/STORAGE.md\n  @@ -144,10 +144,12 @@ would refuse).\n  -cache. The daemon is its single writer and publishes durable atomic snapshots;\n  -the HTTP listener and listing read those snapshots. Only a genuinely missing\n  -file initializes an empty registry. Unreadable, corrupt, unsupported, or\n  -inconsistent history refuses the operation rather than recycling stale URLs.\n  +cache. The broker and brain are separate writer processes: broker control\n  +requests register, remove, and reconcile entries; the brain's live-host pulse\n  […4 changed lines elided…]\n  @@ -183,6 +185,48 @@ Explicit references point outside SPT_HOME too: copying SPT_HOME alone does not\n  +### Writer serialization\n  +\n  +The releases#308 repair preserves schema 1, the CLI, TTLs, and the existing\n  +save decisions.\n  +See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n  +for the lost-update evidence and its limits.\n  […36 changed lines elided…]\n\nFile: traceable-reqs.toml\n  @@ -4880,6 +4880,7 @@ requirements = [\n  +  \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\",\n  @@ -7744,3 +7745,8 @@ required_stages = [\"doc\", \"impl\"]  # ACTIVATED by this lane (todlando 2026-09-13\n  +\n  +[[requirements]]\n  +id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n  +title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side eff…[+418]\n  +required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n[raw output: artifact://493]\n\n\nWall time: 2.29 seconds"}],"details":{"timeoutSeconds":300,"wallTimeMs":2290.5293999999994},"isError":false,"timestamp":1789522886015}}
{"type":"message","id":"7d6f2362","parentId":"233be472","timestamp":"2026-09-16T01:41:27.141Z","message":{"role":"toolResult","toolCallId":"call_JuDPBag20aiJDzP3nM2AzRVt|fc_0cdf94810c888815016aa9f3c3a4d487d1b704f6937a25eed9","toolName":"bash","content":[{"type":"text","text":"crates/spt-daemon/src/livehost.rs | 96 ++++++++++++------------\ncrates/spt-daemon/src/servehost.rs | 118 +++++-------------------\ncrates/spt-store/Cargo.toml | 5 +++++\ncrates/spt-store/src/serving.rs | 36 +++++++++++++++++++++++-\n4 files changed, 107 insertions(+), 148 deletions(-)\n\n--- Changes ---\n\nFile: crates/spt-daemon/src/livehost.rs\n  @@ -1278,62 +1278,60 @@ fn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n  +// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n  -    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n  -    // lock every other writer in this process holds, because this pass is a\n  -    // read-modify-write on a file `serve add` also writes: an entry registered\n  -    // between our load and our save would be clobbered by our stale snapshot\n  -    // and silently lost, on a 5s tick, forever.\n  […90 changed lines elided…]\n\nFile: crates/spt-daemon/src/servehost.rs\n  @@ -3,7 +3,6 @@\n  -use std::sync::Mutex;\n  @@ -15,10 +14,6 @@ use crate::transport::{send_hello, LocalSocketTransport};\n  -// Serializes load/change/publish across the control channel's connection threads.\n  -// The HTTP reader observes an atomic persisted snapshot, never this lock.\n  -static REGISTRY_WRITE: Mutex<()> = Mutex::new(());\n  -\n  @@ -132,29 +127,10 @@ pub enum ServeResult {\n  -/// Run `f` holding the ONE registry write lock, so a whole read-modify-write\n  -/// pass is atomic against every other writer in this process.\n  -///\n  -/// The daemon has a second registry writer: the live host's TTL reaper, which\n  -/// does load -> retain -> save on its own pulse. Without this it raced every\n  -/// `serve add` — an entry registered between the reaper's load and its save was\n  […15 changed lines elided…]\n  @@ -168,10 +144,8 @@ pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::R\n  -    let _writer = REGISTRY_WRITE\n  -        .lock()\n  -        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n  +    let _writer = spt_store::serving::lock_registry_at(&path)?;\n  @@ -658,7 +632,26 @@ mod tests {\n  +    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n  +    #[test]\n  +    fn unavailable_registry_lock_cannot_publish_an_add() {\n  +        let tmp = tempfile::tempdir().unwrap();\n  +        let home = tmp.path().join(\"home\");\n  +        let source = tmp.path().join(\"report.md\");\n  […13 changed lines elided…]\n  @@ -686,75 +679,6 @@ mod tests {\n  -    /// A reap-shaped read-modify-write cannot clobber a concurrent `serve add`.\n  -    ///\n  -    /// The TTL reaper is the daemon's SECOND registry writer, and it runs on a\n  -    /// pulse: load, retain, save. Outside the write lock, an entry registered\n  -    /// between its load and its save is overwritten by its stale snapshot and\n  -    /// silently lost — no error, no log line, once every five seconds forever.\n  […63 changed lines elided…]\n\nFile: crates/spt-store/Cargo.toml\n  @@ -16,6 +16,11 @@ description = \"spt-core persistence layer: atomic writes, spool, perch layout, r\n  +# Test-only second-process fixture for the serving-registry transaction lock.\n  +[[bin]]\n  +name = \"serving_registry_worker\"\n  +path = \"tests/fixtures/serving_registry_worker.rs\"\n  +\n\nFile: crates/spt-store/src/serving.rs\n  @@ -3,7 +3,8 @@\n  -//! The daemon is the single writer; corruption is never an empty store.\n  +//! Broker mutations and brain reaping share a cross-process writer guard;\n  +//! corruption is never an empty store.\n  @@ -132,6 +133,36 @@ pub struct ServingRegistry {\n  +/// Serialize a registry transaction across threads and processes.\n  +///\n  +/// Acquire BEFORE loading and keep the returned file alive through mutation,\n  +/// snapshot cleanup/rollback, and explicit publication. This does not load or\n  +/// save anything itself, so no-op transactions need not publish a registry.\n  +///\n  […24 changed lines elided…]\n  @@ -161,7 +192,8 @@ impl ServingRegistry {\n  -    /// The caller must serialize mutations and retain the previous state on error.\n  +    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\n  +    /// through this publication; locking only this save cannot repair stale state.\n[raw output: artifact://494]\n\n\nWall time: 3.42 seconds"}],"details":{"timeoutSeconds":300,"wallTimeMs":3417.267600000021},"isError":false,"timestamp":1789522887141}}
{"type":"message","id":"70b47b49","parentId":"7d6f2362","timestamp":"2026-09-16T01:41:28.681Z","message":{"role":"toolResult","toolCallId":"call_C51CkfwidOahejQ8Qr7Ylimr|fc_0cdf94810c888815016aa9f3c3a4e487d18808cf4d0a80979d","toolName":"bash","content":[{"type":"text","text":"crates/spt-store/tests/fixtures/serving_registry_worker.rs | 105 ++++++++++++++++++++++++\ncrates/spt-store/tests/serving_registry_two_process_int.rs | 212 ++++++++++++++++++++++++\ncrates/spt/tests/webserve_attachment_e2e.rs | 39 +++++++-----------------\ncrates/spt/tests/webserve_cross_node_e2e.rs | 36 ++++++++++++++++--------\n4 files changed, 351 insertions(+), 41 deletions(-)\n\n--- Changes ---\n\nFile: crates/spt-store/tests/fixtures/serving_registry_worker.rs\n  @@ -0,0 +1,105 @@\n  +//! Test-only serving-registry writer. All IPC is explicit, per-child file\n  +//! barriers under a temporary directory; polling delays never order transactions.\n  +//! Usage: serving_registry_worker <reap|register> <hold|wait> <registry> <barriers> <source>\n  +\n  +use std::fs::{self, File, OpenOptions};\n  +use std::io;\n  […99 changed lines elided…]\n\nFile: crates/spt-store/tests/serving_registry_two_process_int.rs\n  @@ -0,0 +1,212 @@\n  +//! Cross-process storage-contract proof for releases#308. These children use the\n  +//! public registry API, not the broker/brain entry points; daemon callsite coverage\n  +//! belongs to their own tests. Fixed logical time makes expiry deterministic.\n  +\n  +use std::fs;\n  +use std::io;\n  […206 changed lines elided…]\n\nFile: crates/spt/tests/webserve_attachment_e2e.rs\n  @@ -661,43 +661,30 @@ registry: {}\",\n  -    // no-op HERE — but do not lift this loop onto an audienced entry without\n  +    // no-op HERE — but do not lift this mutation onto an audienced entry without\n  -    // The loop re-reads because the daemon is the registry's other writer. Be\n  -    // precise about what that buys: it detects the entry being ABSENT, not a\n  -    // clobber — a daemon save landing between this load and this save would be\n  -    // overwritten by this snapshot and nothing here would notice. That hazard is\n  […29 changed lines elided…]\n  @@ -706,10 +693,6 @@ registry: {}\",\n  -        if snapshot_of(\"kept-by-the-guard.md\").is_some() {\n  -            break;\n  -        }\n  -        assert!(attempt < 4, \"the planted reference entry kept being clobbered\");\n\nFile: crates/spt/tests/webserve_cross_node_e2e.rs\n  @@ -659,11 +659,15 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n  -        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n  -        serving\n  -            .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n  -            .expect(\"the served entry is there to narrow\");\n  -        serving.save_at(&registry_path).expect(\"persist the narrowing\");\n  +        // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n  […8 changed lines elided…]\n  @@ -673,11 +677,14 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n  -        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n  -        serving\n  -            .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n  -            .expect(\"the entry is still there\");\n  -        serving.save_at(&registry_path).expect(\"persist the second narrowing\");\n  +        {\n  […7 changed lines elided…]\n  @@ -757,9 +764,12 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n  -        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n  -        serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n  -        serving.save_at(&registry_path).expect(\"persist the restore\");\n  +        {\n  +            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n  +            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n  […3 changed lines elided…]\n[raw output: artifact://495]\n\n\nWall time: 4.96 seconds"}],"details":{"timeoutSeconds":300,"wallTimeMs":4956.31090000004},"isError":false,"timestamp":1789522888680}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ATzsYONjNk08uZfDoVtEcJ3Z|fc_0cdf94810c888815016aa9f3cfc68487d1abe8a51d825115e6","toolName":"read","startedAt":"2026-09-16T01:41:35.851Z","args":{"path":"artifact://494"},"intent":"Recovering implementation patch context"},"id":"f7049801","parentId":"70b47b49","timestamp":"2026-09-16T01:41:35.851Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CU2qOLJpDSgIKK9OLip31mKF|fc_0cdf94810c888815016aa9f3cfc69887d19791240b9287a94e","toolName":"read","startedAt":"2026-09-16T01:41:35.851Z","args":{"path":"artifact://495"},"intent":"Recovering regression patch context"},"id":"ce74992b","parentId":"f7049801","timestamp":"2026-09-16T01:41:35.851Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JIaiAWfCQPCXeWNcovLmFhmy|fc_0cdf94810c888815016aa9f3cfc6a087d18cbacf633612d07b","toolName":"read","startedAt":"2026-09-16T01:41:35.851Z","args":{"path":"artifact://493"},"intent":"Recovering scope patch context"},"id":"611d4046","parentId":"ce74992b","timestamp":"2026-09-16T01:41:35.851Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5KRHzfuipBk2u6xrTV5kVJuc|fc_0cdf94810c888815016aa9f3cfc6a887d1aff924f6a9ae4b25","toolName":"glob","startedAt":"2026-09-16T01:41:35.851Z","args":{"path":".worktrees/308-registry-process-lock/**/AGENTS.md;.worktrees/308-registry-process-lock/**/CONTRIBUTING.md;.worktrees/308-registry-process-lock/**/CODING_STANDARDS.md"},"intent":"Locating review scope guidance"},"id":"abd7a946","parentId":"611d4046","timestamp":"2026-09-16T01:41:35.851Z"}
{"type":"message","id":"5f5357b0","parentId":"abd7a946","timestamp":"2026-09-16T01:41:35.851Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Seeking raw artifacts**"},{"type":"toolCall","id":"call_ATzsYONjNk08uZfDoVtEcJ3Z|fc_0cdf94810c888815016aa9f3cfc68487d1abe8a51d825115e6","name":"read","arguments":{"i":"Recovering implementation patch context","path":"artifact://494"},"intent":"Recovering implementation patch context"},{"type":"toolCall","id":"call_CU2qOLJpDSgIKK9OLip31mKF|fc_0cdf94810c888815016aa9f3cfc69887d19791240b9287a94e","name":"read","arguments":{"i":"Recovering regression patch context","path":"artifact://495"},"intent":"Recovering regression patch context"},{"type":"toolCall","id":"call_JIaiAWfCQPCXeWNcovLmFhmy|fc_0cdf94810c888815016aa9f3cfc6a087d18cbacf633612d07b","name":"read","arguments":{"i":"Recovering scope patch context","path":"artifact://493"},"intent":"Recovering scope patch context"},{"type":"toolCall","id":"call_5KRHzfuipBk2u6xrTV5kVJuc|fc_0cdf94810c888815016aa9f3cfc6a887d1aff924f6a9ae4b25","name":"glob","arguments":{"i":"Locating review scope guidance","path":".worktrees/308-registry-process-lock/**/AGENTS.md;.worktrees/308-registry-process-lock/**/CONTRIBUTING.md;.worktrees/308-registry-process-lock/**/CODING_STANDARDS.md","hidden":true,"gitignore":false,"limit":50},"intent":"Locating review scope guidance"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":4181,"output":188,"cacheRead":14592,"cacheWrite":0,"totalTokens":18961,"reasoningTokens":17,"cost":{"input":0.04181000000000001,"output":0.0094,"cacheRead":0.014591999999999999,"cacheWrite":0,"total":0.065802}},"stopReason":"toolUse","timestamp":1789522888752,"responseId":"resp_0cdf94810c888815016aa9f3c9019887d1b025b3402d150058","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cdf94810c888815016aa9f3ca25bc87d1a31128ec102f69b5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqfPP3uwV4rpjbDm6dTL5xrY1zXMsWTYBxAyjqKYH5YNnra0D7DXUAYzJ96c74ShHG9n6PpSGkd_fmOcpTfN2PpxpQqH9nsh5Ot1HDo06sXylR_yb3cSbt1SJQbC5jIEdh9dWIIGHpW_umjiiINppAV8LyjYXJLus6l_fqyzool7KoF-xY1GmiHF7VAxlipI7iuNgVkYwCVkB4nZ1cQBW40bAWwN4ojGu6kXp98nLWXR1YME1nbCtcSJufMylKPI6UWhbU-lABFPrnkrlaD85BiH3JSViTa3ui0EdSj25M-gwdoeiEk-NH1mynXVDXaEliSnuadPsEjIfGYv9JlVx_HB-1gPFDRyJbSgYy9VM9vRcbpqfM_Z_b1YidhVe-tLxC5mP6zPKz83WP6zseAB5B_Zj213EQ1JbEGUgCjPLSuRmqjmQxNlMqEPqCPnlHpCqTv3vg703_1FquUlXxbuofEQayreD0h7h-nkYdHg_jRJ3zK5sK2DXio53y-1e4tkQE7o6xh19wwQEdn05BCg-EMdYeYqC9UPiO16vuuKB8bU0PFXkW7-FVflX599G0yG-fZuJO3ry4GQv15GcO7J-FcljmMLFuA-MPMVPOXXciheFOufFAI2_o8Ey-5QTksB3IkjY3Hn9biXlhMfBk007HNXBWPF8ryvYV4fSdnbcChUY460w7o7z5wjQJylBaPCOCNM374litYM7mKJViWtXtPFf5EX0euWzzUARaanIvVQeKBzOPAjw0f6K4dWNBpCwh9kJT9KwUcy7mI6-MactYTlEMQCkwCKB2oEfXKpx8Z8SwRgcOOPSHjhHEGTnUCMYjkAzVafXnL546UobrAiyYW37Umo1MYtByCr62hrtbCzWjbUjJGA6NsYk-lLfyqHQc_OYdSXlxhnwyWdbAT-OblXwHy30GgKvG9L7_jjn0Ef0xhKNL73UZrhj7Jpqdv7cALa5-ikicWG4IkYefYX-6Isd9km0u1JSmf8kAWzH91_p1Ghd0dqocPBFa6KH1fFTFnhvtc6qJ-54SUYmbZ8GqRjycyhBfkNN4FRuf0ZB0m5icZYL0It2G1LbavMSwNCkM0p3C6viHT6P78W7CqjM7w9e-EYuJg8FfnzZw0ex7CZcRubxwkMhFMgF4N9ALjlApfpsu8As-29ljIlvjj1DPBOSsHSDX_AFEZNJnI1xQcme9W1-EGLe1x1CNwDtNGb-iwXHC743JrbGYYR_syJPAVdOR7ay5kyYn10ZTY8YmR8YxJgVrPQZrFSPV1lRP8Og1jBqARxnvQNrlj_N3J3chhYJoQ1Kmq4n5Akk54MXVRPflDzNgC1mWqVrHfiv-WIb2SK4uJvUxGq3VrFyFi9-5mi8ZSBOXNhfjsIefXa8B4cw7-mFnp9Dk7RBZJZ086lM14u2","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"summary":[{"type":"summary_text","text":"**Seeking raw artifacts**"}],"metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3cfc68487d1abe8a51d825115e6","type":"function_call","status":"completed","arguments":"{\"i\":\"Recovering implementation patch context\",\"path\":\"artifact://494\"}","call_id":"call_ATzsYONjNk08uZfDoVtEcJ3Z","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3cfc69887d19791240b9287a94e","type":"function_call","status":"completed","arguments":"{\"i\":\"Recovering regression patch context\",\"path\":\"artifact://495\"}","call_id":"call_CU2qOLJpDSgIKK9OLip31mKF","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3cfc6a087d18cbacf633612d07b","type":"function_call","status":"completed","arguments":"{\"i\":\"Recovering scope patch context\",\"path\":\"artifact://493\"}","call_id":"call_JIaiAWfCQPCXeWNcovLmFhmy","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3cfc6a887d1aff924f6a9ae4b25","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating review scope guidance\",\"path\":\".worktrees/308-registry-process-lock/**/AGENTS.md;.worktrees/308-registry-process-lock/**/CONTRIBUTING.md;.worktrees/308-registry-process-lock/**/CODING_STANDARDS.md\",\"hidden\":true,\"gitignore\":false,\"limit\":50}","call_id":"call_5KRHzfuipBk2u6xrTV5kVJuc","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"glob","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":7095.267300000007,"ttft":1436.6474000000162,"completedAt":1789522895848,"contextSnapshot":{"promptTokens":18773,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"message","id":"c83bfa87","parentId":"5f5357b0","timestamp":"2026-09-16T01:41:35.866Z","message":{"role":"toolResult","toolCallId":"call_JIaiAWfCQPCXeWNcovLmFhmy|fc_0cdf94810c888815016aa9f3cfc6a087d18cbacf633612d07b","toolName":"read","content":[{"type":"text","text":"diff --git a/CHANGELOG.md b/CHANGELOG.md\nindex b6745ec2..72916ab5 100644\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -10,6 +10,16 @@ breaks something, or changes the observable behavior of existing surfaces\n broadly; **patch** for fixes, and for additive opt-in capability — a new key,\n flag, or page that no existing user can encounter without opting into it.\n \n+## [Unreleased]\n+\n+### Fixed\n+\n+<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+- Serving registrations and their stable URL-name assignments no longer disappear\n+  when attachment expiry updates the registry concurrently. Protection requires\n+  both the running broker and brain to be updated; a brain-only update with an\n+  older broker does not activate it.\n+\n ## [0.70.0] - 2026-09-13\n \n ### Added\ndiff --git a/docs-site/src/serving/attachments.md b/docs-site/src/serving/attachments.md\nindex e3dbf6ea..cf30dd46 100644\n--- a/docs-site/src/serving/attachments.md\n+++ b/docs-site/src/serving/attachments.md\n@@ -56,11 +56,23 @@ when the sender meant days is a deleted attachment.\n $ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\n ```\n \n-The daemon's pulse reaps what has expired, deleting the snapshot file and its\n-registry entry together and logging how many it removed. An expired entry stops\n-serving the moment it expires, whether or not the reaper has run yet: between\n-expiry and the next tick the bytes are still on disk, and serving them would\n-make the TTL a suggestion.\n+The brain's pulse reaps expired registry entries and logs how many it removed.\n+For an attachment, it attempts to unlink the core-owned snapshot **before**\n+saving the registry without that entry. These are not one atomic transaction:\n+if saving fails after unlinking, the next pulse can finish removing the expired\n+entry even though its snapshot is already gone. Saving first could instead\n+leave untracked snapshot bytes that later pulses cannot find.\n+\n+Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n+unlinking. Expiry removes a live file or directory reference from the registry\n+but **never deletes the user's source**. The\n+[writer-coordination contract](overview.md#register-inspect-remove) keeps the\n+guard across the reaper's fresh load, snapshot cleanup, and explicit save;\n+a pass with nothing expired does not save the registry.\n+\n+An expired entry stops serving the moment it expires, whether or not the\n+reaper has run yet: between expiry and the next tick the bytes may still be\n+on disk, and serving them would make the TTL a suggestion.\n \n <!-- [doc->REQ-WEB-FETCH-VERB] -->\n ## Pulling one: `spt fetch`\ndiff --git a/docs-site/src/serving/overview.md b/docs-site/src/serving/overview.md\nindex 8e34b324..3521c45a 100644\n--- a/docs-site/src/serving/overview.md\n+++ b/docs-site/src/serving/overview.md\n@@ -115,7 +115,8 @@ Served names remain stable for an entry's lifetime:\n - Allocation history is persisted per node. Another node has its own names;\n   the node prefix keeps their URLs distinct.\n \n-The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n+The broker and brain are separate processes that write\n+`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n registration timestamps, optional origin endpoint ids, and allocation history.\n Optional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\n@@ -125,6 +126,14 @@ registration and its enforcement belong to W2. Do not edit or remove this\n file to reclaim a name: that would destroy the history which prevents stale\n URLs changing meaning.\n \n+Writer coordination requires both processes to hold the\n+same `serve/registry.lock` guard across reading and updating the registry.\n+Atomic snapshot replacement alone does not prevent one writer from losing\n+another's update. Both running writer implementations must be updated:\n+a new brain with an old broker does not fully activate protection. HTTP readers remain\n+lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n+are unchanged.\n+\n <!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n ## Adapter served roots\n \ndiff --git a/docs/KNOWN-HAZARDS.md b/docs/KNOWN-HAZARDS.md\nindex 039c2456..c2d05afd 100644\n--- a/docs/KNOWN-HAZARDS.md\n+++ b/docs/KNOWN-HAZARDS.md\n@@ -459,6 +459,14 @@ Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 c\n - **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value.\n - **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\n \n+### 6.14 Serving-registry writers must serialize across processes\n+<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n+- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n+- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n+- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.\n+- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. Linux validation passed three deterministic two-process storage regressions, 26 serving-store units, 13 servehost units, and both affected serving E2Es. Disabling only the OS lock made the selected regression fail its independent contention witness; restoring the original source passed. This is storage-process plus E2E evidence, not a captured replay of the original broker/brain schedule. Windows validation remains pending.\n+\n ---\n \n ## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\ndiff --git a/docs/STORAGE.md b/docs/STORAGE.md\nindex 575aaa6e..09d88cde 100644\n--- a/docs/STORAGE.md\n+++ b/docs/STORAGE.md\n@@ -144,10 +144,12 @@ would refuse).\n <!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n \n `$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n-cache. The daemon is its single writer and publishes durable atomic snapshots;\n-the HTTP listener and listing read those snapshots. Only a genuinely missing\n-file initializes an empty registry. Unreadable, corrupt, unsupported, or\n-inconsistent history refuses the operation rather than recycling stale URLs.\n+cache. The broker and brain are separate writer processes: broker control\n+requests register, remove, and reconcile entries; the brain's live-host pulse\n+reaps expired entries. They publish durable atomic snapshots; the HTTP listener\n+reads those snapshots without a writer lock. Only a genuinely missing file\n+initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n+history refuses the operation rather than recycling stale URLs.\n \n Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n `name_owners` (every assigned name mapped to its prior absolute `path` and\n@@ -183,6 +185,48 @@ Explicit references point outside SPT_HOME too: copying SPT_HOME alone does not\n copy their bytes or relocate their absolute paths. Losing the registry loses\n the history which prevents an old URL from naming a different resource.\n \n+### Writer serialization\n+\n+The releases#308 repair preserves schema 1, the CLI, TTLs, and the existing\n+save decisions.\n+See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n+for the lost-update evidence and its limits.\n+\n+Every writer acquires `spt_store::serving::lock_registry_at(registry_path)`\n+**before a fresh load**, and keeps its returned `std::fs::File` alive through\n+the complete read-modify-write operation: load, mutation, associated snapshot\n+creation or cleanup, and any explicit `save_at`. Lock acquisition failure\n+refuses the operation; it never permits an unlocked write. The lock is\n+non-reentrant: a guarded caller must not reacquire it for the same registry.\n+\n+The guard opens a fresh file handle and takes an `fs2` exclusive lock on a\n+stable sibling formed by appending `.lock` to the **full registry path**:\n+`$SPT_HOME/serve/registry.lock`. Never lock the registry inode itself: atomic\n+publication replaces that file. Never unlink the sentinel, including on\n+unlock or cleanup: waiters and new writers must lock the same file. Dropping\n+the guard or exiting the process releases the lock without deleting it.\n+\n+Atomic publication and mutual exclusion solve different problems. Atomic\n+replacement gives lock-free HTTP readers a complete old or new snapshot; it\n+does not stop a stale writer from replacing a newer snapshot. Locking only\n+`save_at` is too late, and a process-local mutex cannot coordinate the broker\n+with the brain. Direct writers, including live-daemon fixture mutations, must\n+follow the same full-operation guard contract.\n+\n+The guard **never auto-saves**, including on drop. Read-only listing, an\n+unchanged input-reference replay, and a reaper pass with no expired entries\n+do not save the registry; a missing registry remains missing after a no-op\n+reap. Existing operations that explicitly save, such as reconciliation, still\n+do so. Snapshot unlink-before-save ordering and user-source protection remain\n+the [attachment lifetime contract](../docs-site/src/serving/attachments.md#lifetime)\n+(`REQ-WEB-ATTACHMENT-PULL`); locking does not make snapshot deletion and registry\n+publication one atomic transaction.\n+\n+Protection is active only when **both running writer implementations** use\n+this protocol. Updating only the brain while an old broker survives does not\n+fully activate it; a new broker paired with an old brain is likewise unsafe.\n+The sentinel's presence is not proof that both writers participate.\n+\n ## tracked/ — the context git repo\n \n - A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing (\"which endpoint is most responsible for this task\"). *Mechanics (D6a):* the object DB is a **bare seed repo at `tracked/.seed.git`**; `agents/<id>/` and `projects/<project_id>/` are **linked worktrees** of their branches, managed by `spt-store::{branchstore,contextstore}` over the system git binary (ADR-0013). Each branch seeds as a parentless empty-tree root; cross-node sync joins histories at first contact.\ndiff --git a/traceable-reqs.toml b/traceable-reqs.toml\nindex 1550de0f..bf70ec01 100644\n--- a/traceable-reqs.toml\n+++ b/traceable-reqs.toml\n@@ -4880,6 +4880,7 @@ requirements = [\n   \"REQ-HAZARD-EBUSY-RENAME\",\n   \"REQ-HAZARD-INFO-JSON-TORN-READ\",\n   \"REQ-HAZARD-REGISTRY-STALE-CLEAN\",\n+  \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\",\n   \"REQ-HAZARD-SINGLE-PATH-SOURCE\",\n   \"REQ-HAZARD-STALE-INDEX-LOCK\",\n   \"REQ-HAZARD-STORE-INIT-RACE\",\n@@ -7744,3 +7745,8 @@ required_stages = [\"doc\", \"impl\"]  # ACTIVATED by this lane (todlando 2026-09-13\n id = \"REQ-RC-HITCH-DISCRIMINATOR\"\n title = \"releases#302 diagnostic-only: an explicitly opted-in daemon-status probe reads the live broker's existing net_canary_age_ms and active_dial_tasks atomics on a fresh bounded IPC connection, without session/output-log locks or work scheduled on the net runtime. Report broker sample time and client elapsed time; absent net hosts and unsupported or unresponsive brokers must not appear healthy. Ordinary status is unchanged. This instruments the field discriminator, not a remedy or a claim that the rc hitch is attributed.\"\n required_stages = [\"impl\"] # Instrumentation only; no bug-fix or field acceptance stage claimed.\n+\n+[[requirements]]\n+id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n+title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side effects, and durable publication across processes using one stable per-registry sentinel. Atomic replacement alone must not erase acknowledged registrations or persisted name-allocation history. Lock acquisition failure never permits an unlocked write, and process exit releases the lock. Both writer binaries must participate; a brain-only update paired with an old writer broker does not close the race (releases#308).\"\n+required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]"}],"details":{"resolvedPath":"C:\\Users\\decid\\.omp\\agent\\sessions\\-Documents-projects-spt-core\\2026-09-13T10-23-45-921Z_01a09a4b-5f01-7000-9118-23ce74a49636\\493.bash-original.log","contentType":"text/plain","totalLines":193,"displayContent":{"text":"diff --git a/CHANGELOG.md b/CHANGELOG.md\nindex b6745ec2..72916ab5 100644\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -10,6 +10,16 @@ breaks something, or changes the observable behavior of existing surfaces\n broadly; **patch** for fixes, and for additive opt-in capability — a new key,\n flag, or page that no existing user can encounter without opting into it.\n \n+## [Unreleased]\n+\n+### Fixed\n+\n+<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+- Serving registrations and their stable URL-name assignments no longer disappear\n+  when attachment expiry updates the registry concurrently. Protection requires\n+  both the running broker and brain to be updated; a brain-only update with an\n+  older broker does not activate it.\n+\n ## [0.70.0] - 2026-09-13\n \n ### Added\ndiff --git a/docs-site/src/serving/attachments.md b/docs-site/src/serving/attachments.md\nindex e3dbf6ea..cf30dd46 100644\n--- a/docs-site/src/serving/attachments.md\n+++ b/docs-site/src/serving/attachments.md\n@@ -56,11 +56,23 @@ when the sender meant days is a deleted attachment.\n $ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\n ```\n \n-The daemon's pulse reaps what has expired, deleting the snapshot file and its\n-registry entry together and logging how many it removed. An expired entry stops\n-serving the moment it expires, whether or not the reaper has run yet: between\n-expiry and the next tick the bytes are still on disk, and serving them would\n-make the TTL a suggestion.\n+The brain's pulse reaps expired registry entries and logs how many it removed.\n+For an attachment, it attempts to unlink the core-owned snapshot **before**\n+saving the registry without that entry. These are not one atomic transaction:\n+if saving fails after unlinking, the next pulse can finish removing the expired\n+entry even though its snapshot is already gone. Saving first could instead\n+leave untracked snapshot bytes that later pulses cannot find.\n+\n+Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n+unlinking. Expiry removes a live file or directory reference from the registry\n+but **never deletes the user's source**. The\n+[writer-coordination contract](overview.md#register-inspect-remove) keeps the\n+guard across the reaper's fresh load, snapshot cleanup, and explicit save;\n+a pass with nothing expired does not save the registry.\n+\n+An expired entry stops serving the moment it expires, whether or not the\n+reaper has run yet: between expiry and the next tick the bytes may still be\n+on disk, and serving them would make the TTL a suggestion.\n \n <!-- [doc->REQ-WEB-FETCH-VERB] -->\n ## Pulling one: `spt fetch`\ndiff --git a/docs-site/src/serving/overview.md b/docs-site/src/serving/overview.md\nindex 8e34b324..3521c45a 100644\n--- a/docs-site/src/serving/overview.md\n+++ b/docs-site/src/serving/overview.md\n@@ -115,7 +115,8 @@ Served names remain stable for an entry's lifetime:\n - Allocation history is persisted per node. Another node has its own names;\n   the node prefix keeps their URLs distinct.\n \n-The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n+The broker and brain are separate processes that write\n+`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n registration timestamps, optional origin endpoint ids, and allocation history.\n Optional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\n@@ -125,6 +126,14 @@ registration and its enforcement belong to W2. Do not edit or remove this\n file to reclaim a name: that would destroy the history which prevents stale\n URLs changing meaning.\n \n+Writer coordination requires both processes to hold the\n+same `serve/registry.lock` guard across reading and updating the registry.\n+Atomic snapshot replacement alone does not prevent one writer from losing\n+another's update. Both running writer implementations must be updated:\n+a new brain with an old broker does not fully activate protection. HTTP readers remain\n+lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n+are unchanged.\n+\n <!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n ## Adapter served roots\n \ndiff --git a/docs/KNOWN-HAZARDS.md b/docs/KNOWN-HAZARDS.md\nindex 039c2456..c2d05afd 100644\n--- a/docs/KNOWN-HAZARDS.md\n+++ b/docs/KNOWN-HAZARDS.md\n@@ -459,6 +459,14 @@ Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 c\n - **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value.\n - **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\n \n+### 6.14 Serving-registry writers must serialize across processes\n+<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n+- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n+- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n+- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.\n+- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. Linux validation passed three deterministic two-process storage regressions, 26 serving-store units, 13 servehost units, and both affected serving E2Es. Disabling only the OS lock made the selected regression fail its independent contention witness; restoring the original source passed. This is storage-process plus E2E evidence, not a captured replay of the original broker/brain schedule. Windows validation remains pending.\n+\n ---\n \n ## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\ndiff --git a/docs/STORAGE.md b/docs/STORAGE.md\nindex 575aaa6e..09d88cde 100644\n--- a/docs/STORAGE.md\n+++ b/docs/STORAGE.md\n@@ -144,10 +144,12 @@ would refuse).\n <!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n \n `$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n-cache. The daemon is its single writer and publishes durable atomic snapshots;\n-the HTTP listener and listing read those snapshots. Only a genuinely missing\n-file initializes an empty registry. Unreadable, corrupt, unsupported, or\n-inconsistent history refuses the operation rather than recycling stale URLs.\n+cache. The broker and brain are separate writer processes: broker control\n+requests register, remove, and reconcile entries; the brain's live-host pulse\n+reaps expired entries. They publish durable atomic snapshots; the HTTP listener\n+reads those snapshots without a writer lock. Only a genuinely missing file\n+initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n+history refuses the operation rather than recycling stale URLs.\n \n Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n `name_owners` (every assigned name mapped to its prior absolute `path` and\n@@ -183,6 +185,48 @@ Explicit references point outside SPT_HOME too: copying SPT_HOME alone does not\n copy their bytes or relocate their absolute paths. Losing the registry loses\n the history which prevents an old URL from naming a different resource.\n \n+### Writer serialization\n+\n+The releases#308 repair preserves schema 1, the CLI, TTLs, and the existing\n+save decisions.\n+See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n+for the lost-update evidence and its limits.\n+\n+Every writer acquires `spt_store::serving::lock_registry_at(registry_path)`\n+**before a fresh load**, and keeps its returned `std::fs::File` alive through\n+the complete read-modify-write operation: load, mutation, associated snapshot\n+creation or cleanup, and any explicit `save_at`. Lock acquisition failure\n+refuses the operation; it never permits an unlocked write. The lock is\n+non-reentrant: a guarded caller must not reacquire it for the same registry.\n+\n+The guard opens a fresh file handle and takes an `fs2` exclusive lock on a\n+stable sibling formed by appending `.lock` to the **full registry path**:\n+`$SPT_HOME/serve/registry.lock`. Never lock the registry inode itself: atomic\n+publication replaces that file. Never unlink the sentinel, including on\n+unlock or cleanup: waiters and new writers must lock the same file. Dropping\n+the guard or exiting the process releases the lock without deleting it.\n+\n+Atomic publication and mutual exclusion solve different problems. Atomic\n+replacement gives lock-free HTTP readers a complete old or new snapshot; it\n+does not stop a stale writer from replacing a newer snapshot. Locking only\n+`save_at` is too late, and a process-local mutex cannot coordinate the broker\n+with the brain. Direct writers, including live-daemon fixture mutations, must\n+follow the same full-operation guard contract.\n+\n+The guard **never auto-saves**, including on drop. Read-only listing, an\n+unchanged input-reference replay, and a reaper pass with no expired entries\n+do not save the registry; a missing registry remains missing after a no-op\n+reap. Existing operations that explicitly save, such as reconciliation, still\n+do so. Snapshot unlink-before-save ordering and user-source protection remain\n+the [attachment lifetime contract](../docs-site/src/serving/attachments.md#lifetime)\n+(`REQ-WEB-ATTACHMENT-PULL`); locking does not make snapshot deletion and registry\n+publication one atomic transaction.\n+\n+Protection is active only when **both running writer implementations** use\n+this protocol. Updating only the brain while an old broker survives does not\n+fully activate it; a new broker paired with an old brain is likewise unsafe.\n+The sentinel's presence is not proof that both writers participate.\n+\n ## tracked/ — the context git repo\n \n - A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing (\"which endpoint is most responsible for this task\"). *Mechanics (D6a):* the object DB is a **bare seed repo at `tracked/.seed.git`**; `agents/<id>/` and `projects/<project_id>/` are **linked worktrees** of their branches, managed by `spt-store::{branchstore,contextstore}` over the system git binary (ADR-0013). Each branch seeds as a parentless empty-tree root; cross-node sync joins histories at first contact.\ndiff --git a/traceable-reqs.toml b/traceable-reqs.toml\nindex 1550de0f..bf70ec01 100644\n--- a/traceable-reqs.toml\n+++ b/traceable-reqs.toml\n@@ -4880,6 +4880,7 @@ requirements = [\n   \"REQ-HAZARD-EBUSY-RENAME\",\n   \"REQ-HAZARD-INFO-JSON-TORN-READ\",\n   \"REQ-HAZARD-REGISTRY-STALE-CLEAN\",\n+  \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\",\n   \"REQ-HAZARD-SINGLE-PATH-SOURCE\",\n   \"REQ-HAZARD-STALE-INDEX-LOCK\",\n   \"REQ-HAZARD-STORE-INIT-RACE\",\n@@ -7744,3 +7745,8 @@ required_stages = [\"doc\", \"impl\"]  # ACTIVATED by this lane (todlando 2026-09-13\n id = \"REQ-RC-HITCH-DISCRIMINATOR\"\n title = \"releases#302 diagnostic-only: an explicitly opted-in daemon-status probe reads the live broker's existing net_canary_age_ms and active_dial_tasks atomics on a fresh bounded IPC connection, without session/output-log locks or work scheduled on the net runtime. Report broker sample time and client elapsed time; absent net hosts and unsupported or unresponsive brokers must not appear healthy. Ordinary status is unchanged. This instruments the field discriminator, not a remedy or a claim that the rc hitch is attributed.\"\n required_stages = [\"impl\"] # Instrumentation only; no bug-fix or field acceptance stage claimed.\n+\n+[[requirements]]\n+id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n+title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side effects, and durable publication across processes using one stable per-registry sentinel. Atomic replacement alone must not erase acknowledged registrations or persisted name-allocation history. Lock acquisition failure never permits an unlocked write, and process exit releases the lock. Both writer binaries must participate; a brain-only update paired with an old writer broker does not close the race (releases#308).\"\n+required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193]},"meta":{"source":{"type":"internal","value":"artifact://493"}}},"isError":false,"timestamp":1789522895865}}
{"type":"message","id":"a3600ee3","parentId":"c83bfa87","timestamp":"2026-09-16T01:41:35.867Z","message":{"role":"toolResult","toolCallId":"call_ATzsYONjNk08uZfDoVtEcJ3Z|fc_0cdf94810c888815016aa9f3cfc68487d1abe8a51d825115e6","toolName":"read","content":[{"type":"text","text":"diff --git a/crates/spt-daemon/src/livehost.rs b/crates/spt-daemon/src/livehost.rs\nindex 151c5e2e..f2cda242 100644\n--- a/crates/spt-daemon/src/livehost.rs\n+++ b/crates/spt-daemon/src/livehost.rs\n@@ -1278,62 +1278,60 @@ fn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n /// containment check is belt-and-braces on that same point: a hand-edited or\n /// corrupted registry must not be able to aim a delete at an arbitrary path.\n // [impl->REQ-WEB-ATTACHMENT-PULL]\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n fn reap_expired_attachments() {\n-    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n-    // lock every other writer in this process holds, because this pass is a\n-    // read-modify-write on a file `serve add` also writes: an entry registered\n-    // between our load and our save would be clobbered by our stale snapshot\n-    // and silently lost, on a 5s tick, forever.\n-    let held = crate::servehost::with_registry_write(|| {\n-        let registry_path = perch::serving_registry_file();\n-        if !registry_path.exists() {\n-            return; // nothing has ever been served on this node\n+    let registry_path = perch::serving_registry_file();\n+    if !registry_path.exists() {\n+        return; // nothing has ever been served on this node\n+    }\n+    // The broker is a DIFFERENT process. Hold the shared sentinel from before\n+    // loading until snapshot cleanup and publication have both finished.\n+    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\n+        Ok(writer) => writer,\n+        Err(err) => {\n+            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n+            return; // never reap outside the lock\n         }\n-        let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n-            Ok(registry) => registry,\n-            Err(err) => {\n-                eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n-                return;\n-            }\n-        };\n-        let reaped = registry.reap_expired(now_ms());\n-        if reaped.is_empty() {\n+    };\n+    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n+        Ok(registry) => registry,\n+        Err(err) => {\n+            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n             return;\n         }\n-        let snapshots = perch::serve_snapshots_dir();\n-        let mut bytes_removed: u64 = 0;\n-        for entry in &reaped {\n-            if entry.kind != spt_store::serving::ServedKind::Attachment {\n-                continue; // a reference entry's path is the user's own file\n-            }\n-            if !entry.path.starts_with(&snapshots) {\n-                eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n-                continue;\n-            }\n-            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n-            match std::fs::remove_file(&entry.path) {\n-                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n-                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n-                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n-            }\n+    };\n+    let reaped = registry.reap_expired(now_ms());\n+    if reaped.is_empty() {\n+        return;\n+    }\n+    let snapshots = perch::serve_snapshots_dir();\n+    let mut bytes_removed: u64 = 0;\n+    for entry in &reaped {\n+        if entry.kind != spt_store::serving::ServedKind::Attachment {\n+            continue; // a reference entry's path is the user's own file\n         }\n-        if let Err(err) = registry.save_at(&registry_path) {\n-            // The bytes are gone and the entries are not. Say so rather than\n-            // reporting a reap that only half happened; the next tick retries.\n-            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n-            return;\n+        if !entry.path.starts_with(&snapshots) {\n+            eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n+            continue;\n         }\n-        eprintln!(\n-            \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n-            reaped.len(),\n-            bytes_removed\n-        );\n-    });\n-    if let Err(err) = held {\n-        // A poisoned writer lock is not something a pulse may paper over: say\n-        // it every tick rather than reaping outside the lock.\n-        eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n+        let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n+        match std::fs::remove_file(&entry.path) {\n+            Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n+            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n+            Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n+        }\n+    }\n+    if let Err(err) = registry.save_at(&registry_path) {\n+        // The bytes are gone and the entries are not. Say so rather than\n+        // reporting a reap that only half happened; the next tick retries.\n+        eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n+        return;\n     }\n+    eprintln!(\n+        \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n+        reaped.len(),\n+        bytes_removed\n+    );\n }\n \n /// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\ndiff --git a/crates/spt-daemon/src/servehost.rs b/crates/spt-daemon/src/servehost.rs\nindex 377f4a64..e84c2e69 100644\n--- a/crates/spt-daemon/src/servehost.rs\n+++ b/crates/spt-daemon/src/servehost.rs\n@@ -3,7 +3,6 @@\n \n use std::io;\n use std::path::{Path, PathBuf};\n-use std::sync::Mutex;\n \n use serde::{Deserialize, Serialize};\n use spt_store::serving::{ServedEntry, ServingRegistry};\n@@ -15,10 +14,6 @@ use crate::transport::{send_hello, LocalSocketTransport};\n pub const KIND_SERVE_REQUEST: &str = \"serve_request\";\n pub const KIND_SERVE_RESULT: &str = \"serve_result\";\n \n-// Serializes load/change/publish across the control channel's connection threads.\n-// The HTTP reader observes an atomic persisted snapshot, never this lock.\n-static REGISTRY_WRITE: Mutex<()> = Mutex::new(());\n-\n /// An explicit local registry operation; no HTTP request can mutate exposure.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n #[derive(Debug, Clone, Serialize, Deserialize)]\n@@ -132,29 +127,10 @@ pub enum ServeResult {\n     LanRefused { code: String },\n }\n \n-/// Run `f` holding the ONE registry write lock, so a whole read-modify-write\n-/// pass is atomic against every other writer in this process.\n-///\n-/// The daemon has a second registry writer: the live host's TTL reaper, which\n-/// does load -> retain -> save on its own pulse. Without this it raced every\n-/// `serve add` — an entry registered between the reaper's load and its save was\n-/// clobbered by the reaper's stale snapshot and silently lost, on a 5s tick,\n-/// forever. The lock was already here; only this door into it was missing.\n-///\n-/// Callers must not re-enter `apply_at` from inside `f`: the mutex is not\n-/// reentrant, and doing so would deadlock the pulse thread.\n-// [impl->REQ-WEB-SERVING-REGISTRY]\n-// [impl->REQ-WEB-ATTACHMENT-PULL]\n-pub(crate) fn with_registry_write<R>(f: impl FnOnce() -> R) -> io::Result<R> {\n-    let _writer = REGISTRY_WRITE\n-        .lock()\n-        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n-    Ok(f())\n-}\n-\n /// Apply a request at an explicit home. Publication completes before success\n /// is returned, and a failed publication cannot advance an in-memory counter.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n     // [impl->REQ-WEB-URL-BOUND-PORT]\n     if matches!(&request, ServeRequest::DocsStatus) {\n@@ -168,10 +144,8 @@ pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::R\n     if let Some(result) = lan_request(home, &request) {\n         return Ok(result);\n     }\n-    let _writer = REGISTRY_WRITE\n-        .lock()\n-        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n     let path = spt_store::perch::serving_registry_file_in(home);\n+    let _writer = spt_store::serving::lock_registry_at(&path)?;\n     let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n         if matches!(&request, ServeRequest::AddInputReference { .. }) {\n             io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n@@ -658,7 +632,26 @@ mod tests {\n         }\n     }\n \n+    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+    #[test]\n+    fn unavailable_registry_lock_cannot_publish_an_add() {\n+        let tmp = tempfile::tempdir().unwrap();\n+        let home = tmp.path().join(\"home\");\n+        let source = tmp.path().join(\"report.md\");\n+        std::fs::write(&source, \"must not be registered\").unwrap();\n+        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n+        // A directory at the sentinel path deterministically refuses acquisition\n+        // on both supported platforms, without permission or timing assumptions.\n+        let mut lock_path = registry_path.as_os_str().to_os_string();\n+        lock_path.push(\".lock\");\n+        std::fs::create_dir_all(PathBuf::from(lock_path)).unwrap();\n+\n+        assert!(apply_at(&home, add(&source), 1).is_err());\n+        assert!(!registry_path.exists(), \"a failed lock must not permit publication\");\n+    }\n+\n     // [unit->REQ-WEB-SERVING-REGISTRY]\n+    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n     #[test]\n     fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\n         let tmp = tempfile::tempdir().unwrap();\n@@ -686,75 +679,6 @@ mod tests {\n         assert_eq!(names, [\"report.md\", \"report~1.md\"].into_iter().collect());\n     }\n \n-    /// A reap-shaped read-modify-write cannot clobber a concurrent `serve add`.\n-    ///\n-    /// The TTL reaper is the daemon's SECOND registry writer, and it runs on a\n-    /// pulse: load, retain, save. Outside the write lock, an entry registered\n-    /// between its load and its save is overwritten by its stale snapshot and\n-    /// silently lost — no error, no log line, once every five seconds forever.\n-    ///\n-    /// The reaping thread holds the lock across a deliberate pause, so the\n-    /// adding thread MUST be waiting on it. Without the lock the add lands in\n-    /// that window and the reaper's save drops it; with the lock the two passes\n-    /// serialize and both outcomes survive. No platform gate: the pause is a\n-    /// delay, not a scheduling assumption, and the assertion only holds one way.\n-    // [unit->REQ-WEB-ATTACHMENT-PULL]\n-    #[test]\n-    fn a_reap_shaped_pass_cannot_clobber_a_concurrent_add() {\n-        let tmp = tempfile::tempdir().unwrap();\n-        let home = tmp.path().join(\"home\");\n-        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n-        let doomed_source = tmp.path().join(\"doomed.md\");\n-        let added_source = tmp.path().join(\"added.md\");\n-        std::fs::write(&doomed_source, \"doomed\").unwrap();\n-        std::fs::write(&added_source, \"added\").unwrap();\n-\n-        // One entry already registered, with an already-expired lifetime.\n-        let ServeResult::Entry { entry } = apply_at(&home, add(&doomed_source), 1).unwrap() else {\n-            panic!(\"seed add did not return an entry\");\n-        };\n-        with_registry_write(|| {\n-            let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n-            registry.scope_entry(&entry.id, Some(1), None, None).expect(\"give it a spent ttl\");\n-            registry.save_at(&registry_path).unwrap();\n-        })\n-        .unwrap();\n-\n-        std::thread::scope(|scope| {\n-            let reaper = scope.spawn(|| {\n-                with_registry_write(|| {\n-                    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n-                    let reaped = registry.reap_expired(u64::MAX);\n-                    assert_eq!(reaped.len(), 1, \"the spent entry is what expired\");\n-                    // Held ACROSS the pause: this is the window the add must not\n-                    // be able to slip into.\n-                    std::thread::sleep(std::time::Duration::from_millis(300));\n-                    registry.save_at(&registry_path).unwrap();\n-                })\n-                .unwrap();\n-            });\n-            // Give the reaper the lock first, then race it.\n-            std::thread::sleep(std::time::Duration::from_millis(50));\n-            let adder = scope.spawn(|| apply_at(&home, add(&added_source), 2).unwrap());\n-            reaper.join().unwrap();\n-            adder.join().unwrap();\n-        });\n-\n-        let names: Vec<String> = ServingRegistry::load_at(&registry_path)\n-            .unwrap()\n-            .entries()\n-            .map(|e| e.served_name.clone())\n-            .collect();\n-        assert!(\n-            names.iter().any(|n| n == \"added.md\"),\n-            \"the concurrent add SURVIVED the reap: {names:?}\"\n-        );\n-        assert!(\n-            !names.iter().any(|n| n == \"doomed.md\"),\n-            \"and the expired entry was still reaped: {names:?}\"\n-        );\n-    }\n-\n     /// `AddScoped` is the FILE_ACCESS_HELPER's registration, and it had no cell\n     /// at all — neither for the file case nor this one. A quoted path can name a\n     /// DIRECTORY, and the helper must register it as one: the two halves (add a\ndiff --git a/crates/spt-store/Cargo.toml b/crates/spt-store/Cargo.toml\nindex 104b8321..d2b7fbe0 100644\n--- a/crates/spt-store/Cargo.toml\n+++ b/crates/spt-store/Cargo.toml\n@@ -16,6 +16,11 @@ description = \"spt-core persistence layer: atomic writes, spool, perch layout, r\n name = \"wtlock_worker\"\n path = \"tests/fixtures/wtlock_worker.rs\"\n \n+# Test-only second-process fixture for the serving-registry transaction lock.\n+[[bin]]\n+name = \"serving_registry_worker\"\n+path = \"tests/fixtures/serving_registry_worker.rs\"\n+\n [dependencies]\n fs2 = \"0.4\"\n # The ONE string-trigger match rule (`matchrule`), shared by monic triggers and\ndiff --git a/crates/spt-store/src/serving.rs b/crates/spt-store/src/serving.rs\n\n[Showing lines 1-300 of 360. Use :301 to continue]"}],"details":{"resolvedPath":"C:\\Users\\decid\\.omp\\agent\\sessions\\-Documents-projects-spt-core\\2026-09-13T10-23-45-921Z_01a09a4b-5f01-7000-9118-23ce74a49636\\494.bash-original.log","contentType":"text/plain","totalLines":360,"displayContent":{"text":"diff --git a/crates/spt-daemon/src/livehost.rs b/crates/spt-daemon/src/livehost.rs\nindex 151c5e2e..f2cda242 100644\n--- a/crates/spt-daemon/src/livehost.rs\n+++ b/crates/spt-daemon/src/livehost.rs\n@@ -1278,62 +1278,60 @@ fn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n /// containment check is belt-and-braces on that same point: a hand-edited or\n /// corrupted registry must not be able to aim a delete at an arbitrary path.\n // [impl->REQ-WEB-ATTACHMENT-PULL]\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n fn reap_expired_attachments() {\n-    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n-    // lock every other writer in this process holds, because this pass is a\n-    // read-modify-write on a file `serve add` also writes: an entry registered\n-    // between our load and our save would be clobbered by our stale snapshot\n-    // and silently lost, on a 5s tick, forever.\n-    let held = crate::servehost::with_registry_write(|| {\n-        let registry_path = perch::serving_registry_file();\n-        if !registry_path.exists() {\n-            return; // nothing has ever been served on this node\n+    let registry_path = perch::serving_registry_file();\n+    if !registry_path.exists() {\n+        return; // nothing has ever been served on this node\n+    }\n+    // The broker is a DIFFERENT process. Hold the shared sentinel from before\n+    // loading until snapshot cleanup and publication have both finished.\n+    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\n+        Ok(writer) => writer,\n+        Err(err) => {\n+            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n+            return; // never reap outside the lock\n         }\n-        let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n-            Ok(registry) => registry,\n-            Err(err) => {\n-                eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n-                return;\n-            }\n-        };\n-        let reaped = registry.reap_expired(now_ms());\n-        if reaped.is_empty() {\n+    };\n+    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n+        Ok(registry) => registry,\n+        Err(err) => {\n+            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n             return;\n         }\n-        let snapshots = perch::serve_snapshots_dir();\n-        let mut bytes_removed: u64 = 0;\n-        for entry in &reaped {\n-            if entry.kind != spt_store::serving::ServedKind::Attachment {\n-                continue; // a reference entry's path is the user's own file\n-            }\n-            if !entry.path.starts_with(&snapshots) {\n-                eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n-                continue;\n-            }\n-            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n-            match std::fs::remove_file(&entry.path) {\n-                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n-                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n-                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n-            }\n+    };\n+    let reaped = registry.reap_expired(now_ms());\n+    if reaped.is_empty() {\n+        return;\n+    }\n+    let snapshots = perch::serve_snapshots_dir();\n+    let mut bytes_removed: u64 = 0;\n+    for entry in &reaped {\n+        if entry.kind != spt_store::serving::ServedKind::Attachment {\n+            continue; // a reference entry's path is the user's own file\n         }\n-        if let Err(err) = registry.save_at(&registry_path) {\n-            // The bytes are gone and the entries are not. Say so rather than\n-            // reporting a reap that only half happened; the next tick retries.\n-            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n-            return;\n+        if !entry.path.starts_with(&snapshots) {\n+            eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n+            continue;\n         }\n-        eprintln!(\n-            \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n-            reaped.len(),\n-            bytes_removed\n-        );\n-    });\n-    if let Err(err) = held {\n-        // A poisoned writer lock is not something a pulse may paper over: say\n-        // it every tick rather than reaping outside the lock.\n-        eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n+        let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n+        match std::fs::remove_file(&entry.path) {\n+            Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n+            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n+            Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n+        }\n+    }\n+    if let Err(err) = registry.save_at(&registry_path) {\n+        // The bytes are gone and the entries are not. Say so rather than\n+        // reporting a reap that only half happened; the next tick retries.\n+        eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n+        return;\n     }\n+    eprintln!(\n+        \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n+        reaped.len(),\n+        bytes_removed\n+    );\n }\n \n /// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\ndiff --git a/crates/spt-daemon/src/servehost.rs b/crates/spt-daemon/src/servehost.rs\nindex 377f4a64..e84c2e69 100644\n--- a/crates/spt-daemon/src/servehost.rs\n+++ b/crates/spt-daemon/src/servehost.rs\n@@ -3,7 +3,6 @@\n \n use std::io;\n use std::path::{Path, PathBuf};\n-use std::sync::Mutex;\n \n use serde::{Deserialize, Serialize};\n use spt_store::serving::{ServedEntry, ServingRegistry};\n@@ -15,10 +14,6 @@ use crate::transport::{send_hello, LocalSocketTransport};\n pub const KIND_SERVE_REQUEST: &str = \"serve_request\";\n pub const KIND_SERVE_RESULT: &str = \"serve_result\";\n \n-// Serializes load/change/publish across the control channel's connection threads.\n-// The HTTP reader observes an atomic persisted snapshot, never this lock.\n-static REGISTRY_WRITE: Mutex<()> = Mutex::new(());\n-\n /// An explicit local registry operation; no HTTP request can mutate exposure.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n #[derive(Debug, Clone, Serialize, Deserialize)]\n@@ -132,29 +127,10 @@ pub enum ServeResult {\n     LanRefused { code: String },\n }\n \n-/// Run `f` holding the ONE registry write lock, so a whole read-modify-write\n-/// pass is atomic against every other writer in this process.\n-///\n-/// The daemon has a second registry writer: the live host's TTL reaper, which\n-/// does load -> retain -> save on its own pulse. Without this it raced every\n-/// `serve add` — an entry registered between the reaper's load and its save was\n-/// clobbered by the reaper's stale snapshot and silently lost, on a 5s tick,\n-/// forever. The lock was already here; only this door into it was missing.\n-///\n-/// Callers must not re-enter `apply_at` from inside `f`: the mutex is not\n-/// reentrant, and doing so would deadlock the pulse thread.\n-// [impl->REQ-WEB-SERVING-REGISTRY]\n-// [impl->REQ-WEB-ATTACHMENT-PULL]\n-pub(crate) fn with_registry_write<R>(f: impl FnOnce() -> R) -> io::Result<R> {\n-    let _writer = REGISTRY_WRITE\n-        .lock()\n-        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n-    Ok(f())\n-}\n-\n /// Apply a request at an explicit home. Publication completes before success\n /// is returned, and a failed publication cannot advance an in-memory counter.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n     // [impl->REQ-WEB-URL-BOUND-PORT]\n     if matches!(&request, ServeRequest::DocsStatus) {\n@@ -168,10 +144,8 @@ pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::R\n     if let Some(result) = lan_request(home, &request) {\n         return Ok(result);\n     }\n-    let _writer = REGISTRY_WRITE\n-        .lock()\n-        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n     let path = spt_store::perch::serving_registry_file_in(home);\n+    let _writer = spt_store::serving::lock_registry_at(&path)?;\n     let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n         if matches!(&request, ServeRequest::AddInputReference { .. }) {\n             io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n@@ -658,7 +632,26 @@ mod tests {\n         }\n     }\n \n+    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+    #[test]\n+    fn unavailable_registry_lock_cannot_publish_an_add() {\n+        let tmp = tempfile::tempdir().unwrap();\n+        let home = tmp.path().join(\"home\");\n+        let source = tmp.path().join(\"report.md\");\n+        std::fs::write(&source, \"must not be registered\").unwrap();\n+        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n+        // A directory at the sentinel path deterministically refuses acquisition\n+        // on both supported platforms, without permission or timing assumptions.\n+        let mut lock_path = registry_path.as_os_str().to_os_string();\n+        lock_path.push(\".lock\");\n+        std::fs::create_dir_all(PathBuf::from(lock_path)).unwrap();\n+\n+        assert!(apply_at(&home, add(&source), 1).is_err());\n+        assert!(!registry_path.exists(), \"a failed lock must not permit publication\");\n+    }\n+\n     // [unit->REQ-WEB-SERVING-REGISTRY]\n+    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n     #[test]\n     fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\n         let tmp = tempfile::tempdir().unwrap();\n@@ -686,75 +679,6 @@ mod tests {\n         assert_eq!(names, [\"report.md\", \"report~1.md\"].into_iter().collect());\n     }\n \n-    /// A reap-shaped read-modify-write cannot clobber a concurrent `serve add`.\n-    ///\n-    /// The TTL reaper is the daemon's SECOND registry writer, and it runs on a\n-    /// pulse: load, retain, save. Outside the write lock, an entry registered\n-    /// between its load and its save is overwritten by its stale snapshot and\n-    /// silently lost — no error, no log line, once every five seconds forever.\n-    ///\n-    /// The reaping thread holds the lock across a deliberate pause, so the\n-    /// adding thread MUST be waiting on it. Without the lock the add lands in\n-    /// that window and the reaper's save drops it; with the lock the two passes\n-    /// serialize and both outcomes survive. No platform gate: the pause is a\n-    /// delay, not a scheduling assumption, and the assertion only holds one way.\n-    // [unit->REQ-WEB-ATTACHMENT-PULL]\n-    #[test]\n-    fn a_reap_shaped_pass_cannot_clobber_a_concurrent_add() {\n-        let tmp = tempfile::tempdir().unwrap();\n-        let home = tmp.path().join(\"home\");\n-        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n-        let doomed_source = tmp.path().join(\"doomed.md\");\n-        let added_source = tmp.path().join(\"added.md\");\n-        std::fs::write(&doomed_source, \"doomed\").unwrap();\n-        std::fs::write(&added_source, \"added\").unwrap();\n-\n-        // One entry already registered, with an already-expired lifetime.\n-        let ServeResult::Entry { entry } = apply_at(&home, add(&doomed_source), 1).unwrap() else {\n-            panic!(\"seed add did not return an entry\");\n-        };\n-        with_registry_write(|| {\n-            let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n-            registry.scope_entry(&entry.id, Some(1), None, None).expect(\"give it a spent ttl\");\n-            registry.save_at(&registry_path).unwrap();\n-        })\n-        .unwrap();\n-\n-        std::thread::scope(|scope| {\n-            let reaper = scope.spawn(|| {\n-                with_registry_write(|| {\n-                    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n-                    let reaped = registry.reap_expired(u64::MAX);\n-                    assert_eq!(reaped.len(), 1, \"the spent entry is what expired\");\n-                    // Held ACROSS the pause: this is the window the add must not\n-                    // be able to slip into.\n-                    std::thread::sleep(std::time::Duration::from_millis(300));\n-                    registry.save_at(&registry_path).unwrap();\n-                })\n-                .unwrap();\n-            });\n-            // Give the reaper the lock first, then race it.\n-            std::thread::sleep(std::time::Duration::from_millis(50));\n-            let adder = scope.spawn(|| apply_at(&home, add(&added_source), 2).unwrap());\n-            reaper.join().unwrap();\n-            adder.join().unwrap();\n-        });\n-\n-        let names: Vec<String> = ServingRegistry::load_at(&registry_path)\n-            .unwrap()\n-            .entries()\n-            .map(|e| e.served_name.clone())\n-            .collect();\n-        assert!(\n-            names.iter().any(|n| n == \"added.md\"),\n-            \"the concurrent add SURVIVED the reap: {names:?}\"\n-        );\n-        assert!(\n-            !names.iter().any(|n| n == \"doomed.md\"),\n-            \"and the expired entry was still reaped: {names:?}\"\n-        );\n-    }\n-\n     /// `AddScoped` is the FILE_ACCESS_HELPER's registration, and it had no cell\n     /// at all — neither for the file case nor this one. A quoted path can name a\n     /// DIRECTORY, and the helper must register it as one: the two halves (add a\ndiff --git a/crates/spt-store/Cargo.toml b/crates/spt-store/Cargo.toml\nindex 104b8321..d2b7fbe0 100644\n--- a/crates/spt-store/Cargo.toml\n+++ b/crates/spt-store/Cargo.toml\n@@ -16,6 +16,11 @@ description = \"spt-core persistence layer: atomic writes, spool, perch layout, r\n name = \"wtlock_worker\"\n path = \"tests/fixtures/wtlock_worker.rs\"\n \n+# Test-only second-process fixture for the serving-registry transaction lock.\n+[[bin]]\n+name = \"serving_registry_worker\"\n+path = \"tests/fixtures/serving_registry_worker.rs\"\n+\n [dependencies]\n fs2 = \"0.4\"\n # The ONE string-trigger match rule (`matchrule`), shared by monic triggers and\ndiff --git a/crates/spt-store/src/serving.rs b/crates/spt-store/src/serving.rs","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300]},"truncation":{"content":"diff --git a/crates/spt-daemon/src/livehost.rs b/crates/spt-daemon/src/livehost.rs\nindex 151c5e2e..f2cda242 100644\n--- a/crates/spt-daemon/src/livehost.rs\n+++ b/crates/spt-daemon/src/livehost.rs\n@@ -1278,62 +1278,60 @@ fn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n /// containment check is belt-and-braces on that same point: a hand-edited or\n /// corrupted registry must not be able to aim a delete at an arbitrary path.\n // [impl->REQ-WEB-ATTACHMENT-PULL]\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n fn reap_expired_attachments() {\n-    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n-    // lock every other writer in this process holds, because this pass is a\n-    // read-modify-write on a file `serve add` also writes: an entry registered\n-    // between our load and our save would be clobbered by our stale snapshot\n-    // and silently lost, on a 5s tick, forever.\n-    let held = crate::servehost::with_registry_write(|| {\n-        let registry_path = perch::serving_registry_file();\n-        if !registry_path.exists() {\n-            return; // nothing has ever been served on this node\n+    let registry_path = perch::serving_registry_file();\n+    if !registry_path.exists() {\n+        return; // nothing has ever been served on this node\n+    }\n+    // The broker is a DIFFERENT process. Hold the shared sentinel from before\n+    // loading until snapshot cleanup and publication have both finished.\n+    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\n+        Ok(writer) => writer,\n+        Err(err) => {\n+            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n+            return; // never reap outside the lock\n         }\n-        let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n-            Ok(registry) => registry,\n-            Err(err) => {\n-                eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n-                return;\n-            }\n-        };\n-        let reaped = registry.reap_expired(now_ms());\n-        if reaped.is_empty() {\n+    };\n+    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n+        Ok(registry) => registry,\n+        Err(err) => {\n+            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n             return;\n         }\n-        let snapshots = perch::serve_snapshots_dir();\n-        let mut bytes_removed: u64 = 0;\n-        for entry in &reaped {\n-            if entry.kind != spt_store::serving::ServedKind::Attachment {\n-                continue; // a reference entry's path is the user's own file\n-            }\n-            if !entry.path.starts_with(&snapshots) {\n-                eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n-                continue;\n-            }\n-            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n-            match std::fs::remove_file(&entry.path) {\n-                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n-                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n-                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n-            }\n+    };\n+    let reaped = registry.reap_expired(now_ms());\n+    if reaped.is_empty() {\n+        return;\n+    }\n+    let snapshots = perch::serve_snapshots_dir();\n+    let mut bytes_removed: u64 = 0;\n+    for entry in &reaped {\n+        if entry.kind != spt_store::serving::ServedKind::Attachment {\n+            continue; // a reference entry's path is the user's own file\n         }\n-        if let Err(err) = registry.save_at(&registry_path) {\n-            // The bytes are gone and the entries are not. Say so rather than\n-            // reporting a reap that only half happened; the next tick retries.\n-            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n-            return;\n+        if !entry.path.starts_with(&snapshots) {\n+            eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n+            continue;\n         }\n-        eprintln!(\n-            \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n-            reaped.len(),\n-            bytes_removed\n-        );\n-    });\n-    if let Err(err) = held {\n-        // A poisoned writer lock is not something a pulse may paper over: say\n-        // it every tick rather than reaping outside the lock.\n-        eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n+        let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n+        match std::fs::remove_file(&entry.path) {\n+            Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n+            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n+            Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n+        }\n+    }\n+    if let Err(err) = registry.save_at(&registry_path) {\n+        // The bytes are gone and the entries are not. Say so rather than\n+        // reporting a reap that only half happened; the next tick retries.\n+        eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n+        return;\n     }\n+    eprintln!(\n+        \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n+        reaped.len(),\n+        bytes_removed\n+    );\n }\n \n /// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\ndiff --git a/crates/spt-daemon/src/servehost.rs b/crates/spt-daemon/src/servehost.rs\nindex 377f4a64..e84c2e69 100644\n--- a/crates/spt-daemon/src/servehost.rs\n+++ b/crates/spt-daemon/src/servehost.rs\n@@ -3,7 +3,6 @@\n \n use std::io;\n use std::path::{Path, PathBuf};\n-use std::sync::Mutex;\n \n use serde::{Deserialize, Serialize};\n use spt_store::serving::{ServedEntry, ServingRegistry};\n@@ -15,10 +14,6 @@ use crate::transport::{send_hello, LocalSocketTransport};\n pub const KIND_SERVE_REQUEST: &str = \"serve_request\";\n pub const KIND_SERVE_RESULT: &str = \"serve_result\";\n \n-// Serializes load/change/publish across the control channel's connection threads.\n-// The HTTP reader observes an atomic persisted snapshot, never this lock.\n-static REGISTRY_WRITE: Mutex<()> = Mutex::new(());\n-\n /// An explicit local registry operation; no HTTP request can mutate exposure.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n #[derive(Debug, Clone, Serialize, Deserialize)]\n@@ -132,29 +127,10 @@ pub enum ServeResult {\n     LanRefused { code: String },\n }\n \n-/// Run `f` holding the ONE registry write lock, so a whole read-modify-write\n-/// pass is atomic against every other writer in this process.\n-///\n-/// The daemon has a second registry writer: the live host's TTL reaper, which\n-/// does load -> retain -> save on its own pulse. Without this it raced every\n-/// `serve add` — an entry registered between the reaper's load and its save was\n-/// clobbered by the reaper's stale snapshot and silently lost, on a 5s tick,\n-/// forever. The lock was already here; only this door into it was missing.\n-///\n-/// Callers must not re-enter `apply_at` from inside `f`: the mutex is not\n-/// reentrant, and doing so would deadlock the pulse thread.\n-// [impl->REQ-WEB-SERVING-REGISTRY]\n-// [impl->REQ-WEB-ATTACHMENT-PULL]\n-pub(crate) fn with_registry_write<R>(f: impl FnOnce() -> R) -> io::Result<R> {\n-    let _writer = REGISTRY_WRITE\n-        .lock()\n-        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n-    Ok(f())\n-}\n-\n /// Apply a request at an explicit home. Publication completes before success\n /// is returned, and a failed publication cannot advance an in-memory counter.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n     // [impl->REQ-WEB-URL-BOUND-PORT]\n     if matches!(&request, ServeRequest::DocsStatus) {\n@@ -168,10 +144,8 @@ pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::R\n     if let Some(result) = lan_request(home, &request) {\n         return Ok(result);\n     }\n-    let _writer = REGISTRY_WRITE\n-        .lock()\n-        .map_err(|_| io::Error::other(\"SERVE_REGISTRY_POISONED: writer state is unproven\"))?;\n     let path = spt_store::perch::serving_registry_file_in(home);\n+    let _writer = spt_store::serving::lock_registry_at(&path)?;\n     let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n         if matches!(&request, ServeRequest::AddInputReference { .. }) {\n             io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n@@ -658,7 +632,26 @@ mod tests {\n         }\n     }\n \n+    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+    #[test]\n+    fn unavailable_registry_lock_cannot_publish_an_add() {\n+        let tmp = tempfile::tempdir().unwrap();\n+        let home = tmp.path().join(\"home\");\n+        let source = tmp.path().join(\"report.md\");\n+        std::fs::write(&source, \"must not be registered\").unwrap();\n+        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n+        // A directory at the sentinel path deterministically refuses acquisition\n+        // on both supported platforms, without permission or timing assumptions.\n+        let mut lock_path = registry_path.as_os_str().to_os_string();\n+        lock_path.push(\".lock\");\n+        std::fs::create_dir_all(PathBuf::from(lock_path)).unwrap();\n+\n+        assert!(apply_at(&home, add(&source), 1).is_err());\n+        assert!(!registry_path.exists(), \"a failed lock must not permit publication\");\n+    }\n+\n     // [unit->REQ-WEB-SERVING-REGISTRY]\n+    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n     #[test]\n     fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\n         let tmp = tempfile::tempdir().unwrap();\n@@ -686,75 +679,6 @@ mod tests {\n         assert_eq!(names, [\"report.md\", \"report~1.md\"].into_iter().collect());\n     }\n \n-    /// A reap-shaped read-modify-write cannot clobber a concurrent `serve add`.\n-    ///\n-    /// The TTL reaper is the daemon's SECOND registry writer, and it runs on a\n-    /// pulse: load, retain, save. Outside the write lock, an entry registered\n-    /// between its load and its save is overwritten by its stale snapshot and\n-    /// silently lost — no error, no log line, once every five seconds forever.\n-    ///\n-    /// The reaping thread holds the lock across a deliberate pause, so the\n-    /// adding thread MUST be waiting on it. Without the lock the add lands in\n-    /// that window and the reaper's save drops it; with the lock the two passes\n-    /// serialize and both outcomes survive. No platform gate: the pause is a\n-    /// delay, not a scheduling assumption, and the assertion only holds one way.\n-    // [unit->REQ-WEB-ATTACHMENT-PULL]\n-    #[test]\n-    fn a_reap_shaped_pass_cannot_clobber_a_concurrent_add() {\n-        let tmp = tempfile::tempdir().unwrap();\n-        let home = tmp.path().join(\"home\");\n-        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n-        let doomed_source = tmp.path().join(\"doomed.md\");\n-        let added_source = tmp.path().join(\"added.md\");\n-        std::fs::write(&doomed_source, \"doomed\").unwrap();\n-        std::fs::write(&added_source, \"added\").unwrap();\n-\n-        // One entry already registered, with an already-expired lifetime.\n-        let ServeResult::Entry { entry } = apply_at(&home, add(&doomed_source), 1).unwrap() else {\n-            panic!(\"seed add did not return an entry\");\n-        };\n-        with_registry_write(|| {\n-            let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n-            registry.scope_entry(&entry.id, Some(1), None, None).expect(\"give it a spent ttl\");\n-            registry.save_at(&registry_path).unwrap();\n-        })\n-        .unwrap();\n-\n-        std::thread::scope(|scope| {\n-            let reaper = scope.spawn(|| {\n-                with_registry_write(|| {\n-                    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n-                    let reaped = registry.reap_expired(u64::MAX);\n-                    assert_eq!(reaped.len(), 1, \"the spent entry is what expired\");\n-                    // Held ACROSS the pause: this is the window the add must not\n-                    // be able to slip into.\n-                    std::thread::sleep(std::time::Duration::from_millis(300));\n-                    registry.save_at(&registry_path).unwrap();\n-                })\n-                .unwrap();\n-            });\n-            // Give the reaper the lock first, then race it.\n-            std::thread::sleep(std::time::Duration::from_millis(50));\n-            let adder = scope.spawn(|| apply_at(&home, add(&added_source), 2).unwrap());\n-            reaper.join().unwrap();\n-            adder.join().unwrap();\n-        });\n-\n-        let names: Vec<String> = ServingRegistry::load_at(&registry_path)\n-            .unwrap()\n-            .entries()\n-            .map(|e| e.served_name.clone())\n-            .collect();\n-        assert!(\n-            names.iter().any(|n| n == \"added.md\"),\n-            \"the concurrent add SURVIVED the reap: {names:?}\"\n-        );\n-        assert!(\n-            !names.iter().any(|n| n == \"doomed.md\"),\n-            \"and the expired entry was still reaped: {names:?}\"\n-        );\n-    }\n-\n     /// `AddScoped` is the FILE_ACCESS_HELPER's registration, and it had no cell\n     /// at all — neither for the file case nor this one. A quoted path can name a\n     /// DIRECTORY, and the helper must register it as one: the two halves (add a\ndiff --git a/crates/spt-store/Cargo.toml b/crates/spt-store/Cargo.toml\nindex 104b8321..d2b7fbe0 100644\n--- a/crates/spt-store/Cargo.toml\n+++ b/crates/spt-store/Cargo.toml\n@@ -16,6 +16,11 @@ description = \"spt-core persistence layer: atomic writes, spool, perch layout, r\n name = \"wtlock_worker\"\n path = \"tests/fixtures/wtlock_worker.rs\"\n \n+# Test-only second-process fixture for the serving-registry transaction lock.\n+[[bin]]\n+name = \"serving_registry_worker\"\n+path = \"tests/fixtures/serving_registry_worker.rs\"\n+\n [dependencies]\n fs2 = \"0.4\"\n # The ONE string-trigger match rule (`matchrule`), shared by monic triggers and\ndiff --git a/crates/spt-store/src/serving.rs b/crates/spt-store/src/serving.rs","truncated":true,"truncatedBy":"lines","totalLines":360,"totalBytes":14169,"outputLines":300,"outputBytes":14169,"lastLinePartial":false,"firstLineExceedsLimit":false},"meta":{"source":{"type":"internal","value":"artifact://494"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":360,"totalBytes":14169,"outputLines":300,"outputBytes":14169,"maxBytes":153600,"shownRange":{"start":1,"end":300},"nextOffset":301}}},"isError":false,"timestamp":1789522895866}}
{"type":"message","id":"d0cf7de0","parentId":"a3600ee3","timestamp":"2026-09-16T01:41:35.868Z","message":{"role":"toolResult","toolCallId":"call_CU2qOLJpDSgIKK9OLip31mKF|fc_0cdf94810c888815016aa9f3cfc69887d19791240b9287a94e","toolName":"read","content":[{"type":"text","text":"diff --git a/crates/spt-store/tests/fixtures/serving_registry_worker.rs b/crates/spt-store/tests/fixtures/serving_registry_worker.rs\nnew file mode 100644\nindex 00000000..01c3b94b\n--- /dev/null\n+++ b/crates/spt-store/tests/fixtures/serving_registry_worker.rs\n@@ -0,0 +1,105 @@\n+//! Test-only serving-registry writer. All IPC is explicit, per-child file\n+//! barriers under a temporary directory; polling delays never order transactions.\n+//! Usage: serving_registry_worker <reap|register> <hold|wait> <registry> <barriers> <source>\n+\n+use std::fs::{self, File, OpenOptions};\n+use std::io;\n+use std::path::Path;\n+use std::time::{Duration, Instant};\n+\n+use spt_store::serving::{lock_registry_at, ServedKind, ServingRegistry};\n+\n+const COMMAND_BOUND: Duration = Duration::from_secs(120);\n+\n+fn announce(barriers: &Path, event: &str) -> io::Result<()> {\n+    fs::write(barriers.join(event), b\"\")\n+}\n+\n+fn await_command(barriers: &Path, command: &str) -> io::Result<()> {\n+    let deadline = Instant::now() + COMMAND_BOUND;\n+    loop {\n+        if barriers.join(command).try_exists()? {\n+            return Ok(());\n+        }\n+        if Instant::now() >= deadline {\n+            return Err(io::Error::new(io::ErrorKind::TimedOut, format!(\"waiting for {command}\")));\n+        }\n+        std::thread::sleep(Duration::from_millis(10));\n+    }\n+}\n+\n+/// Probe a fresh handle to the documented sentinel, not the atomically replaced\n+/// registry inode. Only genuine lock contention counts as the exclusion witness.\n+fn witness_exclusion(registry_path: &Path) -> io::Result<()> {\n+    let mut sentinel = registry_path.as_os_str().to_os_string();\n+    sentinel.push(\".lock\");\n+    let probe = OpenOptions::new().read(true).write(true).open(Path::new(&sentinel))?;\n+    match fs2::FileExt::try_lock_exclusive(&probe) {\n+        Ok(()) => Err(io::Error::other(\"another process reported HELD but its sentinel was unlocked\")),\n+        Err(error) if error.raw_os_error() == fs2::lock_contended_error().raw_os_error() => Ok(()),\n+        Err(error) => Err(error),\n+    }\n+}\n+\n+/// Keep the witness and pre-acquire barrier inside this call: moving a load\n+/// above this call deterministically captures the holder's unpublished state,\n+/// rather than depending on how quickly the parent schedules the next command.\n+fn acquire(registry_path: &Path, barriers: &Path, wait: bool) -> io::Result<File> {\n+    if wait {\n+        witness_exclusion(registry_path)?;\n+        announce(barriers, \"blocked\")?;\n+        // Re-probe after the holder has atomically replaced the registry, while\n+        // it still owns its guard. A lock on the replaced inode is insufficient.\n+        await_command(barriers, \"probe-published\")?;\n+        witness_exclusion(registry_path)?;\n+        announce(barriers, \"blocked-after-publish\")?;\n+        await_command(barriers, \"acquire\")?;\n+    }\n+    lock_registry_at(registry_path)\n+}\n+\n+fn run() -> io::Result<()> {\n+    let args: Vec<_> = std::env::args_os().collect();\n+    if args.len() != 6 {\n+        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected mode, role, registry, barriers, source\"));\n+    }\n+    let mode = args[1].to_str().ok_or_else(|| io::Error::other(\"invalid mode\"))?;\n+    if mode != \"reap\" && mode != \"register\" {\n+        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected reap or register\"));\n+    }\n+    let wait = match args[2].to_str() {\n+        Some(\"hold\") => false,\n+        Some(\"wait\") => true,\n+        _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected hold or wait\")),\n+    };\n+    let registry_path = Path::new(&args[3]);\n+    let barriers = Path::new(&args[4]);\n+    let source = Path::new(&args[5]);\n+\n+    let guard = acquire(registry_path, barriers, wait)?;\n+    let mut registry = ServingRegistry::load_at(registry_path)?;\n+    announce(barriers, \"loaded\")?;\n+    await_command(barriers, \"mutate\")?;\n+    if mode == \"reap\" {\n+        for entry in registry.reap_expired(1_000) {\n+            if entry.kind == ServedKind::Attachment {\n+                fs::remove_file(&entry.path)?;\n+            }\n+        }\n+    } else {\n+        registry.add_reference(source, Some(\"report.txt\"), None, 1_000)?;\n+    }\n+    registry.save_at(registry_path)?;\n+    announce(barriers, \"saved\")?;\n+    // The guard deliberately spans publication and the explicit release barrier.\n+    await_command(barriers, \"release\")?;\n+    drop(guard);\n+    Ok(())\n+}\n+\n+fn main() {\n+    if let Err(error) = run() {\n+        eprintln!(\"serving registry worker: {error}\");\n+        std::process::exit(1);\n+    }\n+}\ndiff --git a/crates/spt-store/tests/serving_registry_two_process_int.rs b/crates/spt-store/tests/serving_registry_two_process_int.rs\nnew file mode 100644\nindex 00000000..111a5e8d\n--- /dev/null\n+++ b/crates/spt-store/tests/serving_registry_two_process_int.rs\n@@ -0,0 +1,212 @@\n+//! Cross-process storage-contract proof for releases#308. These children use the\n+//! public registry API, not the broker/brain entry points; daemon callsite coverage\n+//! belongs to their own tests. Fixed logical time makes expiry deterministic.\n+\n+use std::fs;\n+use std::io;\n+use std::path::{Path, PathBuf};\n+use std::process::{Child, Command, ExitStatus, Stdio};\n+use std::time::{Duration, Instant};\n+\n+use spt_store::serving::{lock_registry_at, ServingRegistry};\n+\n+const CHILD_BOUND: Duration = Duration::from_secs(120);\n+const REAP_BOUND: Duration = Duration::from_secs(10);\n+\n+/// Own exactly the child we spawned, before any fallible readiness/assertion\n+/// work. No image-name sweeping, inherited pipes, or descendant processes.\n+struct RegistryWorker {\n+    child: Child,\n+    barriers: PathBuf,\n+}\n+\n+impl RegistryWorker {\n+    fn spawn(mode: &str, role: &str, registry: &Path, barriers: PathBuf, source: &Path) -> Self {\n+        fs::create_dir_all(&barriers).expect(\"worker barrier directory\");\n+        let child = Command::new(env!(\"CARGO_BIN_EXE_serving_registry_worker\"))\n+            .args([mode, role])\n+            .arg(registry)\n+            .arg(&barriers)\n+            .arg(source)\n+            .stdin(Stdio::null())\n+            .stdout(Stdio::null())\n+            .stderr(Stdio::inherit())\n+            .spawn()\n+            .expect(\"spawn serving registry worker\");\n+        Self { child, barriers }\n+    }\n+\n+    fn command(&self, command: &str) {\n+        fs::write(self.barriers.join(command), b\"\").expect(\"publish worker command\");\n+    }\n+\n+    fn await_event(&mut self, event: &str) {\n+        let deadline = Instant::now() + CHILD_BOUND;\n+        loop {\n+            if self.barriers.join(event).try_exists().expect(\"read worker event\") {\n+                return;\n+            }\n+            if let Some(status) = self.child.try_wait().expect(\"poll worker\") {\n+                panic!(\"worker exited {status} before {event}\");\n+            }\n+            assert!(Instant::now() < deadline, \"worker did not announce {event}\");\n+            // Polling only: the event, never elapsed time, establishes ordering.\n+            std::thread::sleep(Duration::from_millis(10));\n+        }\n+    }\n+\n+    fn wait_exit(&mut self, bound: Duration) -> io::Result<ExitStatus> {\n+        let deadline = Instant::now() + bound;\n+        loop {\n+            if let Some(status) = self.child.try_wait()? {\n+                return Ok(status);\n+            }\n+            if Instant::now() >= deadline {\n+                return Err(io::Error::new(io::ErrorKind::TimedOut, \"worker did not exit\"));\n+            }\n+            std::thread::sleep(Duration::from_millis(10));\n+        }\n+    }\n+\n+    fn finish(&mut self) {\n+        let status = self.wait_exit(CHILD_BOUND).expect(\"worker completion deadline\");\n+        assert!(status.success(), \"worker failed: {status}\");\n+    }\n+\n+    fn kill_and_reap(&mut self) -> io::Result<ExitStatus> {\n+        if let Some(status) = self.child.try_wait()? {\n+            return Ok(status);\n+        }\n+        if let Err(error) = self.child.kill() {\n+            // A natural exit may race the kill; do not report that as a leak.\n+            if let Some(status) = self.child.try_wait()? {\n+                return Ok(status);\n+            }\n+            return Err(error);\n+        }\n+        self.wait_exit(REAP_BOUND)\n+    }\n+}\n+\n+impl Drop for RegistryWorker {\n+    fn drop(&mut self) {\n+        if let Err(error) = self.kill_and_reap() {\n+            // Cleanup must not double-panic during assertion unwinding. A real\n+            // OS kill/reap failure is reported rather than silently swallowed.\n+            eprintln!(\"failed to reap registry worker {}: {error}\", self.child.id());\n+        }\n+    }\n+}\n+\n+fn overlap(holder_mode: &str, abrupt_exit: bool) {\n+    let temp = tempfile::tempdir().expect(\"isolated registry home\");\n+    let home = temp.path();\n+    // Cover the canonical name and an extension-bearing explicit path. The\n+    // sentinel must append .lock, not replace an existing extension.\n+    let registry_path = home.join(\"serve\").join(if holder_mode == \"reap\" { \"registry\" } else { \"registry.json\" });\n+    let expired_path = home.join(\"expired.snapshot\");\n+    let retired_path = home.join(\"retired.txt\");\n+    let registered_path = home.join(\"new.txt\");\n+    let third_path = home.join(\"third.txt\");\n+    for path in [&expired_path, &retired_path, &registered_path, &third_path] {\n+        fs::write(path, b\"fixture bytes\").expect(\"fixture source\");\n+    }\n+    let expired;\n+    let retired;\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"seed guard\");\n+        let mut seed = ServingRegistry::load_at(&registry_path).expect(\"empty registry\");\n+        expired = seed.add_attachment(&expired_path, \"report.txt\", None, 10, None, 100)\n+            .expect(\"expiring bare name\");\n+        retired = seed.add_reference(&retired_path, Some(\"report.txt\"), None, 100)\n+            .expect(\"historical suffixed name\");\n+        assert_eq!(expired.served_name, \"report.txt\");\n+        assert_eq!(retired.served_name, \"report~1.txt\");\n+        seed.remove(&retired.id).expect(\"retire suffix without forgetting owner\");\n+        seed.save_at(&registry_path).expect(\"seed publication\");\n+    }\n+\n+    let waiter_mode = if holder_mode == \"reap\" { \"register\" } else { \"reap\" };\n+    let mut holder = RegistryWorker::spawn(holder_mode, \"hold\", &registry_path, home.join(\"holder\"), &registered_path);\n+    holder.await_event(\"loaded\");\n+    let mut waiter = RegistryWorker::spawn(waiter_mode, \"wait\", &registry_path, home.join(\"waiter\"), &registered_path);\n+    // A real fs2 try-lock must fail with the platform's contention error while\n+    // the other PROCESS confirms its loaded transaction owns the guard.\n+    waiter.await_event(\"blocked\");\n+    holder.command(\"mutate\");\n+    holder.await_event(\"saved\");\n+    waiter.command(\"probe-published\");\n+    waiter.await_event(\"blocked-after-publish\");\n+    waiter.command(\"acquire\");\n+    if abrupt_exit {\n+        assert!(holder.child.try_wait().expect(\"holder still alive\").is_none(), \"holder exited before the abrupt-exit command\");\n+        holder.child.kill().expect(\"force-kill lock holder\");\n+        let status = holder.wait_exit(REAP_BOUND).expect(\"reap killed lock holder\");\n+        assert!(!status.success(), \"holder must exit abruptly without releasing its Rust guard\");\n+    } else {\n+        holder.command(\"release\");\n+        holder.finish();\n+    }\n+    waiter.await_event(\"loaded\");\n+    waiter.command(\"mutate\");\n+    waiter.await_event(\"saved\");\n+    waiter.command(\"release\");\n+    waiter.finish();\n+\n+    // Both orders matter: a stale reaper erases the acknowledged registration;\n+    // a stale registrar resurrects the expired entry after its snapshot is gone.\n+    let final_registry = ServingRegistry::load_at(&registry_path).expect(\"read durable final state\");\n+    assert!(final_registry.get(&expired.id).is_none(), \"expired entry was resurrected by a stale writer\");\n+    assert!(!expired_path.try_exists().expect(\"snapshot existence\"), \"reap must remove expired snapshot bytes\");\n+    let registration = final_registry.get(\"report~2.txt\").expect(\"acknowledged registration survives later publication\");\n+    assert_eq!(registration.path, registered_path);\n+    assert_eq!(final_registry.entries().count(), 1, \"only the new registration remains exposed\");\n+    let registration_id = registration.id.clone();\n+\n+    // Observe history through future assignments, not private JSON fields. A\n+    // save/reload after retirement must retain BOTH ownership and next suffix.\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"retirement guard\");\n+        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"fresh retirement load\");\n+        registry.remove(&registration_id).expect(\"retire acknowledged registration\");\n+        registry.save_at(&registry_path).expect(\"persist retirement\");\n+    }\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"history guard\");\n+        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"reload allocation history\");\n+        let reclaimed = registry.add_reference(&registered_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"reclaim recent owner\");\n+        assert_eq!(reclaimed.served_name, \"report~2.txt\", \"retiring must preserve the new owner's name\");\n+        let old_owner = registry.add_reference(&retired_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"reclaim historical owner\");\n+        assert_eq!(old_owner.served_name, \"report~1.txt\", \"reap must preserve previously retired ownership\");\n+        let next = registry.add_reference(&third_path, Some(\"report.txt\"), None, 2_000)\n\n[Showing lines 1-300 of 460. Use :301 to continue]"}],"details":{"resolvedPath":"C:\\Users\\decid\\.omp\\agent\\sessions\\-Documents-projects-spt-core\\2026-09-13T10-23-45-921Z_01a09a4b-5f01-7000-9118-23ce74a49636\\495.bash-original.log","contentType":"text/plain","totalLines":460,"displayContent":{"text":"diff --git a/crates/spt-store/tests/fixtures/serving_registry_worker.rs b/crates/spt-store/tests/fixtures/serving_registry_worker.rs\nnew file mode 100644\nindex 00000000..01c3b94b\n--- /dev/null\n+++ b/crates/spt-store/tests/fixtures/serving_registry_worker.rs\n@@ -0,0 +1,105 @@\n+//! Test-only serving-registry writer. All IPC is explicit, per-child file\n+//! barriers under a temporary directory; polling delays never order transactions.\n+//! Usage: serving_registry_worker <reap|register> <hold|wait> <registry> <barriers> <source>\n+\n+use std::fs::{self, File, OpenOptions};\n+use std::io;\n+use std::path::Path;\n+use std::time::{Duration, Instant};\n+\n+use spt_store::serving::{lock_registry_at, ServedKind, ServingRegistry};\n+\n+const COMMAND_BOUND: Duration = Duration::from_secs(120);\n+\n+fn announce(barriers: &Path, event: &str) -> io::Result<()> {\n+    fs::write(barriers.join(event), b\"\")\n+}\n+\n+fn await_command(barriers: &Path, command: &str) -> io::Result<()> {\n+    let deadline = Instant::now() + COMMAND_BOUND;\n+    loop {\n+        if barriers.join(command).try_exists()? {\n+            return Ok(());\n+        }\n+        if Instant::now() >= deadline {\n+            return Err(io::Error::new(io::ErrorKind::TimedOut, format!(\"waiting for {command}\")));\n+        }\n+        std::thread::sleep(Duration::from_millis(10));\n+    }\n+}\n+\n+/// Probe a fresh handle to the documented sentinel, not the atomically replaced\n+/// registry inode. Only genuine lock contention counts as the exclusion witness.\n+fn witness_exclusion(registry_path: &Path) -> io::Result<()> {\n+    let mut sentinel = registry_path.as_os_str().to_os_string();\n+    sentinel.push(\".lock\");\n+    let probe = OpenOptions::new().read(true).write(true).open(Path::new(&sentinel))?;\n+    match fs2::FileExt::try_lock_exclusive(&probe) {\n+        Ok(()) => Err(io::Error::other(\"another process reported HELD but its sentinel was unlocked\")),\n+        Err(error) if error.raw_os_error() == fs2::lock_contended_error().raw_os_error() => Ok(()),\n+        Err(error) => Err(error),\n+    }\n+}\n+\n+/// Keep the witness and pre-acquire barrier inside this call: moving a load\n+/// above this call deterministically captures the holder's unpublished state,\n+/// rather than depending on how quickly the parent schedules the next command.\n+fn acquire(registry_path: &Path, barriers: &Path, wait: bool) -> io::Result<File> {\n+    if wait {\n+        witness_exclusion(registry_path)?;\n+        announce(barriers, \"blocked\")?;\n+        // Re-probe after the holder has atomically replaced the registry, while\n+        // it still owns its guard. A lock on the replaced inode is insufficient.\n+        await_command(barriers, \"probe-published\")?;\n+        witness_exclusion(registry_path)?;\n+        announce(barriers, \"blocked-after-publish\")?;\n+        await_command(barriers, \"acquire\")?;\n+    }\n+    lock_registry_at(registry_path)\n+}\n+\n+fn run() -> io::Result<()> {\n+    let args: Vec<_> = std::env::args_os().collect();\n+    if args.len() != 6 {\n+        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected mode, role, registry, barriers, source\"));\n+    }\n+    let mode = args[1].to_str().ok_or_else(|| io::Error::other(\"invalid mode\"))?;\n+    if mode != \"reap\" && mode != \"register\" {\n+        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected reap or register\"));\n+    }\n+    let wait = match args[2].to_str() {\n+        Some(\"hold\") => false,\n+        Some(\"wait\") => true,\n+        _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected hold or wait\")),\n+    };\n+    let registry_path = Path::new(&args[3]);\n+    let barriers = Path::new(&args[4]);\n+    let source = Path::new(&args[5]);\n+\n+    let guard = acquire(registry_path, barriers, wait)?;\n+    let mut registry = ServingRegistry::load_at(registry_path)?;\n+    announce(barriers, \"loaded\")?;\n+    await_command(barriers, \"mutate\")?;\n+    if mode == \"reap\" {\n+        for entry in registry.reap_expired(1_000) {\n+            if entry.kind == ServedKind::Attachment {\n+                fs::remove_file(&entry.path)?;\n+            }\n+        }\n+    } else {\n+        registry.add_reference(source, Some(\"report.txt\"), None, 1_000)?;\n+    }\n+    registry.save_at(registry_path)?;\n+    announce(barriers, \"saved\")?;\n+    // The guard deliberately spans publication and the explicit release barrier.\n+    await_command(barriers, \"release\")?;\n+    drop(guard);\n+    Ok(())\n+}\n+\n+fn main() {\n+    if let Err(error) = run() {\n+        eprintln!(\"serving registry worker: {error}\");\n+        std::process::exit(1);\n+    }\n+}\ndiff --git a/crates/spt-store/tests/serving_registry_two_process_int.rs b/crates/spt-store/tests/serving_registry_two_process_int.rs\nnew file mode 100644\nindex 00000000..111a5e8d\n--- /dev/null\n+++ b/crates/spt-store/tests/serving_registry_two_process_int.rs\n@@ -0,0 +1,212 @@\n+//! Cross-process storage-contract proof for releases#308. These children use the\n+//! public registry API, not the broker/brain entry points; daemon callsite coverage\n+//! belongs to their own tests. Fixed logical time makes expiry deterministic.\n+\n+use std::fs;\n+use std::io;\n+use std::path::{Path, PathBuf};\n+use std::process::{Child, Command, ExitStatus, Stdio};\n+use std::time::{Duration, Instant};\n+\n+use spt_store::serving::{lock_registry_at, ServingRegistry};\n+\n+const CHILD_BOUND: Duration = Duration::from_secs(120);\n+const REAP_BOUND: Duration = Duration::from_secs(10);\n+\n+/// Own exactly the child we spawned, before any fallible readiness/assertion\n+/// work. No image-name sweeping, inherited pipes, or descendant processes.\n+struct RegistryWorker {\n+    child: Child,\n+    barriers: PathBuf,\n+}\n+\n+impl RegistryWorker {\n+    fn spawn(mode: &str, role: &str, registry: &Path, barriers: PathBuf, source: &Path) -> Self {\n+        fs::create_dir_all(&barriers).expect(\"worker barrier directory\");\n+        let child = Command::new(env!(\"CARGO_BIN_EXE_serving_registry_worker\"))\n+            .args([mode, role])\n+            .arg(registry)\n+            .arg(&barriers)\n+            .arg(source)\n+            .stdin(Stdio::null())\n+            .stdout(Stdio::null())\n+            .stderr(Stdio::inherit())\n+            .spawn()\n+            .expect(\"spawn serving registry worker\");\n+        Self { child, barriers }\n+    }\n+\n+    fn command(&self, command: &str) {\n+        fs::write(self.barriers.join(command), b\"\").expect(\"publish worker command\");\n+    }\n+\n+    fn await_event(&mut self, event: &str) {\n+        let deadline = Instant::now() + CHILD_BOUND;\n+        loop {\n+            if self.barriers.join(event).try_exists().expect(\"read worker event\") {\n+                return;\n+            }\n+            if let Some(status) = self.child.try_wait().expect(\"poll worker\") {\n+                panic!(\"worker exited {status} before {event}\");\n+            }\n+            assert!(Instant::now() < deadline, \"worker did not announce {event}\");\n+            // Polling only: the event, never elapsed time, establishes ordering.\n+            std::thread::sleep(Duration::from_millis(10));\n+        }\n+    }\n+\n+    fn wait_exit(&mut self, bound: Duration) -> io::Result<ExitStatus> {\n+        let deadline = Instant::now() + bound;\n+        loop {\n+            if let Some(status) = self.child.try_wait()? {\n+                return Ok(status);\n+            }\n+            if Instant::now() >= deadline {\n+                return Err(io::Error::new(io::ErrorKind::TimedOut, \"worker did not exit\"));\n+            }\n+            std::thread::sleep(Duration::from_millis(10));\n+        }\n+    }\n+\n+    fn finish(&mut self) {\n+        let status = self.wait_exit(CHILD_BOUND).expect(\"worker completion deadline\");\n+        assert!(status.success(), \"worker failed: {status}\");\n+    }\n+\n+    fn kill_and_reap(&mut self) -> io::Result<ExitStatus> {\n+        if let Some(status) = self.child.try_wait()? {\n+            return Ok(status);\n+        }\n+        if let Err(error) = self.child.kill() {\n+            // A natural exit may race the kill; do not report that as a leak.\n+            if let Some(status) = self.child.try_wait()? {\n+                return Ok(status);\n+            }\n+            return Err(error);\n+        }\n+        self.wait_exit(REAP_BOUND)\n+    }\n+}\n+\n+impl Drop for RegistryWorker {\n+    fn drop(&mut self) {\n+        if let Err(error) = self.kill_and_reap() {\n+            // Cleanup must not double-panic during assertion unwinding. A real\n+            // OS kill/reap failure is reported rather than silently swallowed.\n+            eprintln!(\"failed to reap registry worker {}: {error}\", self.child.id());\n+        }\n+    }\n+}\n+\n+fn overlap(holder_mode: &str, abrupt_exit: bool) {\n+    let temp = tempfile::tempdir().expect(\"isolated registry home\");\n+    let home = temp.path();\n+    // Cover the canonical name and an extension-bearing explicit path. The\n+    // sentinel must append .lock, not replace an existing extension.\n+    let registry_path = home.join(\"serve\").join(if holder_mode == \"reap\" { \"registry\" } else { \"registry.json\" });\n+    let expired_path = home.join(\"expired.snapshot\");\n+    let retired_path = home.join(\"retired.txt\");\n+    let registered_path = home.join(\"new.txt\");\n+    let third_path = home.join(\"third.txt\");\n+    for path in [&expired_path, &retired_path, &registered_path, &third_path] {\n+        fs::write(path, b\"fixture bytes\").expect(\"fixture source\");\n+    }\n+    let expired;\n+    let retired;\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"seed guard\");\n+        let mut seed = ServingRegistry::load_at(&registry_path).expect(\"empty registry\");\n+        expired = seed.add_attachment(&expired_path, \"report.txt\", None, 10, None, 100)\n+            .expect(\"expiring bare name\");\n+        retired = seed.add_reference(&retired_path, Some(\"report.txt\"), None, 100)\n+            .expect(\"historical suffixed name\");\n+        assert_eq!(expired.served_name, \"report.txt\");\n+        assert_eq!(retired.served_name, \"report~1.txt\");\n+        seed.remove(&retired.id).expect(\"retire suffix without forgetting owner\");\n+        seed.save_at(&registry_path).expect(\"seed publication\");\n+    }\n+\n+    let waiter_mode = if holder_mode == \"reap\" { \"register\" } else { \"reap\" };\n+    let mut holder = RegistryWorker::spawn(holder_mode, \"hold\", &registry_path, home.join(\"holder\"), &registered_path);\n+    holder.await_event(\"loaded\");\n+    let mut waiter = RegistryWorker::spawn(waiter_mode, \"wait\", &registry_path, home.join(\"waiter\"), &registered_path);\n+    // A real fs2 try-lock must fail with the platform's contention error while\n+    // the other PROCESS confirms its loaded transaction owns the guard.\n+    waiter.await_event(\"blocked\");\n+    holder.command(\"mutate\");\n+    holder.await_event(\"saved\");\n+    waiter.command(\"probe-published\");\n+    waiter.await_event(\"blocked-after-publish\");\n+    waiter.command(\"acquire\");\n+    if abrupt_exit {\n+        assert!(holder.child.try_wait().expect(\"holder still alive\").is_none(), \"holder exited before the abrupt-exit command\");\n+        holder.child.kill().expect(\"force-kill lock holder\");\n+        let status = holder.wait_exit(REAP_BOUND).expect(\"reap killed lock holder\");\n+        assert!(!status.success(), \"holder must exit abruptly without releasing its Rust guard\");\n+    } else {\n+        holder.command(\"release\");\n+        holder.finish();\n+    }\n+    waiter.await_event(\"loaded\");\n+    waiter.command(\"mutate\");\n+    waiter.await_event(\"saved\");\n+    waiter.command(\"release\");\n+    waiter.finish();\n+\n+    // Both orders matter: a stale reaper erases the acknowledged registration;\n+    // a stale registrar resurrects the expired entry after its snapshot is gone.\n+    let final_registry = ServingRegistry::load_at(&registry_path).expect(\"read durable final state\");\n+    assert!(final_registry.get(&expired.id).is_none(), \"expired entry was resurrected by a stale writer\");\n+    assert!(!expired_path.try_exists().expect(\"snapshot existence\"), \"reap must remove expired snapshot bytes\");\n+    let registration = final_registry.get(\"report~2.txt\").expect(\"acknowledged registration survives later publication\");\n+    assert_eq!(registration.path, registered_path);\n+    assert_eq!(final_registry.entries().count(), 1, \"only the new registration remains exposed\");\n+    let registration_id = registration.id.clone();\n+\n+    // Observe history through future assignments, not private JSON fields. A\n+    // save/reload after retirement must retain BOTH ownership and next suffix.\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"retirement guard\");\n+        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"fresh retirement load\");\n+        registry.remove(&registration_id).expect(\"retire acknowledged registration\");\n+        registry.save_at(&registry_path).expect(\"persist retirement\");\n+    }\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"history guard\");\n+        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"reload allocation history\");\n+        let reclaimed = registry.add_reference(&registered_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"reclaim recent owner\");\n+        assert_eq!(reclaimed.served_name, \"report~2.txt\", \"retiring must preserve the new owner's name\");\n+        let old_owner = registry.add_reference(&retired_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"reclaim historical owner\");\n+        assert_eq!(old_owner.served_name, \"report~1.txt\", \"reap must preserve previously retired ownership\");\n+        let next = registry.add_reference(&third_path, Some(\"report.txt\"), None, 2_000)","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300]},"truncation":{"content":"diff --git a/crates/spt-store/tests/fixtures/serving_registry_worker.rs b/crates/spt-store/tests/fixtures/serving_registry_worker.rs\nnew file mode 100644\nindex 00000000..01c3b94b\n--- /dev/null\n+++ b/crates/spt-store/tests/fixtures/serving_registry_worker.rs\n@@ -0,0 +1,105 @@\n+//! Test-only serving-registry writer. All IPC is explicit, per-child file\n+//! barriers under a temporary directory; polling delays never order transactions.\n+//! Usage: serving_registry_worker <reap|register> <hold|wait> <registry> <barriers> <source>\n+\n+use std::fs::{self, File, OpenOptions};\n+use std::io;\n+use std::path::Path;\n+use std::time::{Duration, Instant};\n+\n+use spt_store::serving::{lock_registry_at, ServedKind, ServingRegistry};\n+\n+const COMMAND_BOUND: Duration = Duration::from_secs(120);\n+\n+fn announce(barriers: &Path, event: &str) -> io::Result<()> {\n+    fs::write(barriers.join(event), b\"\")\n+}\n+\n+fn await_command(barriers: &Path, command: &str) -> io::Result<()> {\n+    let deadline = Instant::now() + COMMAND_BOUND;\n+    loop {\n+        if barriers.join(command).try_exists()? {\n+            return Ok(());\n+        }\n+        if Instant::now() >= deadline {\n+            return Err(io::Error::new(io::ErrorKind::TimedOut, format!(\"waiting for {command}\")));\n+        }\n+        std::thread::sleep(Duration::from_millis(10));\n+    }\n+}\n+\n+/// Probe a fresh handle to the documented sentinel, not the atomically replaced\n+/// registry inode. Only genuine lock contention counts as the exclusion witness.\n+fn witness_exclusion(registry_path: &Path) -> io::Result<()> {\n+    let mut sentinel = registry_path.as_os_str().to_os_string();\n+    sentinel.push(\".lock\");\n+    let probe = OpenOptions::new().read(true).write(true).open(Path::new(&sentinel))?;\n+    match fs2::FileExt::try_lock_exclusive(&probe) {\n+        Ok(()) => Err(io::Error::other(\"another process reported HELD but its sentinel was unlocked\")),\n+        Err(error) if error.raw_os_error() == fs2::lock_contended_error().raw_os_error() => Ok(()),\n+        Err(error) => Err(error),\n+    }\n+}\n+\n+/// Keep the witness and pre-acquire barrier inside this call: moving a load\n+/// above this call deterministically captures the holder's unpublished state,\n+/// rather than depending on how quickly the parent schedules the next command.\n+fn acquire(registry_path: &Path, barriers: &Path, wait: bool) -> io::Result<File> {\n+    if wait {\n+        witness_exclusion(registry_path)?;\n+        announce(barriers, \"blocked\")?;\n+        // Re-probe after the holder has atomically replaced the registry, while\n+        // it still owns its guard. A lock on the replaced inode is insufficient.\n+        await_command(barriers, \"probe-published\")?;\n+        witness_exclusion(registry_path)?;\n+        announce(barriers, \"blocked-after-publish\")?;\n+        await_command(barriers, \"acquire\")?;\n+    }\n+    lock_registry_at(registry_path)\n+}\n+\n+fn run() -> io::Result<()> {\n+    let args: Vec<_> = std::env::args_os().collect();\n+    if args.len() != 6 {\n+        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected mode, role, registry, barriers, source\"));\n+    }\n+    let mode = args[1].to_str().ok_or_else(|| io::Error::other(\"invalid mode\"))?;\n+    if mode != \"reap\" && mode != \"register\" {\n+        return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected reap or register\"));\n+    }\n+    let wait = match args[2].to_str() {\n+        Some(\"hold\") => false,\n+        Some(\"wait\") => true,\n+        _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, \"expected hold or wait\")),\n+    };\n+    let registry_path = Path::new(&args[3]);\n+    let barriers = Path::new(&args[4]);\n+    let source = Path::new(&args[5]);\n+\n+    let guard = acquire(registry_path, barriers, wait)?;\n+    let mut registry = ServingRegistry::load_at(registry_path)?;\n+    announce(barriers, \"loaded\")?;\n+    await_command(barriers, \"mutate\")?;\n+    if mode == \"reap\" {\n+        for entry in registry.reap_expired(1_000) {\n+            if entry.kind == ServedKind::Attachment {\n+                fs::remove_file(&entry.path)?;\n+            }\n+        }\n+    } else {\n+        registry.add_reference(source, Some(\"report.txt\"), None, 1_000)?;\n+    }\n+    registry.save_at(registry_path)?;\n+    announce(barriers, \"saved\")?;\n+    // The guard deliberately spans publication and the explicit release barrier.\n+    await_command(barriers, \"release\")?;\n+    drop(guard);\n+    Ok(())\n+}\n+\n+fn main() {\n+    if let Err(error) = run() {\n+        eprintln!(\"serving registry worker: {error}\");\n+        std::process::exit(1);\n+    }\n+}\ndiff --git a/crates/spt-store/tests/serving_registry_two_process_int.rs b/crates/spt-store/tests/serving_registry_two_process_int.rs\nnew file mode 100644\nindex 00000000..111a5e8d\n--- /dev/null\n+++ b/crates/spt-store/tests/serving_registry_two_process_int.rs\n@@ -0,0 +1,212 @@\n+//! Cross-process storage-contract proof for releases#308. These children use the\n+//! public registry API, not the broker/brain entry points; daemon callsite coverage\n+//! belongs to their own tests. Fixed logical time makes expiry deterministic.\n+\n+use std::fs;\n+use std::io;\n+use std::path::{Path, PathBuf};\n+use std::process::{Child, Command, ExitStatus, Stdio};\n+use std::time::{Duration, Instant};\n+\n+use spt_store::serving::{lock_registry_at, ServingRegistry};\n+\n+const CHILD_BOUND: Duration = Duration::from_secs(120);\n+const REAP_BOUND: Duration = Duration::from_secs(10);\n+\n+/// Own exactly the child we spawned, before any fallible readiness/assertion\n+/// work. No image-name sweeping, inherited pipes, or descendant processes.\n+struct RegistryWorker {\n+    child: Child,\n+    barriers: PathBuf,\n+}\n+\n+impl RegistryWorker {\n+    fn spawn(mode: &str, role: &str, registry: &Path, barriers: PathBuf, source: &Path) -> Self {\n+        fs::create_dir_all(&barriers).expect(\"worker barrier directory\");\n+        let child = Command::new(env!(\"CARGO_BIN_EXE_serving_registry_worker\"))\n+            .args([mode, role])\n+            .arg(registry)\n+            .arg(&barriers)\n+            .arg(source)\n+            .stdin(Stdio::null())\n+            .stdout(Stdio::null())\n+            .stderr(Stdio::inherit())\n+            .spawn()\n+            .expect(\"spawn serving registry worker\");\n+        Self { child, barriers }\n+    }\n+\n+    fn command(&self, command: &str) {\n+        fs::write(self.barriers.join(command), b\"\").expect(\"publish worker command\");\n+    }\n+\n+    fn await_event(&mut self, event: &str) {\n+        let deadline = Instant::now() + CHILD_BOUND;\n+        loop {\n+            if self.barriers.join(event).try_exists().expect(\"read worker event\") {\n+                return;\n+            }\n+            if let Some(status) = self.child.try_wait().expect(\"poll worker\") {\n+                panic!(\"worker exited {status} before {event}\");\n+            }\n+            assert!(Instant::now() < deadline, \"worker did not announce {event}\");\n+            // Polling only: the event, never elapsed time, establishes ordering.\n+            std::thread::sleep(Duration::from_millis(10));\n+        }\n+    }\n+\n+    fn wait_exit(&mut self, bound: Duration) -> io::Result<ExitStatus> {\n+        let deadline = Instant::now() + bound;\n+        loop {\n+            if let Some(status) = self.child.try_wait()? {\n+                return Ok(status);\n+            }\n+            if Instant::now() >= deadline {\n+                return Err(io::Error::new(io::ErrorKind::TimedOut, \"worker did not exit\"));\n+            }\n+            std::thread::sleep(Duration::from_millis(10));\n+        }\n+    }\n+\n+    fn finish(&mut self) {\n+        let status = self.wait_exit(CHILD_BOUND).expect(\"worker completion deadline\");\n+        assert!(status.success(), \"worker failed: {status}\");\n+    }\n+\n+    fn kill_and_reap(&mut self) -> io::Result<ExitStatus> {\n+        if let Some(status) = self.child.try_wait()? {\n+            return Ok(status);\n+        }\n+        if let Err(error) = self.child.kill() {\n+            // A natural exit may race the kill; do not report that as a leak.\n+            if let Some(status) = self.child.try_wait()? {\n+                return Ok(status);\n+            }\n+            return Err(error);\n+        }\n+        self.wait_exit(REAP_BOUND)\n+    }\n+}\n+\n+impl Drop for RegistryWorker {\n+    fn drop(&mut self) {\n+        if let Err(error) = self.kill_and_reap() {\n+            // Cleanup must not double-panic during assertion unwinding. A real\n+            // OS kill/reap failure is reported rather than silently swallowed.\n+            eprintln!(\"failed to reap registry worker {}: {error}\", self.child.id());\n+        }\n+    }\n+}\n+\n+fn overlap(holder_mode: &str, abrupt_exit: bool) {\n+    let temp = tempfile::tempdir().expect(\"isolated registry home\");\n+    let home = temp.path();\n+    // Cover the canonical name and an extension-bearing explicit path. The\n+    // sentinel must append .lock, not replace an existing extension.\n+    let registry_path = home.join(\"serve\").join(if holder_mode == \"reap\" { \"registry\" } else { \"registry.json\" });\n+    let expired_path = home.join(\"expired.snapshot\");\n+    let retired_path = home.join(\"retired.txt\");\n+    let registered_path = home.join(\"new.txt\");\n+    let third_path = home.join(\"third.txt\");\n+    for path in [&expired_path, &retired_path, &registered_path, &third_path] {\n+        fs::write(path, b\"fixture bytes\").expect(\"fixture source\");\n+    }\n+    let expired;\n+    let retired;\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"seed guard\");\n+        let mut seed = ServingRegistry::load_at(&registry_path).expect(\"empty registry\");\n+        expired = seed.add_attachment(&expired_path, \"report.txt\", None, 10, None, 100)\n+            .expect(\"expiring bare name\");\n+        retired = seed.add_reference(&retired_path, Some(\"report.txt\"), None, 100)\n+            .expect(\"historical suffixed name\");\n+        assert_eq!(expired.served_name, \"report.txt\");\n+        assert_eq!(retired.served_name, \"report~1.txt\");\n+        seed.remove(&retired.id).expect(\"retire suffix without forgetting owner\");\n+        seed.save_at(&registry_path).expect(\"seed publication\");\n+    }\n+\n+    let waiter_mode = if holder_mode == \"reap\" { \"register\" } else { \"reap\" };\n+    let mut holder = RegistryWorker::spawn(holder_mode, \"hold\", &registry_path, home.join(\"holder\"), &registered_path);\n+    holder.await_event(\"loaded\");\n+    let mut waiter = RegistryWorker::spawn(waiter_mode, \"wait\", &registry_path, home.join(\"waiter\"), &registered_path);\n+    // A real fs2 try-lock must fail with the platform's contention error while\n+    // the other PROCESS confirms its loaded transaction owns the guard.\n+    waiter.await_event(\"blocked\");\n+    holder.command(\"mutate\");\n+    holder.await_event(\"saved\");\n+    waiter.command(\"probe-published\");\n+    waiter.await_event(\"blocked-after-publish\");\n+    waiter.command(\"acquire\");\n+    if abrupt_exit {\n+        assert!(holder.child.try_wait().expect(\"holder still alive\").is_none(), \"holder exited before the abrupt-exit command\");\n+        holder.child.kill().expect(\"force-kill lock holder\");\n+        let status = holder.wait_exit(REAP_BOUND).expect(\"reap killed lock holder\");\n+        assert!(!status.success(), \"holder must exit abruptly without releasing its Rust guard\");\n+    } else {\n+        holder.command(\"release\");\n+        holder.finish();\n+    }\n+    waiter.await_event(\"loaded\");\n+    waiter.command(\"mutate\");\n+    waiter.await_event(\"saved\");\n+    waiter.command(\"release\");\n+    waiter.finish();\n+\n+    // Both orders matter: a stale reaper erases the acknowledged registration;\n+    // a stale registrar resurrects the expired entry after its snapshot is gone.\n+    let final_registry = ServingRegistry::load_at(&registry_path).expect(\"read durable final state\");\n+    assert!(final_registry.get(&expired.id).is_none(), \"expired entry was resurrected by a stale writer\");\n+    assert!(!expired_path.try_exists().expect(\"snapshot existence\"), \"reap must remove expired snapshot bytes\");\n+    let registration = final_registry.get(\"report~2.txt\").expect(\"acknowledged registration survives later publication\");\n+    assert_eq!(registration.path, registered_path);\n+    assert_eq!(final_registry.entries().count(), 1, \"only the new registration remains exposed\");\n+    let registration_id = registration.id.clone();\n+\n+    // Observe history through future assignments, not private JSON fields. A\n+    // save/reload after retirement must retain BOTH ownership and next suffix.\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"retirement guard\");\n+        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"fresh retirement load\");\n+        registry.remove(&registration_id).expect(\"retire acknowledged registration\");\n+        registry.save_at(&registry_path).expect(\"persist retirement\");\n+    }\n+    {\n+        let _guard = lock_registry_at(&registry_path).expect(\"history guard\");\n+        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"reload allocation history\");\n+        let reclaimed = registry.add_reference(&registered_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"reclaim recent owner\");\n+        assert_eq!(reclaimed.served_name, \"report~2.txt\", \"retiring must preserve the new owner's name\");\n+        let old_owner = registry.add_reference(&retired_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"reclaim historical owner\");\n+        assert_eq!(old_owner.served_name, \"report~1.txt\", \"reap must preserve previously retired ownership\");\n+        let next = registry.add_reference(&third_path, Some(\"report.txt\"), None, 2_000)","truncated":true,"truncatedBy":"lines","totalLines":460,"totalBytes":13647,"outputLines":300,"outputBytes":13647,"lastLinePartial":false,"firstLineExceedsLimit":false},"meta":{"source":{"type":"internal","value":"artifact://495"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":460,"totalBytes":13647,"outputLines":300,"outputBytes":13647,"maxBytes":153600,"shownRange":{"start":1,"end":300},"nextOffset":301}}},"isError":false,"timestamp":1789522895867}}
{"type":"message","id":"de03369a","parentId":"d0cf7de0","timestamp":"2026-09-16T01:41:35.872Z","message":{"role":"toolResult","toolCallId":"call_5KRHzfuipBk2u6xrTV5kVJuc|fc_0cdf94810c888815016aa9f3cfc6a887d1aff924f6a9ae4b25","toolName":"glob","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/\nAGENTS.md"}],"details":{"scopePath":".worktrees/308-registry-process-lock/**/AGENTS.md, .worktrees/308-registry-process-lock/**/CONTRIBUTING.md, .worktrees/308-registry-process-lock/**/CODING_STANDARDS.md","fileCount":1,"files":[".worktrees/308-registry-process-lock/AGENTS.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789522895871}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Kth1ScXOJJCMslNe0uiXsbIc|fc_0cdf94810c888815016aa9f3d6a92087d18e489e4bed00b22e","toolName":"read","startedAt":"2026-09-16T01:41:42.732Z","args":{"path":"artifact://494:301-360"},"intent":"Reading remaining implementation patch"},"id":"02c849ad","parentId":"de03369a","timestamp":"2026-09-16T01:41:42.732Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_woxh4DstF0F2A7K21HPrCq9H|fc_0cdf94810c888815016aa9f3d6a93087d1a386fd2fa1cd57cc","toolName":"read","startedAt":"2026-09-16T01:41:42.733Z","args":{"path":"artifact://495:301-460"},"intent":"Reading remaining regression patch"},"id":"614783b8","parentId":"02c849ad","timestamp":"2026-09-16T01:41:42.733Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_d0jkfv1aavQfBPNYdUyRSL7p|fc_0cdf94810c888815016aa9f3d6a93887d18686ce5412ede1f6","toolName":"read","startedAt":"2026-09-16T01:41:42.733Z","args":{"path":".worktrees/308-registry-process-lock/AGENTS.md"},"intent":"Reading worktree review guidance"},"id":"482c0369","parentId":"614783b8","timestamp":"2026-09-16T01:41:42.733Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Y3dqSrXHSIZ2ROd1GrbxODvi|fc_0cdf94810c888815016aa9f3d6a94087d1b0a654c432953b6a","toolName":"read","startedAt":"2026-09-16T01:41:42.733Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs:1-320"},"intent":"Reading registry transaction context"},"id":"76583354","parentId":"482c0369","timestamp":"2026-09-16T01:41:42.733Z"}
{"type":"message","id":"e83b8905","parentId":"76583354","timestamp":"2026-09-16T01:41:42.733Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_Kth1ScXOJJCMslNe0uiXsbIc|fc_0cdf94810c888815016aa9f3d6a92087d18e489e4bed00b22e","name":"read","arguments":{"i":"Reading remaining implementation patch","path":"artifact://494:301-360"},"intent":"Reading remaining implementation patch"},{"type":"toolCall","id":"call_woxh4DstF0F2A7K21HPrCq9H|fc_0cdf94810c888815016aa9f3d6a93087d1a386fd2fa1cd57cc","name":"read","arguments":{"i":"Reading remaining regression patch","path":"artifact://495:301-460"},"intent":"Reading remaining regression patch"},{"type":"toolCall","id":"call_d0jkfv1aavQfBPNYdUyRSL7p|fc_0cdf94810c888815016aa9f3d6a93887d18686ce5412ede1f6","name":"read","arguments":{"i":"Reading worktree review guidance","path":".worktrees/308-registry-process-lock/AGENTS.md"},"intent":"Reading worktree review guidance"},{"type":"toolCall","id":"call_Y3dqSrXHSIZ2ROd1GrbxODvi|fc_0cdf94810c888815016aa9f3d6a94087d1b0a654c432953b6a","name":"read","arguments":{"i":"Reading registry transaction context","path":".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs:1-320"},"intent":"Reading registry transaction context"},{"type":"toolCall","id":"call_a70qvKerXAXNFCvZ2oitN9kT|fc_0cdf94810c888815016aa9f3d6a94c87d1bef984dc3d127e29","name":"read","arguments":{"i":"Reading broker mutation context","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs:1-390"},"intent":"Reading broker mutation context"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":10825,"output":190,"cacheRead":18688,"cacheWrite":0,"totalTokens":29703,"cost":{"input":0.10825000000000001,"output":0.0095,"cacheRead":0.018688,"cacheWrite":0,"total":0.136438}},"stopReason":"toolUse","timestamp":1789522895878,"responseId":"resp_0cdf94810c888815016aa9f3d01bd487d19c6c9f7352239c59","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_0cdf94810c888815016aa9f3d6a92087d18e489e4bed00b22e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading remaining implementation patch\",\"path\":\"artifact://494:301-360\"}","call_id":"call_Kth1ScXOJJCMslNe0uiXsbIc","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3d6a93087d1a386fd2fa1cd57cc","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading remaining regression patch\",\"path\":\"artifact://495:301-460\"}","call_id":"call_woxh4DstF0F2A7K21HPrCq9H","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3d6a93887d18686ce5412ede1f6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading worktree review guidance\",\"path\":\".worktrees/308-registry-process-lock/AGENTS.md\"}","call_id":"call_d0jkfv1aavQfBPNYdUyRSL7p","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3d6a94087d1b0a654c432953b6a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry transaction context\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs:1-320\"}","call_id":"call_Y3dqSrXHSIZ2ROd1GrbxODvi","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3d6a94c87d1bef984dc3d127e29","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker mutation context\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs:1-390\"}","call_id":"call_a70qvKerXAXNFCvZ2oitN9kT","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":6853.048699999985,"ttft":6815.094400000002,"completedAt":1789522902731,"contextSnapshot":{"promptTokens":29513,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_a70qvKerXAXNFCvZ2oitN9kT|fc_0cdf94810c888815016aa9f3d6a94c87d1bef984dc3d127e29","toolName":"read","startedAt":"2026-09-16T01:41:42.733Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs:1-390"},"intent":"Reading broker mutation context"},"id":"1571f013","parentId":"e83b8905","timestamp":"2026-09-16T01:41:42.733Z"}
{"type":"message","id":"1def06bc","parentId":"1571f013","timestamp":"2026-09-16T01:41:42.757Z","message":{"role":"toolResult","toolCallId":"call_Y3dqSrXHSIZ2ROd1GrbxODvi|fc_0cdf94810c888815016aa9f3d6a94087d1b0a654c432953b6a","toolName":"read","content":[{"type":"text","text":"//! Node-local, authoritative serving assignments (ADR-0057).\n//!\n//! Schema 1 stores live entries, per-family suffix counters, and the absolute\n//! path and kind each name last identified. Removal changes only live entries;\n//! the same path and kind may reclaim their names (ADR-0057 Amendment 2).\n//! Broker mutations and brain reaping share a cross-process writer guard;\n//! corruption is never an empty store.\n// [impl->REQ-WEB-SERVING-REGISTRY]\n\nuse std::collections::{BTreeMap, BTreeSet};\nuse std::fs;\nuse std::io;\nuse std::path::{Component, Path, PathBuf};\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::atomic::atomic_write_bytes_durable;\n\nconst SCHEMA_VERSION: u32 = 1;\n\n/// An attachment's lifetime when the sender names none: 30 days (ADR-0058,\n/// operator-set). It is a CONSTANT rather than a config knob on purpose — the\n/// figure is a published contract in the attachments page, and a per-send\n/// `--ttl` already covers the case where a sender wants something else.\n// [impl->REQ-WEB-ATTACHMENT-PULL]\npub const DEFAULT_ATTACHMENT_TTL_MS: u64 = 30 * 24 * 60 * 60 * 1_000;\n\n/// The lifetime a FILE_ACCESS_HELPER entry carries: 24 hours (ADR-0058\n/// Amendment 1, operator-directed). Shorter than an attachment's because the\n/// user quoted a path in passing rather than sending a copy.\n// [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\npub const HELPER_ENTRY_TTL_MS: u64 = 24 * 60 * 60 * 1_000;\n\n/// One attachment as the message envelope carries it: a name, the URL to pull\n/// it from, and the size in bytes.\n///\n/// The SIZE rides with the link because attachments are pull-model — a receiver\n/// deciding whether to fetch has to be able to decide BEFORE it fetches, and a\n/// link alone makes that impossible without a request.\n// [impl->REQ-WEB-ATTACHMENT-PULL]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct AttachmentRef {\n    /// The SERVED name, which is what the URL ends in — never the source\n    /// basename, because the registry disambiguates a repeated name and the\n    /// receiver must be told the name it can actually address.\n    pub name: String,\n    pub url: String,\n    pub bytes: u64,\n}\n\n/// Parse a `--ttl` specification into milliseconds.\n///\n/// A UNIT IS REQUIRED. A bare number would have to mean seconds, or days, or\n/// milliseconds, and every reader would pick a different one — a `--ttl 30`\n/// that silently meant 30 seconds when the sender meant 30 days is a deleted\n/// attachment, so the refusal is deliberate and names what it wanted.\n// [impl->REQ-WEB-ATTACHMENT-PULL]\npub fn parse_ttl_ms(spec: &str) -> Result<u64, String> {\n    let trimmed = spec.trim();\n    let (digits, unit) = trimmed.split_at(\n        trimmed\n            .find(|c: char| !c.is_ascii_digit())\n            .ok_or_else(|| format!(\"ttl needs a unit (s, m, h, d): {trimmed}\"))?,\n    );\n    let count: u64 = digits\n        .parse()\n        .map_err(|_| format!(\"ttl must start with a count: {trimmed}\"))?;\n    let unit_ms: u64 = match unit {\n        \"s\" => 1_000,\n        \"m\" => 60 * 1_000,\n        \"h\" => 60 * 60 * 1_000,\n        \"d\" => 24 * 60 * 60 * 1_000,\n        other => return Err(format!(\"unknown ttl unit {other:?} (want s, m, h, d)\")),\n    };\n    // A ttl that overflows is refused rather than saturated: saturating would\n    // hand back a lifetime the sender did not ask for and cannot see.\n    count\n        .checked_mul(unit_ms)\n        .filter(|ms| *ms > 0)\n        .ok_or_else(|| format!(\"ttl out of range: {trimmed}\"))\n}\n\n/// How an entry's path is interpreted. W0 creates references only.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum ServedKind {\n    File,\n    Dir,\n    Attachment,\n}\n\n/// A stable assignment; reference paths are resolved again when requested.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct ServedEntry {\n    pub id: String,\n    pub kind: ServedKind,\n    pub path: PathBuf,\n    pub served_name: String,\n    pub registered_at_ms: u64,\n    /// Optional lifetime in milliseconds from registration, independent of kind.\n    // [impl->REQ-WEB-SERVING-REGISTRY]\n    pub ttl_ms: Option<u64>,\n    /// Optional receiving endpoint, independent of kind. Fetch enforcement is W2.\n    #[serde(default)]\n    pub audience: Option<String>,\n    /// Registering endpoint reported by the local caller; not a WEB access credential.\n    pub origin: Option<String>,\n    /// Adapter owning a manifest-created entry, absent for explicit registrations.\n    pub adapter: Option<String>,\n    /// Expose the same served name directly below the node, as well as `/f/`.\n    #[serde(default)]\n    pub short_alias: bool,\n}\n\n/// Retired names remain reserved for exactly this source identity.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(deny_unknown_fields)]\nstruct NameOwner {\n    path: PathBuf,\n    kind: ServedKind,\n}\n\n/// Persistent allocation history and the currently exposed entries.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct ServingRegistry {\n    schema_version: u32,\n    entries: Vec<ServedEntry>,\n    // The key is the requested name, not an assigned suffix. Splitting at the\n    // final non-leading dot is one-to-one, including dotfiles and trailing dots.\n    next_suffix: BTreeMap<String, u64>,\n    name_owners: BTreeMap<String, NameOwner>,\n}\n\n/// Serialize a registry transaction across threads and processes.\n///\n/// Acquire BEFORE loading and keep the returned file alive through mutation,\n/// snapshot cleanup/rollback, and explicit publication. This does not load or\n/// save anything itself, so no-op transactions need not publish a registry.\n///\n/// The stable sibling `registry.lock` is never renamed or removed: locking the\n/// registry itself would stop coordinating writers after atomic replacement.\n/// Closing the returned file (including process exit) releases the lock. Do not\n/// acquire it again while holding it; independently opened handles contend.\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n// [impl->REQ-WEB-SERVING-REGISTRY]\n// [impl->REQ-WEB-ATTACHMENT-PULL]\npub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n    use fs2::FileExt;\n\n    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\n        fs::create_dir_all(parent)?;\n    }\n    let mut lock_path = registry_path.as_os_str().to_os_string();\n    lock_path.push(\".lock\");\n    let lock = fs::OpenOptions::new()\n        .create(true)\n        .write(true)\n        .truncate(false)\n        .open(PathBuf::from(lock_path))?;\n    lock.lock_exclusive()?;\n    Ok(lock)\n}\n\nimpl ServingRegistry {\n    /// Missing is empty. Unreadable, malformed, or inconsistent history is an\n    /// error: never recover by resetting the allocator and reusing stale URLs.\n    pub fn load_at(registry_path: &Path) -> io::Result<Self> {\n        let bytes = match fs::read(registry_path) {\n            Ok(bytes) => bytes,\n            Err(error) if error.kind() == io::ErrorKind::NotFound => {\n                // A dangling symlink is an existing, broken store, not absence.\n                match fs::symlink_metadata(registry_path) {\n                    Err(missing) if missing.kind() == io::ErrorKind::NotFound => {\n                        return Ok(Self {\n                            schema_version: SCHEMA_VERSION,\n                            entries: Vec::new(),\n                            next_suffix: BTreeMap::new(),\n                            name_owners: BTreeMap::new(),\n                        });\n                    }\n                    Err(error) => return Err(error),\n                    Ok(_) => return Err(error),\n                }\n            }\n            Err(error) => return Err(error),\n        };\n        let registry: Self = serde_json::from_slice(&bytes).map_err(invalid_data)?;\n        registry.validate()?;\n        Ok(registry)\n    }\n\n    /// Publish a durable old-or-new snapshot, creating its parent directory.\n    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\n    /// through this publication; locking only this save cannot repair stale state.\n    pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\n        self.validate()?;\n        let bytes = serde_json::to_vec_pretty(self).map_err(invalid_data)?;\n        if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\n            fs::create_dir_all(parent)?;\n        }\n        atomic_write_bytes_durable(registry_path, &bytes)\n    }\n\n    pub fn entries(&self) -> impl Iterator<Item = &ServedEntry> {\n        self.entries.iter()\n    }\n\n    /// Served names take precedence over IDs if a caller explicitly chose an ID\n    /// as a name. Both are exact, case-sensitive registry identifiers.\n    pub fn get(&self, name_or_id: &str) -> Option<&ServedEntry> {\n        self.entries\n            .iter()\n            .find(|entry| entry.served_name == name_or_id)\n            .or_else(|| self.entries.iter().find(|entry| entry.id == name_or_id))\n    }\n\n    /// Register an absolute, lexically normalized file/directory reference.\n    /// No canonicalization: symlink targets and bytes may change after registration.\n    /// An existing path returns its original assignment even after deletion or\n    /// when a different explicit name, origin, or timestamp is supplied.\n    // [impl->REQ-WEB-SERVING-REGISTRY]\n    pub fn add_reference(\n        &mut self,\n        absolute_path: &Path,\n        requested_name: Option<&str>,\n        origin: Option<&str>,\n        now_ms: u64,\n    ) -> io::Result<ServedEntry> {\n        if !valid_reference_path(absolute_path) {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidInput,\n                \"served source must be an absolute, lexically normalized path\",\n            ));\n        }\n        if let Some(entry) = self.entries.iter().find(|entry| entry.path == absolute_path) {\n            return Ok(entry.clone());\n        }\n        let name = requested_name\n            .or_else(|| absolute_path.file_name().and_then(|name| name.to_str()))\n            .filter(|name| valid_name(name))\n            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, \"invalid served name\"))?;\n        let metadata = fs::metadata(absolute_path)?;\n        let kind = if metadata.is_file() {\n            ServedKind::File\n        } else if metadata.is_dir() {\n            ServedKind::Dir\n        } else {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidInput,\n                \"served source must be a regular file or directory\",\n            ));\n        };\n        let (served_name, next) = self.allocate_name(name, absolute_path, kind)?;\n        let entry = ServedEntry {\n            id: uuid::Uuid::new_v4().to_string(),\n            kind,\n            path: absolute_path.to_path_buf(),\n            served_name,\n            registered_at_ms: now_ms,\n            ttl_ms: None,\n            audience: None,\n            origin: origin.map(str::to_owned),\n            adapter: None,\n            short_alias: false,\n        };\n        self.next_suffix.insert(name.to_owned(), next);\n        self.name_owners.insert(entry.served_name.clone(), NameOwner {\n            path: entry.path.clone(),\n            kind,\n        });\n        self.entries.push(entry.clone());\n        Ok(entry)\n    }\n\n    /// Receipt-scoped live reference. A duplicate never changes exposure or its\n    /// deadline; an unrelated registration is not authority to rescope it.\n    /// `None` means the source is absent, not a diagnostic.\n    // [impl->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\n    pub fn add_input_reference(\n        &mut self,\n        source: &Path,\n        receipt_id: &str,\n        audience: &str,\n        received_at_ms: u64,\n        now_ms: u64,\n    ) -> io::Result<Option<ServedEntry>> {\n        if !valid_reference_path(source) || receipt_id.is_empty() || audience.trim().is_empty() {\n            return Err(io::Error::new(io::ErrorKind::InvalidInput, \"INPUT_PATH_INVALID\"));\n        }\n        match fs::metadata(source) {\n            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),\n            Err(error) => return Err(io::Error::new(error.kind(), format!(\"INPUT_PATH_METADATA: {error}\"))),\n            Ok(metadata) if !metadata.is_file() && !metadata.is_dir() => {\n                return Err(io::Error::new(io::ErrorKind::InvalidInput, \"INPUT_PATH_INVALID\"));\n            }\n            Ok(_) => {}\n        }\n        let origin = format!(\"user-input:{receipt_id}\");\n        if let Some(entry) = self.entries.iter().find(|entry| entry.path == source) {\n            if !entry.origin.as_deref().is_some_and(|value| value.starts_with(\"user-input:\"))\n                || entry.audience.as_deref() != Some(audience)\n                || entry.ttl_ms != Some(HELPER_ENTRY_TTL_MS)\n                || entry.adapter.is_some()\n                || entry.kind == ServedKind::Attachment\n            {\n                return Err(io::Error::new(io::ErrorKind::PermissionDenied, \"INPUT_PATH_SCOPE_CONFLICT\"));\n            }\n            if entry.registered_at_ms.saturating_add(HELPER_ENTRY_TTL_MS) > now_ms {\n                return Ok(Some(entry.clone()));\n            }\n            // A later submission may replace its same-audience expired input\n            // reference. Replaying the original receipt cannot renew it.\n            if entry.origin.as_deref() == Some(origin.as_str()) {\n                return Err(io::Error::new(io::ErrorKind::PermissionDenied, \"INPUT_PATH_EXPIRED\"));\n            }\n            let id = entry.id.clone();\n            self.remove(&id);\n        }\n        let registered_at_ms = received_at_ms.min(now_ms);\n        if registered_at_ms.saturating_add(HELPER_ENTRY_TTL_MS) <= now_ms\n        {\n…\n        }\n…\n    }\n…\n}\n\n[Showing lines 1-323 of 1414. Use :324 to continue]"}],"details":{"truncation":{"content":"//! Node-local, authoritative serving assignments (ADR-0057).\r\n//!\r\n//! Schema 1 stores live entries, per-family suffix counters, and the absolute\r\n//! path and kind each name last identified. Removal changes only live entries;\r\n//! the same path and kind may reclaim their names (ADR-0057 Amendment 2).\r\n//! Broker mutations and brain reaping share a cross-process writer guard;\r\n//! corruption is never an empty store.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n\r\nuse std::collections::{BTreeMap, BTreeSet};\r\nuse std::fs;\r\nuse std::io;\r\nuse std::path::{Component, Path, PathBuf};\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::atomic::atomic_write_bytes_durable;\r\n\r\nconst SCHEMA_VERSION: u32 = 1;\r\n\r\n/// An attachment's lifetime when the sender names none: 30 days (ADR-0058,\r\n/// operator-set). It is a CONSTANT rather than a config knob on purpose — the\r\n/// figure is a published contract in the attachments page, and a per-send\r\n/// `--ttl` already covers the case where a sender wants something else.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\npub const DEFAULT_ATTACHMENT_TTL_MS: u64 = 30 * 24 * 60 * 60 * 1_000;\r\n\r\n/// The lifetime a FILE_ACCESS_HELPER entry carries: 24 hours (ADR-0058\r\n/// Amendment 1, operator-directed). Shorter than an attachment's because the\r\n/// user quoted a path in passing rather than sending a copy.\r\n// [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\npub const HELPER_ENTRY_TTL_MS: u64 = 24 * 60 * 60 * 1_000;\r\n\r\n/// One attachment as the message envelope carries it: a name, the URL to pull\r\n/// it from, and the size in bytes.\r\n///\r\n/// The SIZE rides with the link because attachments are pull-model — a receiver\r\n/// deciding whether to fetch has to be able to decide BEFORE it fetches, and a\r\n/// link alone makes that impossible without a request.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct AttachmentRef {\r\n    /// The SERVED name, which is what the URL ends in — never the source\r\n    /// basename, because the registry disambiguates a repeated name and the\r\n    /// receiver must be told the name it can actually address.\r\n    pub name: String,\r\n    pub url: String,\r\n    pub bytes: u64,\r\n}\r\n\r\n/// Parse a `--ttl` specification into milliseconds.\r\n///\r\n/// A UNIT IS REQUIRED. A bare number would have to mean seconds, or days, or\r\n/// milliseconds, and every reader would pick a different one — a `--ttl 30`\r\n/// that silently meant 30 seconds when the sender meant 30 days is a deleted\r\n/// attachment, so the refusal is deliberate and names what it wanted.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\npub fn parse_ttl_ms(spec: &str) -> Result<u64, String> {\r\n    let trimmed = spec.trim();\r\n    let (digits, unit) = trimmed.split_at(\r\n        trimmed\r\n            .find(|c: char| !c.is_ascii_digit())\r\n            .ok_or_else(|| format!(\"ttl needs a unit (s, m, h, d): {trimmed}\"))?,\r\n    );\r\n    let count: u64 = digits\r\n        .parse()\r\n        .map_err(|_| format!(\"ttl must start with a count: {trimmed}\"))?;\r\n    let unit_ms: u64 = match unit {\r\n        \"s\" => 1_000,\r\n        \"m\" => 60 * 1_000,\r\n        \"h\" => 60 * 60 * 1_000,\r\n        \"d\" => 24 * 60 * 60 * 1_000,\r\n        other => return Err(format!(\"unknown ttl unit {other:?} (want s, m, h, d)\")),\r\n    };\r\n    // A ttl that overflows is refused rather than saturated: saturating would\r\n    // hand back a lifetime the sender did not ask for and cannot see.\r\n    count\r\n        .checked_mul(unit_ms)\r\n        .filter(|ms| *ms > 0)\r\n        .ok_or_else(|| format!(\"ttl out of range: {trimmed}\"))\r\n}\r\n\r\n/// How an entry's path is interpreted. W0 creates references only.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(rename_all = \"snake_case\")]\r\npub enum ServedKind {\r\n    File,\r\n    Dir,\r\n    Attachment,\r\n}\r\n\r\n/// A stable assignment; reference paths are resolved again when requested.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(deny_unknown_fields)]\r\npub struct ServedEntry {\r\n    pub id: String,\r\n    pub kind: ServedKind,\r\n    pub path: PathBuf,\r\n    pub served_name: String,\r\n    pub registered_at_ms: u64,\r\n    /// Optional lifetime in milliseconds from registration, independent of kind.\r\n    // [impl->REQ-WEB-SERVING-REGISTRY]\r\n    pub ttl_ms: Option<u64>,\r\n    /// Optional receiving endpoint, independent of kind. Fetch enforcement is W2.\r\n    #[serde(default)]\r\n    pub audience: Option<String>,\r\n    /// Registering endpoint reported by the local caller; not a WEB access credential.\r\n    pub origin: Option<String>,\r\n    /// Adapter owning a manifest-created entry, absent for explicit registrations.\r\n    pub adapter: Option<String>,\r\n    /// Expose the same served name directly below the node, as well as `/f/`.\r\n    #[serde(default)]\r\n    pub short_alias: bool,\r\n}\r\n\r\n/// Retired names remain reserved for exactly this source identity.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\n#[serde(deny_unknown_fields)]\r\nstruct NameOwner {\r\n    path: PathBuf,\r\n    kind: ServedKind,\r\n}\r\n\r\n/// Persistent allocation history and the currently exposed entries.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\n#[serde(deny_unknown_fields)]\r\npub struct ServingRegistry {\r\n    schema_version: u32,\r\n    entries: Vec<ServedEntry>,\r\n    // The key is the requested name, not an assigned suffix. Splitting at the\r\n    // final non-leading dot is one-to-one, including dotfiles and trailing dots.\r\n    next_suffix: BTreeMap<String, u64>,\r\n    name_owners: BTreeMap<String, NameOwner>,\r\n}\r\n\r\n/// Serialize a registry transaction across threads and processes.\r\n///\r\n/// Acquire BEFORE loading and keep the returned file alive through mutation,\r\n/// snapshot cleanup/rollback, and explicit publication. This does not load or\r\n/// save anything itself, so no-op transactions need not publish a registry.\r\n///\r\n/// The stable sibling `registry.lock` is never renamed or removed: locking the\r\n/// registry itself would stop coordinating writers after atomic replacement.\r\n/// Closing the returned file (including process exit) releases the lock. Do not\r\n/// acquire it again while holding it; independently opened handles contend.\r\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\npub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\r\n    use fs2::FileExt;\r\n\r\n    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\r\n        fs::create_dir_all(parent)?;\r\n    }\r\n    let mut lock_path = registry_path.as_os_str().to_os_string();\r\n    lock_path.push(\".lock\");\r\n    let lock = fs::OpenOptions::new()\r\n        .create(true)\r\n        .write(true)\r\n        .truncate(false)\r\n        .open(PathBuf::from(lock_path))?;\r\n    lock.lock_exclusive()?;\r\n    Ok(lock)\r\n}\r\n\r\nimpl ServingRegistry {\r\n    /// Missing is empty. Unreadable, malformed, or inconsistent history is an\r\n    /// error: never recover by resetting the allocator and reusing stale URLs.\r\n    pub fn load_at(registry_path: &Path) -> io::Result<Self> {\r\n        let bytes = match fs::read(registry_path) {\r\n            Ok(bytes) => bytes,\r\n            Err(error) if error.kind() == io::ErrorKind::NotFound => {\r\n                // A dangling symlink is an existing, broken store, not absence.\r\n                match fs::symlink_metadata(registry_path) {\r\n                    Err(missing) if missing.kind() == io::ErrorKind::NotFound => {\r\n                        return Ok(Self {\r\n                            schema_version: SCHEMA_VERSION,\r\n                            entries: Vec::new(),\r\n                            next_suffix: BTreeMap::new(),\r\n                            name_owners: BTreeMap::new(),\r\n                        });\r\n                    }\r\n                    Err(error) => return Err(error),\r\n                    Ok(_) => return Err(error),\r\n                }\r\n            }\r\n            Err(error) => return Err(error),\r\n        };\r\n        let registry: Self = serde_json::from_slice(&bytes).map_err(invalid_data)?;\r\n        registry.validate()?;\r\n        Ok(registry)\r\n    }\r\n\r\n    /// Publish a durable old-or-new snapshot, creating its parent directory.\r\n    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\r\n    /// through this publication; locking only this save cannot repair stale state.\r\n    pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\r\n        self.validate()?;\r\n        let bytes = serde_json::to_vec_pretty(self).map_err(invalid_data)?;\r\n        if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\r\n            fs::create_dir_all(parent)?;\r\n        }\r\n        atomic_write_bytes_durable(registry_path, &bytes)\r\n    }\r\n\r\n    pub fn entries(&self) -> impl Iterator<Item = &ServedEntry> {\r\n        self.entries.iter()\r\n    }\r\n\r\n    /// Served names take precedence over IDs if a caller explicitly chose an ID\r\n    /// as a name. Both are exact, case-sensitive registry identifiers.\r\n    pub fn get(&self, name_or_id: &str) -> Option<&ServedEntry> {\r\n        self.entries\r\n            .iter()\r\n            .find(|entry| entry.served_name == name_or_id)\r\n            .or_else(|| self.entries.iter().find(|entry| entry.id == name_or_id))\r\n    }\r\n\r\n    /// Register an absolute, lexically normalized file/directory reference.\r\n    /// No canonicalization: symlink targets and bytes may change after registration.\r\n    /// An existing path returns its original assignment even after deletion or\r\n    /// when a different explicit name, origin, or timestamp is supplied.\r\n    // [impl->REQ-WEB-SERVING-REGISTRY]\r\n    pub fn add_reference(\r\n        &mut self,\r\n        absolute_path: &Path,\r\n        requested_name: Option<&str>,\r\n        origin: Option<&str>,\r\n        now_ms: u64,\r\n    ) -> io::Result<ServedEntry> {\r\n        if !valid_reference_path(absolute_path) {\r\n            return Err(io::Error::new(\r\n                io::ErrorKind::InvalidInput,\r\n                \"served source must be an absolute, lexically normalized path\",\r\n            ));\r\n        }\r\n        if let Some(entry) = self.entries.iter().find(|entry| entry.path == absolute_path) {\r\n            return Ok(entry.clone());\r\n        }\r\n        let name = requested_name\r\n            .or_else(|| absolute_path.file_name().and_then(|name| name.to_str()))\r\n            .filter(|name| valid_name(name))\r\n            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, \"invalid served name\"))?;\r\n        let metadata = fs::metadata(absolute_path)?;\r\n        let kind = if metadata.is_file() {\r\n            ServedKind::File\r\n        } else if metadata.is_dir() {\r\n            ServedKind::Dir\r\n        } else {\r\n            return Err(io::Error::new(\r\n                io::ErrorKind::InvalidInput,\r\n                \"served source must be a regular file or directory\",\r\n            ));\r\n        };\r\n        let (served_name, next) = self.allocate_name(name, absolute_path, kind)?;\r\n        let entry = ServedEntry {\r\n            id: uuid::Uuid::new_v4().to_string(),\r\n            kind,\r\n            path: absolute_path.to_path_buf(),\r\n            served_name,\r\n            registered_at_ms: now_ms,\r\n            ttl_ms: None,\r\n            audience: None,\r\n            origin: origin.map(str::to_owned),\r\n            adapter: None,\r\n            short_alias: false,\r\n        };\r\n        self.next_suffix.insert(name.to_owned(), next);\r\n        self.name_owners.insert(entry.served_name.clone(), NameOwner {\r\n            path: entry.path.clone(),\r\n            kind,\r\n        });\r\n        self.entries.push(entry.clone());\r\n        Ok(entry)\r\n    }\r\n\r\n    /// Receipt-scoped live reference. A duplicate never changes exposure or its\r\n    /// deadline; an unrelated registration is not authority to rescope it.\r\n    /// `None` means the source is absent, not a diagnostic.\r\n    // [impl->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\r\n    pub fn add_input_reference(\r\n        &mut self,\r\n        source: &Path,\r\n        receipt_id: &str,\r\n        audience: &str,\r\n        received_at_ms: u64,\r\n        now_ms: u64,\r\n    ) -> io::Result<Option<ServedEntry>> {\r\n        if !valid_reference_path(source) || receipt_id.is_empty() || audience.trim().is_empty() {\r\n            return Err(io::Error::new(io::ErrorKind::InvalidInput, \"INPUT_PATH_INVALID\"));\r\n        }\r\n        match fs::metadata(source) {\r\n            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),\r\n            Err(error) => return Err(io::Error::new(error.kind(), format!(\"INPUT_PATH_METADATA: {error}\"))),\r\n            Ok(metadata) if !metadata.is_file() && !metadata.is_dir() => {\r\n                return Err(io::Error::new(io::ErrorKind::InvalidInput, \"INPUT_PATH_INVALID\"));\r\n            }\r\n            Ok(_) => {}\r\n        }\r\n        let origin = format!(\"user-input:{receipt_id}\");\r\n        if let Some(entry) = self.entries.iter().find(|entry| entry.path == source) {\r\n            if !entry.origin.as_deref().is_some_and(|value| value.starts_with(\"user-input:\"))\r\n                || entry.audience.as_deref() != Some(audience)\r\n                || entry.ttl_ms != Some(HELPER_ENTRY_TTL_MS)\r\n                || entry.adapter.is_some()\r\n                || entry.kind == ServedKind::Attachment\r\n            {\r\n                return Err(io::Error::new(io::ErrorKind::PermissionDenied, \"INPUT_PATH_SCOPE_CONFLICT\"));\r\n            }\r\n            if entry.registered_at_ms.saturating_add(HELPER_ENTRY_TTL_MS) > now_ms {\r\n                return Ok(Some(entry.clone()));\r\n            }\r\n            // A later submission may replace its same-audience expired input\r\n            // reference. Replaying the original receipt cannot renew it.\r\n            if entry.origin.as_deref() == Some(origin.as_str()) {\r\n                return Err(io::Error::new(io::ErrorKind::PermissionDenied, \"INPUT_PATH_EXPIRED\"));\r\n            }\r\n            let id = entry.id.clone();\r\n            self.remove(&id);\r\n        }\r\n        let registered_at_ms = received_at_ms.min(now_ms);\r\n        if registered_at_ms.saturating_add(HELPER_ENTRY_TTL_MS) <= now_ms\r\n        {\r","truncated":true,"truncatedBy":"lines","totalLines":1414,"totalBytes":14122,"outputLines":323,"outputBytes":14122,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1414,"displayContent":{"text":"//! Node-local, authoritative serving assignments (ADR-0057).\r\n//!\r\n//! Schema 1 stores live entries, per-family suffix counters, and the absolute\r\n//! path and kind each name last identified. Removal changes only live entries;\r\n//! the same path and kind may reclaim their names (ADR-0057 Amendment 2).\r\n//! Broker mutations and brain reaping share a cross-process writer guard;\r\n//! corruption is never an empty store.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n\r\nuse std::collections::{BTreeMap, BTreeSet};\r\nuse std::fs;\r\nuse std::io;\r\nuse std::path::{Component, Path, PathBuf};\r\n\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse crate::atomic::atomic_write_bytes_durable;\r\n\r\nconst SCHEMA_VERSION: u32 = 1;\r\n\r\n/// An attachment's lifetime when the sender names none: 30 days (ADR-0058,\r\n/// operator-set). It is a CONSTANT rather than a config knob on purpose — the\r\n/// figure is a published contract in the attachments page, and a per-send\r\n/// `--ttl` already covers the case where a sender wants something else.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\npub const DEFAULT_ATTACHMENT_TTL_MS: u64 = 30 * 24 * 60 * 60 * 1_000;\r\n\r\n/// The lifetime a FILE_ACCESS_HELPER entry carries: 24 hours (ADR-0058\r\n/// Amendment 1, operator-directed). Shorter than an attachment's because the\r\n/// user quoted a path in passing rather than sending a copy.\r\n// [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\npub const HELPER_ENTRY_TTL_MS: u64 = 24 * 60 * 60 * 1_000;\r\n\r\n/// One attachment as the message envelope carries it: a name, the URL to pull\r\n/// it from, and the size in bytes.\r\n///\r\n/// The SIZE rides with the link because attachments are pull-model — a receiver\r\n/// deciding whether to fetch has to be able to decide BEFORE it fetches, and a\r\n/// link alone makes that impossible without a request.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct AttachmentRef {\r\n    /// The SERVED name, which is what the URL ends in — never the source\r\n    /// basename, because the registry disambiguates a repeated name and the\r\n    /// receiver must be told the name it can actually address.\r\n    pub name: String,\r\n    pub url: String,\r\n    pub bytes: u64,\r\n}\r\n\r\n/// Parse a `--ttl` specification into milliseconds.\r\n///\r\n/// A UNIT IS REQUIRED. A bare number would have to mean seconds, or days, or\r\n/// milliseconds, and every reader would pick a different one — a `--ttl 30`\r\n/// that silently meant 30 seconds when the sender meant 30 days is a deleted\r\n/// attachment, so the refusal is deliberate and names what it wanted.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\npub fn parse_ttl_ms(spec: &str) -> Result<u64, String> {\r\n    let trimmed = spec.trim();\r\n    let (digits, unit) = trimmed.split_at(\r\n        trimmed\r\n            .find(|c: char| !c.is_ascii_digit())\r\n            .ok_or_else(|| format!(\"ttl needs a unit (s, m, h, d): {trimmed}\"))?,\r\n    );\r\n    let count: u64 = digits\r\n        .parse()\r\n        .map_err(|_| format!(\"ttl must start with a count: {trimmed}\"))?;\r\n    let unit_ms: u64 = match unit {\r\n        \"s\" => 1_000,\r\n        \"m\" => 60 * 1_000,\r\n        \"h\" => 60 * 60 * 1_000,\r\n        \"d\" => 24 * 60 * 60 * 1_000,\r\n        other => return Err(format!(\"unknown ttl unit {other:?} (want s, m, h, d)\")),\r\n    };\r\n    // A ttl that overflows is refused rather than saturated: saturating would\r\n    // hand back a lifetime the sender did not ask for and cannot see.\r\n    count\r\n        .checked_mul(unit_ms)\r\n        .filter(|ms| *ms > 0)\r\n        .ok_or_else(|| format!(\"ttl out of range: {trimmed}\"))\r\n}\r\n\r\n/// How an entry's path is interpreted. W0 creates references only.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(rename_all = \"snake_case\")]\r\npub enum ServedKind {\r\n    File,\r\n    Dir,\r\n    Attachment,\r\n}\r\n\r\n/// A stable assignment; reference paths are resolved again when requested.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\n#[serde(deny_unknown_fields)]\r\npub struct ServedEntry {\r\n    pub id: String,\r\n    pub kind: ServedKind,\r\n    pub path: PathBuf,\r\n    pub served_name: String,\r\n    pub registered_at_ms: u64,\r\n    /// Optional lifetime in milliseconds from registration, independent of kind.\r\n    // [impl->REQ-WEB-SERVING-REGISTRY]\r\n    pub ttl_ms: Option<u64>,\r\n    /// Optional receiving endpoint, independent of kind. Fetch enforcement is W2.\r\n    #[serde(default)]\r\n    pub audience: Option<String>,\r\n    /// Registering endpoint reported by the local caller; not a WEB access credential.\r\n    pub origin: Option<String>,\r\n    /// Adapter owning a manifest-created entry, absent for explicit registrations.\r\n    pub adapter: Option<String>,\r\n    /// Expose the same served name directly below the node, as well as `/f/`.\r\n    #[serde(default)]\r\n    pub short_alias: bool,\r\n}\r\n\r\n/// Retired names remain reserved for exactly this source identity.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\n#[serde(deny_unknown_fields)]\r\nstruct NameOwner {\r\n    path: PathBuf,\r\n    kind: ServedKind,\r\n}\r\n\r\n/// Persistent allocation history and the currently exposed entries.\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\n#[serde(deny_unknown_fields)]\r\npub struct ServingRegistry {\r\n    schema_version: u32,\r\n    entries: Vec<ServedEntry>,\r\n    // The key is the requested name, not an assigned suffix. Splitting at the\r\n    // final non-leading dot is one-to-one, including dotfiles and trailing dots.\r\n    next_suffix: BTreeMap<String, u64>,\r\n    name_owners: BTreeMap<String, NameOwner>,\r\n}\r\n\r\n/// Serialize a registry transaction across threads and processes.\r\n///\r\n/// Acquire BEFORE loading and keep the returned file alive through mutation,\r\n/// snapshot cleanup/rollback, and explicit publication. This does not load or\r\n/// save anything itself, so no-op transactions need not publish a registry.\r\n///\r\n/// The stable sibling `registry.lock` is never renamed or removed: locking the\r\n/// registry itself would stop coordinating writers after atomic replacement.\r\n/// Closing the returned file (including process exit) releases the lock. Do not\r\n/// acquire it again while holding it; independently opened handles contend.\r\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\npub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\r\n    use fs2::FileExt;\r\n\r\n    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\r\n        fs::create_dir_all(parent)?;\r\n    }\r\n    let mut lock_path = registry_path.as_os_str().to_os_string();\r\n    lock_path.push(\".lock\");\r\n    let lock = fs::OpenOptions::new()\r\n        .create(true)\r\n        .write(true)\r\n        .truncate(false)\r\n        .open(PathBuf::from(lock_path))?;\r\n    lock.lock_exclusive()?;\r\n    Ok(lock)\r\n}\r\n\r\nimpl ServingRegistry {\r\n    /// Missing is empty. Unreadable, malformed, or inconsistent history is an\r\n    /// error: never recover by resetting the allocator and reusing stale URLs.\r\n    pub fn load_at(registry_path: &Path) -> io::Result<Self> {\r\n        let bytes = match fs::read(registry_path) {\r\n            Ok(bytes) => bytes,\r\n            Err(error) if error.kind() == io::ErrorKind::NotFound => {\r\n                // A dangling symlink is an existing, broken store, not absence.\r\n                match fs::symlink_metadata(registry_path) {\r\n                    Err(missing) if missing.kind() == io::ErrorKind::NotFound => {\r\n                        return Ok(Self {\r\n                            schema_version: SCHEMA_VERSION,\r\n                            entries: Vec::new(),\r\n                            next_suffix: BTreeMap::new(),\r\n                            name_owners: BTreeMap::new(),\r\n                        });\r\n                    }\r\n                    Err(error) => return Err(error),\r\n                    Ok(_) => return Err(error),\r\n                }\r\n            }\r\n            Err(error) => return Err(error),\r\n        };\r\n        let registry: Self = serde_json::from_slice(&bytes).map_err(invalid_data)?;\r\n        registry.validate()?;\r\n        Ok(registry)\r\n    }\r\n\r\n    /// Publish a durable old-or-new snapshot, creating its parent directory.\r\n    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\r\n    /// through this publication; locking only this save cannot repair stale state.\r\n    pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\r\n        self.validate()?;\r\n        let bytes = serde_json::to_vec_pretty(self).map_err(invalid_data)?;\r\n        if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\r\n            fs::create_dir_all(parent)?;\r\n        }\r\n        atomic_write_bytes_durable(registry_path, &bytes)\r\n    }\r\n\r\n    pub fn entries(&self) -> impl Iterator<Item = &ServedEntry> {\r\n        self.entries.iter()\r\n    }\r\n\r\n    /// Served names take precedence over IDs if a caller explicitly chose an ID\r\n    /// as a name. Both are exact, case-sensitive registry identifiers.\r\n    pub fn get(&self, name_or_id: &str) -> Option<&ServedEntry> {\r\n        self.entries\r\n            .iter()\r\n            .find(|entry| entry.served_name == name_or_id)\r\n            .or_else(|| self.entries.iter().find(|entry| entry.id == name_or_id))\r\n    }\r\n\r\n    /// Register an absolute, lexically normalized file/directory reference.\r\n    /// No canonicalization: symlink targets and bytes may change after registration.\r\n    /// An existing path returns its original assignment even after deletion or\r\n    /// when a different explicit name, origin, or timestamp is supplied.\r\n    // [impl->REQ-WEB-SERVING-REGISTRY]\r\n    pub fn add_reference(\r\n        &mut self,\r\n        absolute_path: &Path,\r\n        requested_name: Option<&str>,\r\n        origin: Option<&str>,\r\n        now_ms: u64,\r\n    ) -> io::Result<ServedEntry> {\r\n        if !valid_reference_path(absolute_path) {\r\n            return Err(io::Error::new(\r\n                io::ErrorKind::InvalidInput,\r\n                \"served source must be an absolute, lexically normalized path\",\r\n            ));\r\n        }\r\n        if let Some(entry) = self.entries.iter().find(|entry| entry.path == absolute_path) {\r\n            return Ok(entry.clone());\r\n        }\r\n        let name = requested_name\r\n            .or_else(|| absolute_path.file_name().and_then(|name| name.to_str()))\r\n            .filter(|name| valid_name(name))\r\n            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, \"invalid served name\"))?;\r\n        let metadata = fs::metadata(absolute_path)?;\r\n        let kind = if metadata.is_file() {\r\n            ServedKind::File\r\n        } else if metadata.is_dir() {\r\n            ServedKind::Dir\r\n        } else {\r\n            return Err(io::Error::new(\r\n                io::ErrorKind::InvalidInput,\r\n                \"served source must be a regular file or directory\",\r\n            ));\r\n        };\r\n        let (served_name, next) = self.allocate_name(name, absolute_path, kind)?;\r\n        let entry = ServedEntry {\r\n            id: uuid::Uuid::new_v4().to_string(),\r\n            kind,\r\n            path: absolute_path.to_path_buf(),\r\n            served_name,\r\n            registered_at_ms: now_ms,\r\n            ttl_ms: None,\r\n            audience: None,\r\n            origin: origin.map(str::to_owned),\r\n            adapter: None,\r\n            short_alias: false,\r\n        };\r\n        self.next_suffix.insert(name.to_owned(), next);\r\n        self.name_owners.insert(entry.served_name.clone(), NameOwner {\r\n            path: entry.path.clone(),\r\n            kind,\r\n        });\r\n        self.entries.push(entry.clone());\r\n        Ok(entry)\r\n    }\r\n\r\n    /// Receipt-scoped live reference. A duplicate never changes exposure or its\r\n    /// deadline; an unrelated registration is not authority to rescope it.\r\n    /// `None` means the source is absent, not a diagnostic.\r\n    // [impl->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\r\n    pub fn add_input_reference(\r\n        &mut self,\r\n        source: &Path,\r\n        receipt_id: &str,\r\n        audience: &str,\r\n        received_at_ms: u64,\r\n        now_ms: u64,\r\n    ) -> io::Result<Option<ServedEntry>> {\r\n        if !valid_reference_path(source) || receipt_id.is_empty() || audience.trim().is_empty() {\r\n            return Err(io::Error::new(io::ErrorKind::InvalidInput, \"INPUT_PATH_INVALID\"));\r\n        }\r\n        match fs::metadata(source) {\r\n            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),\r\n            Err(error) => return Err(io::Error::new(error.kind(), format!(\"INPUT_PATH_METADATA: {error}\"))),\r\n            Ok(metadata) if !metadata.is_file() && !metadata.is_dir() => {\r\n                return Err(io::Error::new(io::ErrorKind::InvalidInput, \"INPUT_PATH_INVALID\"));\r\n            }\r\n            Ok(_) => {}\r\n        }\r\n        let origin = format!(\"user-input:{receipt_id}\");\r\n        if let Some(entry) = self.entries.iter().find(|entry| entry.path == source) {\r\n            if !entry.origin.as_deref().is_some_and(|value| value.starts_with(\"user-input:\"))\r\n                || entry.audience.as_deref() != Some(audience)\r\n                || entry.ttl_ms != Some(HELPER_ENTRY_TTL_MS)\r\n                || entry.adapter.is_some()\r\n                || entry.kind == ServedKind::Attachment\r\n            {\r\n                return Err(io::Error::new(io::ErrorKind::PermissionDenied, \"INPUT_PATH_SCOPE_CONFLICT\"));\r\n            }\r\n            if entry.registered_at_ms.saturating_add(HELPER_ENTRY_TTL_MS) > now_ms {\r\n                return Ok(Some(entry.clone()));\r\n            }\r\n            // A later submission may replace its same-audience expired input\r\n            // reference. Replaying the original receipt cannot renew it.\r\n            if entry.origin.as_deref() == Some(origin.as_str()) {\r\n                return Err(io::Error::new(io::ErrorKind::PermissionDenied, \"INPUT_PATH_EXPIRED\"));\r\n            }\r\n            let id = entry.id.clone();\r\n            self.remove(&id);\r\n        }\r\n        let registered_at_ms = received_at_ms.min(now_ms);\r\n        if registered_at_ms.saturating_add(HELPER_ENTRY_TTL_MS) <= now_ms\r\n        {\r\n…\n        }\n…\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,null,325,null,334,null,602]},"fileSize":65355,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-store\\src\\serving.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1414,"totalBytes":14122,"outputLines":323,"outputBytes":14122,"shownRange":{"start":1,"end":323},"nextOffset":324}}},"isError":false,"timestamp":1789522902757}}
{"type":"message","id":"0387d56b","parentId":"1def06bc","timestamp":"2026-09-16T01:41:42.765Z","message":{"role":"toolResult","toolCallId":"call_a70qvKerXAXNFCvZ2oitN9kT|fc_0cdf94810c888815016aa9f3d6a94c87d1bef984dc3d127e29","toolName":"read","content":[{"type":"text","text":"//! Local serving-registry mutations. The daemon is the only writer; CLI\n//! clients use the existing local control transport and never save the store.\n\nuse std::io;\nuse std::path::{Path, PathBuf};\n\nuse serde::{Deserialize, Serialize};\nuse spt_store::serving::{ServedEntry, ServingRegistry};\n\nuse crate::codec::{read_frame, write_frame};\nuse crate::frame::{Envelope, Role};\nuse crate::transport::{send_hello, LocalSocketTransport};\n\npub const KIND_SERVE_REQUEST: &str = \"serve_request\";\npub const KIND_SERVE_RESULT: &str = \"serve_result\";\n\n/// An explicit local registry operation; no HTTP request can mutate exposure.\n// [impl->REQ-WEB-SERVING-REGISTRY]\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"op\", rename_all = \"snake_case\")]\npub enum ServeRequest {\n    Add {\n        path: PathBuf,\n        as_name: Option<String>,\n        origin: Option<String>,\n    },\n    /// Snapshot `path` into the node's attachment store and serve the COPY\n    /// (ADR-0058). The snapshot is taken HERE rather than by the caller for the\n    /// reason the registry has one writer at all: the bytes and the entry that\n    /// names them are created under the same lock, so there is no window in\n    /// which one exists without the other.\n    // [impl->REQ-WEB-ATTACHMENT-PULL]\n    Attach {\n        path: PathBuf,\n        as_name: Option<String>,\n        origin: Option<String>,\n        ttl_ms: u64,\n        audience: Option<String>,\n    },\n    /// Register `path` as an ordinary REFERENCE and narrow it in the same\n    /// locked pass (ADR-0058 Amendment 1): the FILE_ACCESS_HELPER's entry is a\n    /// live reference — edits visible, 404 once deleted — that merely expires\n    /// and is addressed to one endpoint.\n    ///\n    /// Two ops would leave a window in which the file is registered and NOT yet\n    /// narrowed, which is the one state this request must never produce.\n    // [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\n    AddScoped {\n        path: PathBuf,\n        as_name: Option<String>,\n        origin: Option<String>,\n        ttl_ms: u64,\n        audience: String,\n    },\n    /// Only the locally held remote-controller viewport requests this operation.\n    /// The audience is its established target, never a field supplied by the peer.\n    AddInputReference {\n        path: PathBuf,\n        receipt_id: String,\n        audience: String,\n        received_at_ms: u64,\n    },\n    Remove {\n        name_or_id: String,\n    },\n    List,\n    Reconcile,\n    /// Query the broker-owned listener, never the caller's bind configuration.\n    // [impl->REQ-WEB-URL-BOUND-PORT]\n    DocsStatus,\n    /// Start the LAN bootstrap listener (ADR-0059, releases#266). The listener\n    /// lives HERE, in the daemon, because the set gate anchors on the daemon's\n    /// OWN executable captured at t=0 and the listener must outlive the CLI\n    /// process that asked for it.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    LanBootstrap {\n        /// An explicit port for this start; `None` = config, env, default.\n        port: Option<u16>,\n    },\n    /// Tear the LAN bootstrap listener down.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    LanStop,\n    /// Read the listener's state without changing it — the source the\n    /// LAN-EXPOSED now-signal line reads.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    LanStatus,\n}\n\n/// The daemon's persisted outcome, or its actual refusal. An empty list is\n/// distinct from an unreadable registry.\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(tag = \"outcome\", rename_all = \"snake_case\")]\npub enum ServeResult {\n    Entry { entry: ServedEntry },\n    /// An input reference resolved to no source. No diagnostic is attached.\n    Missing,\n    /// An attachment, with the SNAPSHOT's size — the sender needs it for the\n    /// envelope, and the daemon is the only party that ever saw the copy.\n    // [impl->REQ-WEB-ATTACHMENT-PULL]\n    Attached { entry: ServedEntry, bytes: u64 },\n    Removed { entry: ServedEntry },\n    Listed { entries: Vec<ServedEntry> },\n    Refused { error: String },\n    // [impl->REQ-WEB-URL-BOUND-PORT]\n    DocsStatus { port: Option<u16> },\n    /// The LAN bootstrap listener is up: the URL a stranger opens and one\n    /// `(triple, sha256)` anchor per SERVED triple, host triple first.\n    /// `already_up` marks the idempotent answer — the SAME url and shas, with\n    /// no second listener and no re-run of the gate.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    LanUp {\n        url: String,\n        shas: Vec<(String, String)>,\n        port: u16,\n        already_up: bool,\n        /// Absent on older brokers: admission cannot infer the CLI is the binder.\n        // [impl->REQ-WEB-LAN-BOOTSTRAP-FIREWALL]\n        #[serde(default)]\n        binder: Option<PathBuf>,\n    },\n    /// The listener is down. `code` is `LAN_BOOTSTRAP_NOT_UP` when the stop\n    /// found nothing to stop — an outcome, not an error.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    LanStopped { code: Option<String> },\n    /// The set gate refused the start BY NAME, and nothing is being served.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-INTEGRITY]\n    LanRefused { code: String },\n}\n\n/// Apply a request at an explicit home. Publication completes before success\n/// is returned, and a failed publication cannot advance an in-memory counter.\n// [impl->REQ-WEB-SERVING-REGISTRY]\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\npub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n    // [impl->REQ-WEB-URL-BOUND-PORT]\n    if matches!(&request, ServeRequest::DocsStatus) {\n        return Ok(ServeResult::DocsStatus { port: crate::docshost::bound_docs_port() });\n    }\n    // The LAN listener is PROCESS state, not registry state: it lives in this\n    // daemon's memory and nothing about it is written to disk. It is answered\n    // BEFORE the registry writer lock so a bootstrap can never be serialized\n    // behind — or worse, blocked by — an unrelated registry mutation.\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    if let Some(result) = lan_request(home, &request) {\n        return Ok(result);\n    }\n    let path = spt_store::perch::serving_registry_file_in(home);\n    let _writer = spt_store::serving::lock_registry_at(&path)?;\n    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n        if matches!(&request, ServeRequest::AddInputReference { .. }) {\n            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n        } else {\n            error\n        }\n    })?;\n    match request {\n        ServeRequest::Add { path: source, as_name, origin } => {\n            let entry = registry.add_reference(\n                &source,\n                as_name.as_deref(),\n                origin.as_deref(),\n                now_ms,\n            )?;\n            registry.save_at(&path)?;\n            Ok(ServeResult::Entry { entry })\n        }\n        // [impl->REQ-WEB-ATTACHMENT-PULL]\n        ServeRequest::Attach { path: source, as_name, origin, ttl_ms, audience } => {\n            let (snapshot, bytes) = spt_store::serving::write_snapshot_in(home, &source)?;\n            // The requested name defaults to the SOURCE's basename; the registry\n            // disambiguates a repeat, so two sends of one file get two stable\n            // distinct URLs and neither link ever changes meaning.\n            let requested = match as_name {\n                Some(name) => name,\n                None => source\n                    .file_name()\n                    .and_then(|name| name.to_str())\n                    .map(str::to_owned)\n                    .ok_or_else(|| {\n                        io::Error::new(\n                            io::ErrorKind::InvalidInput,\n                            \"SERVE_ATTACH_NAME: the attachment path has no usable file name\",\n                        )\n                    })?,\n            };\n            let entry = match registry.add_attachment(\n                &snapshot,\n                &requested,\n                origin.as_deref(),\n                ttl_ms,\n                audience.as_deref(),\n                now_ms,\n            ) {\n                Ok(entry) => entry,\n                Err(error) => {\n                    // A refused registration must not leave bytes nothing points\n                    // at: the reaper only ever deletes what an ENTRY named, so an\n                    // orphan snapshot would live until the store is wiped by hand.\n                    let _ = std::fs::remove_file(&snapshot);\n                    return Err(error);\n                }\n            };\n            if let Err(error) = registry.save_at(&path) {\n                // Same reasoning as the refused registration above: an entry that\n                // did not persist names nothing, so its bytes must not survive.\n                let _ = std::fs::remove_file(&snapshot);\n                return Err(error);\n            }\n            Ok(ServeResult::Attached { entry, bytes })\n        }\n        // [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\n        ServeRequest::AddScoped { path: source, as_name, origin, ttl_ms, audience } => {\n            let entry = registry.add_reference(&source, as_name.as_deref(), origin.as_deref(), now_ms)?;\n            let scoped = registry\n                .scope_entry(&entry.id, Some(ttl_ms), Some(&audience), origin.as_deref())\n                .ok_or_else(|| io::Error::other(\"SERVE_SCOPE_LOST: the entry vanished between add and scope\"))?;\n            registry.save_at(&path)?;\n            Ok(ServeResult::Entry { entry: scoped })\n        }\n        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\n            // Resolve the submitter's home HERE, never on the receiving node.\n            let source = if let Some(relative) = source.to_str().and_then(|p| p.strip_prefix(\"~/\")) {\n                std::env::home_dir()\n                    .ok_or_else(|| io::Error::other(\"INPUT_PATH_HOME_UNAVAILABLE\"))?\n                    .join(relative)\n            } else {\n                source\n            };\n            let previous_id = registry.entries().find(|entry| entry.path == source)\n                .map(|entry| entry.id.clone());\n            match registry.add_input_reference(&source, &receipt_id, &audience, received_at_ms, now_ms)? {\n                None => Ok(ServeResult::Missing),\n                Some(entry) => {\n                    if previous_id.as_deref() != Some(entry.id.as_str()) {\n                        registry.save_at(&path)\n                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\n                    }\n                    Ok(ServeResult::Entry { entry })\n                }\n            }\n        }\n        ServeRequest::Remove { name_or_id } => {\n            let entry = registry.remove(&name_or_id).ok_or_else(|| {\n                io::Error::new(io::ErrorKind::NotFound, format!(\"SERVE_NOT_FOUND: {name_or_id}\"))\n            })?;\n            registry.save_at(&path)?;\n            Ok(ServeResult::Removed { entry })\n        }\n        ServeRequest::List => Ok(ServeResult::Listed {\n            entries: registry.entries().cloned().collect(),\n        }),\n        // Answered above, before the writer lock.\n        ServeRequest::DocsStatus => unreachable!(\"docs status is answered before the registry writer\"),\n        ServeRequest::LanBootstrap { .. } | ServeRequest::LanStop | ServeRequest::LanStatus => {\n            unreachable!(\"LAN requests are answered before the registry writer\")\n        }\n        ServeRequest::Reconcile => {\n            reconcile_registry(home, &mut registry, now_ms)?;\n            registry.save_at(&path)?;\n            Ok(ServeResult::Listed {\n                entries: registry.entries().cloned().collect(),\n            })\n        }\n    }\n}\n\n/// Answer the three LAN requests, or `None` for a registry request.\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\nfn lan_request(home: &Path, request: &ServeRequest) -> Option<ServeResult> {\n    match request {\n        ServeRequest::LanBootstrap { port } => Some(lan_bootstrap(home, *port)),\n        ServeRequest::LanStop => Some(match crate::lanhost::stop() {\n            Ok(true) => ServeResult::LanStopped { code: None },\n            Ok(false) => ServeResult::LanStopped {\n                code: Some(crate::lanhost::NOT_UP.to_string()),\n            },\n            Err(error) => ServeResult::Refused { error },\n        }),\n        ServeRequest::LanStatus => Some(match crate::lanhost::status() {\n            Some(up) => ServeResult::LanUp {\n                url: up.url,\n                shas: up.shas,\n                port: up.port,\n                already_up: true,\n                binder: Some(up.binder),\n            },\n            None => ServeResult::LanStopped {\n                code: Some(crate::lanhost::NOT_UP.to_string()),\n            },\n        }),\n        _ => None,\n    }\n}\n\n/// Start the LAN bootstrap listener at `home`, resolving the port the same way\n/// the daemon resolves every advisory port (explicit > env > config > default,\n/// with the rig's ephemeral posture on top).\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\nfn lan_bootstrap(home: &Path, port: Option<u16>) -> ServeResult {\n    let resolved = port.unwrap_or_else(|| {\n        crate::lanhost::resolve_daemon_lan_port(\n            crate::config::DaemonConfig::load().lan_bootstrap_port,\n            std::env::var(crate::lanhost::LAN_BOOTSTRAP_PORT_ENV).ok().as_deref(),\n            std::env::var_os(crate::docshost::TEST_EPHEMERAL_ADVISORY_PORTS_ENV)\n                .is_some_and(|value| value == \"1\"),\n        )\n    });\n    match crate::lanhost::start_at(home, resolved) {\n        Ok(up) => ServeResult::LanUp {\n            url: up.url,\n            shas: up.shas,\n            port: up.port,\n            already_up: up.already_up,\n            binder: Some(up.binder),\n        },\n        Err(code) => ServeResult::LanRefused { code },\n    }\n}\n\n/// Reconcile active base adapters at daemon startup under the same serialized\n/// writer as explicit registrations. Never call this from a client process.\n// [impl->REQ-WEB-SERVING-REGISTRY]\npub fn reconcile_at(home: &Path, now_ms: u64) -> io::Result<()> {\n    apply_at(home, ServeRequest::Reconcile, now_ms).map(|_| ())\n}\n\nfn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\n    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\n    let retired: Vec<_> = registry\n        .entries()\n        .filter(|entry| entry.adapter.as_ref().is_some_and(|adapter| {\n            !registered.iter().any(|(record, _)| &record.name == adapter)\n        }))\n        .map(|entry| entry.id.clone())\n        .collect();\n    for id in retired {\n        registry.remove(&id);\n    }\n    // Base registrations, not profile options: every adapter gets exactly one\n    // root, and an optional alias names that entry rather than another source.\n    for (record, manifest) in registered {\n        let root = spt_store::perch::create_adapter_web_dir_at(&home.join(\"adapters\"), &record.name)?;\n        registry.add_adapter(&root, &record.name, manifest.adapter.web_short_path.as_deref(), now_ms)?;\n    }\n    Ok(())\n}\n\n/// Ask the broker's single writer to reconcile after an adapter mutation.\n/// Transport timeouts remain unconfirmed outcomes, never client-side writes.\n// [impl->REQ-WEB-SERVING-REGISTRY]\npub fn reconcile(name: &str) -> io::Result<()> {\n    match call(name, ServeRequest::Reconcile)? {\n        ServeResult::Listed { .. } => Ok(()),\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\n        _ => Err(io::Error::new(io::ErrorKind::InvalidData, \"SERVE_UNEXPECTED_REPLY: reconcile\")),\n    }\n}\n\n/// Discover a running broker's listener. An old broker or unavailable listener\n/// is an error, not permission to guess the configured port.\n// [impl->REQ-WEB-URL-BOUND-PORT]\npub fn docs_port(name: &str) -> io::Result<u16> {\n    port_from_status(call(name, ServeRequest::DocsStatus)?)\n}\n\n/// The whole port decision over an ALREADY-RECEIVED reply, split out of\n/// [`docs_port`] so a bound port, an unavailable listener, a refusal and an old\n/// broker's unrelated reply can each be reached without a running daemon (doyle\n/// ruled the split 2026-09-11, the same decision-inside-IO shape hertz measured\n/// in `bootstrap_firewall::windows::verify`). Behaviour is unchanged: this is\n/// the match `docs_port` used to inline.\n// [impl->REQ-WEB-URL-BOUND-PORT]\npub fn port_from_status(result: ServeResult) -> io::Result<u16> {\n    match result {\n        ServeResult::DocsStatus { port: Some(port) } if port != 0 => Ok(port),\n        ServeResult::DocsStatus { .. } => Err(io::Error::other(\n            \"DOCS_LISTENER_UNAVAILABLE: the running daemon has no bound docs listener\",\n        )),\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\n        _ => Err(io::Error::other(\n            \"DOCS_LISTENER_UNREPORTED: the running daemon did not report its bound docs port\",\n        )),\n    }\n}\n\n/// Dispatch only after the local control transport's version handshake.\n// [impl->REQ-WEB-SERVING-REGISTRY]\npub(crate) fn handle(payload: serde_json::Value) -> Envelope {\n    let result = serde_json::from_value(payload)\n        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))\n        .and_then(|request| {\n            let now_ms = std::time::SystemTime::now()\n                .duration_since(std::time::UNIX_EPOCH)\n                .map_err(io::Error::other)?\n                .as_millis()\n                .try_into()\n                .map_err(io::Error::other)?;\n…\n        })\n        .unwrap_or_else(|e| ServeResult::Refused { error: e.to_string() });\n…\n}\n\n[Showing lines 1-393 of 851. Use :394 to continue]"}],"details":{"truncation":{"content":"//! Local serving-registry mutations. The daemon is the only writer; CLI\r\n//! clients use the existing local control transport and never save the store.\r\n\r\nuse std::io;\r\nuse std::path::{Path, PathBuf};\r\n\r\nuse serde::{Deserialize, Serialize};\r\nuse spt_store::serving::{ServedEntry, ServingRegistry};\r\n\r\nuse crate::codec::{read_frame, write_frame};\r\nuse crate::frame::{Envelope, Role};\r\nuse crate::transport::{send_hello, LocalSocketTransport};\r\n\r\npub const KIND_SERVE_REQUEST: &str = \"serve_request\";\r\npub const KIND_SERVE_RESULT: &str = \"serve_result\";\r\n\r\n/// An explicit local registry operation; no HTTP request can mutate exposure.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\n#[serde(tag = \"op\", rename_all = \"snake_case\")]\r\npub enum ServeRequest {\r\n    Add {\r\n        path: PathBuf,\r\n        as_name: Option<String>,\r\n        origin: Option<String>,\r\n    },\r\n    /// Snapshot `path` into the node's attachment store and serve the COPY\r\n    /// (ADR-0058). The snapshot is taken HERE rather than by the caller for the\r\n    /// reason the registry has one writer at all: the bytes and the entry that\r\n    /// names them are created under the same lock, so there is no window in\r\n    /// which one exists without the other.\r\n    // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n    Attach {\r\n        path: PathBuf,\r\n        as_name: Option<String>,\r\n        origin: Option<String>,\r\n        ttl_ms: u64,\r\n        audience: Option<String>,\r\n    },\r\n    /// Register `path` as an ordinary REFERENCE and narrow it in the same\r\n    /// locked pass (ADR-0058 Amendment 1): the FILE_ACCESS_HELPER's entry is a\r\n    /// live reference — edits visible, 404 once deleted — that merely expires\r\n    /// and is addressed to one endpoint.\r\n    ///\r\n    /// Two ops would leave a window in which the file is registered and NOT yet\r\n    /// narrowed, which is the one state this request must never produce.\r\n    // [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\n    AddScoped {\r\n        path: PathBuf,\r\n        as_name: Option<String>,\r\n        origin: Option<String>,\r\n        ttl_ms: u64,\r\n        audience: String,\r\n    },\r\n    /// Only the locally held remote-controller viewport requests this operation.\r\n    /// The audience is its established target, never a field supplied by the peer.\r\n    AddInputReference {\r\n        path: PathBuf,\r\n        receipt_id: String,\r\n        audience: String,\r\n        received_at_ms: u64,\r\n    },\r\n    Remove {\r\n        name_or_id: String,\r\n    },\r\n    List,\r\n    Reconcile,\r\n    /// Query the broker-owned listener, never the caller's bind configuration.\r\n    // [impl->REQ-WEB-URL-BOUND-PORT]\r\n    DocsStatus,\r\n    /// Start the LAN bootstrap listener (ADR-0059, releases#266). The listener\r\n    /// lives HERE, in the daemon, because the set gate anchors on the daemon's\r\n    /// OWN executable captured at t=0 and the listener must outlive the CLI\r\n    /// process that asked for it.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanBootstrap {\r\n        /// An explicit port for this start; `None` = config, env, default.\r\n        port: Option<u16>,\r\n    },\r\n    /// Tear the LAN bootstrap listener down.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanStop,\r\n    /// Read the listener's state without changing it — the source the\r\n    /// LAN-EXPOSED now-signal line reads.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanStatus,\r\n}\r\n\r\n/// The daemon's persisted outcome, or its actual refusal. An empty list is\r\n/// distinct from an unreadable registry.\r\n#[derive(Debug, Serialize, Deserialize)]\r\n#[serde(tag = \"outcome\", rename_all = \"snake_case\")]\r\npub enum ServeResult {\r\n    Entry { entry: ServedEntry },\r\n    /// An input reference resolved to no source. No diagnostic is attached.\r\n    Missing,\r\n    /// An attachment, with the SNAPSHOT's size — the sender needs it for the\r\n    /// envelope, and the daemon is the only party that ever saw the copy.\r\n    // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n    Attached { entry: ServedEntry, bytes: u64 },\r\n    Removed { entry: ServedEntry },\r\n    Listed { entries: Vec<ServedEntry> },\r\n    Refused { error: String },\r\n    // [impl->REQ-WEB-URL-BOUND-PORT]\r\n    DocsStatus { port: Option<u16> },\r\n    /// The LAN bootstrap listener is up: the URL a stranger opens and one\r\n    /// `(triple, sha256)` anchor per SERVED triple, host triple first.\r\n    /// `already_up` marks the idempotent answer — the SAME url and shas, with\r\n    /// no second listener and no re-run of the gate.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanUp {\r\n        url: String,\r\n        shas: Vec<(String, String)>,\r\n        port: u16,\r\n        already_up: bool,\r\n        /// Absent on older brokers: admission cannot infer the CLI is the binder.\r\n        // [impl->REQ-WEB-LAN-BOOTSTRAP-FIREWALL]\r\n        #[serde(default)]\r\n        binder: Option<PathBuf>,\r\n    },\r\n    /// The listener is down. `code` is `LAN_BOOTSTRAP_NOT_UP` when the stop\r\n    /// found nothing to stop — an outcome, not an error.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanStopped { code: Option<String> },\r\n    /// The set gate refused the start BY NAME, and nothing is being served.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-INTEGRITY]\r\n    LanRefused { code: String },\r\n}\r\n\r\n/// Apply a request at an explicit home. Publication completes before success\r\n/// is returned, and a failed publication cannot advance an in-memory counter.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\npub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\r\n    // [impl->REQ-WEB-URL-BOUND-PORT]\r\n    if matches!(&request, ServeRequest::DocsStatus) {\r\n        return Ok(ServeResult::DocsStatus { port: crate::docshost::bound_docs_port() });\r\n    }\r\n    // The LAN listener is PROCESS state, not registry state: it lives in this\r\n    // daemon's memory and nothing about it is written to disk. It is answered\r\n    // BEFORE the registry writer lock so a bootstrap can never be serialized\r\n    // behind — or worse, blocked by — an unrelated registry mutation.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    if let Some(result) = lan_request(home, &request) {\r\n        return Ok(result);\r\n    }\r\n    let path = spt_store::perch::serving_registry_file_in(home);\r\n    let _writer = spt_store::serving::lock_registry_at(&path)?;\r\n    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\r\n        if matches!(&request, ServeRequest::AddInputReference { .. }) {\r\n            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\r\n        } else {\r\n            error\r\n        }\r\n    })?;\r\n    match request {\r\n        ServeRequest::Add { path: source, as_name, origin } => {\r\n            let entry = registry.add_reference(\r\n                &source,\r\n                as_name.as_deref(),\r\n                origin.as_deref(),\r\n                now_ms,\r\n            )?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Entry { entry })\r\n        }\r\n        // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n        ServeRequest::Attach { path: source, as_name, origin, ttl_ms, audience } => {\r\n            let (snapshot, bytes) = spt_store::serving::write_snapshot_in(home, &source)?;\r\n            // The requested name defaults to the SOURCE's basename; the registry\r\n            // disambiguates a repeat, so two sends of one file get two stable\r\n            // distinct URLs and neither link ever changes meaning.\r\n            let requested = match as_name {\r\n                Some(name) => name,\r\n                None => source\r\n                    .file_name()\r\n                    .and_then(|name| name.to_str())\r\n                    .map(str::to_owned)\r\n                    .ok_or_else(|| {\r\n                        io::Error::new(\r\n                            io::ErrorKind::InvalidInput,\r\n                            \"SERVE_ATTACH_NAME: the attachment path has no usable file name\",\r\n                        )\r\n                    })?,\r\n            };\r\n            let entry = match registry.add_attachment(\r\n                &snapshot,\r\n                &requested,\r\n                origin.as_deref(),\r\n                ttl_ms,\r\n                audience.as_deref(),\r\n                now_ms,\r\n            ) {\r\n                Ok(entry) => entry,\r\n                Err(error) => {\r\n                    // A refused registration must not leave bytes nothing points\r\n                    // at: the reaper only ever deletes what an ENTRY named, so an\r\n                    // orphan snapshot would live until the store is wiped by hand.\r\n                    let _ = std::fs::remove_file(&snapshot);\r\n                    return Err(error);\r\n                }\r\n            };\r\n            if let Err(error) = registry.save_at(&path) {\r\n                // Same reasoning as the refused registration above: an entry that\r\n                // did not persist names nothing, so its bytes must not survive.\r\n                let _ = std::fs::remove_file(&snapshot);\r\n                return Err(error);\r\n            }\r\n            Ok(ServeResult::Attached { entry, bytes })\r\n        }\r\n        // [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\n        ServeRequest::AddScoped { path: source, as_name, origin, ttl_ms, audience } => {\r\n            let entry = registry.add_reference(&source, as_name.as_deref(), origin.as_deref(), now_ms)?;\r\n            let scoped = registry\r\n                .scope_entry(&entry.id, Some(ttl_ms), Some(&audience), origin.as_deref())\r\n                .ok_or_else(|| io::Error::other(\"SERVE_SCOPE_LOST: the entry vanished between add and scope\"))?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Entry { entry: scoped })\r\n        }\r\n        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\r\n            // Resolve the submitter's home HERE, never on the receiving node.\r\n            let source = if let Some(relative) = source.to_str().and_then(|p| p.strip_prefix(\"~/\")) {\r\n                std::env::home_dir()\r\n                    .ok_or_else(|| io::Error::other(\"INPUT_PATH_HOME_UNAVAILABLE\"))?\r\n                    .join(relative)\r\n            } else {\r\n                source\r\n            };\r\n            let previous_id = registry.entries().find(|entry| entry.path == source)\r\n                .map(|entry| entry.id.clone());\r\n            match registry.add_input_reference(&source, &receipt_id, &audience, received_at_ms, now_ms)? {\r\n                None => Ok(ServeResult::Missing),\r\n                Some(entry) => {\r\n                    if previous_id.as_deref() != Some(entry.id.as_str()) {\r\n                        registry.save_at(&path)\r\n                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\r\n                    }\r\n                    Ok(ServeResult::Entry { entry })\r\n                }\r\n            }\r\n        }\r\n        ServeRequest::Remove { name_or_id } => {\r\n            let entry = registry.remove(&name_or_id).ok_or_else(|| {\r\n                io::Error::new(io::ErrorKind::NotFound, format!(\"SERVE_NOT_FOUND: {name_or_id}\"))\r\n            })?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Removed { entry })\r\n        }\r\n        ServeRequest::List => Ok(ServeResult::Listed {\r\n            entries: registry.entries().cloned().collect(),\r\n        }),\r\n        // Answered above, before the writer lock.\r\n        ServeRequest::DocsStatus => unreachable!(\"docs status is answered before the registry writer\"),\r\n        ServeRequest::LanBootstrap { .. } | ServeRequest::LanStop | ServeRequest::LanStatus => {\r\n            unreachable!(\"LAN requests are answered before the registry writer\")\r\n        }\r\n        ServeRequest::Reconcile => {\r\n            reconcile_registry(home, &mut registry, now_ms)?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Listed {\r\n                entries: registry.entries().cloned().collect(),\r\n            })\r\n        }\r\n    }\r\n}\r\n\r\n/// Answer the three LAN requests, or `None` for a registry request.\r\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\nfn lan_request(home: &Path, request: &ServeRequest) -> Option<ServeResult> {\r\n    match request {\r\n        ServeRequest::LanBootstrap { port } => Some(lan_bootstrap(home, *port)),\r\n        ServeRequest::LanStop => Some(match crate::lanhost::stop() {\r\n            Ok(true) => ServeResult::LanStopped { code: None },\r\n            Ok(false) => ServeResult::LanStopped {\r\n                code: Some(crate::lanhost::NOT_UP.to_string()),\r\n            },\r\n            Err(error) => ServeResult::Refused { error },\r\n        }),\r\n        ServeRequest::LanStatus => Some(match crate::lanhost::status() {\r\n            Some(up) => ServeResult::LanUp {\r\n                url: up.url,\r\n                shas: up.shas,\r\n                port: up.port,\r\n                already_up: true,\r\n                binder: Some(up.binder),\r\n            },\r\n            None => ServeResult::LanStopped {\r\n                code: Some(crate::lanhost::NOT_UP.to_string()),\r\n            },\r\n        }),\r\n        _ => None,\r\n    }\r\n}\r\n\r\n/// Start the LAN bootstrap listener at `home`, resolving the port the same way\r\n/// the daemon resolves every advisory port (explicit > env > config > default,\r\n/// with the rig's ephemeral posture on top).\r\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\nfn lan_bootstrap(home: &Path, port: Option<u16>) -> ServeResult {\r\n    let resolved = port.unwrap_or_else(|| {\r\n        crate::lanhost::resolve_daemon_lan_port(\r\n            crate::config::DaemonConfig::load().lan_bootstrap_port,\r\n            std::env::var(crate::lanhost::LAN_BOOTSTRAP_PORT_ENV).ok().as_deref(),\r\n            std::env::var_os(crate::docshost::TEST_EPHEMERAL_ADVISORY_PORTS_ENV)\r\n                .is_some_and(|value| value == \"1\"),\r\n        )\r\n    });\r\n    match crate::lanhost::start_at(home, resolved) {\r\n        Ok(up) => ServeResult::LanUp {\r\n            url: up.url,\r\n            shas: up.shas,\r\n            port: up.port,\r\n            already_up: up.already_up,\r\n            binder: Some(up.binder),\r\n        },\r\n        Err(code) => ServeResult::LanRefused { code },\r\n    }\r\n}\r\n\r\n/// Reconcile active base adapters at daemon startup under the same serialized\r\n/// writer as explicit registrations. Never call this from a client process.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub fn reconcile_at(home: &Path, now_ms: u64) -> io::Result<()> {\r\n    apply_at(home, ServeRequest::Reconcile, now_ms).map(|_| ())\r\n}\r\n\r\nfn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\r\n    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\r\n    let retired: Vec<_> = registry\r\n        .entries()\r\n        .filter(|entry| entry.adapter.as_ref().is_some_and(|adapter| {\r\n            !registered.iter().any(|(record, _)| &record.name == adapter)\r\n        }))\r\n        .map(|entry| entry.id.clone())\r\n        .collect();\r\n    for id in retired {\r\n        registry.remove(&id);\r\n    }\r\n    // Base registrations, not profile options: every adapter gets exactly one\r\n    // root, and an optional alias names that entry rather than another source.\r\n    for (record, manifest) in registered {\r\n        let root = spt_store::perch::create_adapter_web_dir_at(&home.join(\"adapters\"), &record.name)?;\r\n        registry.add_adapter(&root, &record.name, manifest.adapter.web_short_path.as_deref(), now_ms)?;\r\n    }\r\n    Ok(())\r\n}\r\n\r\n/// Ask the broker's single writer to reconcile after an adapter mutation.\r\n/// Transport timeouts remain unconfirmed outcomes, never client-side writes.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub fn reconcile(name: &str) -> io::Result<()> {\r\n    match call(name, ServeRequest::Reconcile)? {\r\n        ServeResult::Listed { .. } => Ok(()),\r\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\r\n        _ => Err(io::Error::new(io::ErrorKind::InvalidData, \"SERVE_UNEXPECTED_REPLY: reconcile\")),\r\n    }\r\n}\r\n\r\n/// Discover a running broker's listener. An old broker or unavailable listener\r\n/// is an error, not permission to guess the configured port.\r\n// [impl->REQ-WEB-URL-BOUND-PORT]\r\npub fn docs_port(name: &str) -> io::Result<u16> {\r\n    port_from_status(call(name, ServeRequest::DocsStatus)?)\r\n}\r\n\r\n/// The whole port decision over an ALREADY-RECEIVED reply, split out of\r\n/// [`docs_port`] so a bound port, an unavailable listener, a refusal and an old\r\n/// broker's unrelated reply can each be reached without a running daemon (doyle\r\n/// ruled the split 2026-09-11, the same decision-inside-IO shape hertz measured\r\n/// in `bootstrap_firewall::windows::verify`). Behaviour is unchanged: this is\r\n/// the match `docs_port` used to inline.\r\n// [impl->REQ-WEB-URL-BOUND-PORT]\r\npub fn port_from_status(result: ServeResult) -> io::Result<u16> {\r\n    match result {\r\n        ServeResult::DocsStatus { port: Some(port) } if port != 0 => Ok(port),\r\n        ServeResult::DocsStatus { .. } => Err(io::Error::other(\r\n            \"DOCS_LISTENER_UNAVAILABLE: the running daemon has no bound docs listener\",\r\n        )),\r\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\r\n        _ => Err(io::Error::other(\r\n            \"DOCS_LISTENER_UNREPORTED: the running daemon did not report its bound docs port\",\r\n        )),\r\n    }\r\n}\r\n\r\n/// Dispatch only after the local control transport's version handshake.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub(crate) fn handle(payload: serde_json::Value) -> Envelope {\r\n    let result = serde_json::from_value(payload)\r\n        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))\r\n        .and_then(|request| {\r\n            let now_ms = std::time::SystemTime::now()\r\n                .duration_since(std::time::UNIX_EPOCH)\r","truncated":true,"truncatedBy":"lines","totalLines":851,"totalBytes":17820,"outputLines":393,"outputBytes":17820,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":851,"displayContent":{"text":"//! Local serving-registry mutations. The daemon is the only writer; CLI\r\n//! clients use the existing local control transport and never save the store.\r\n\r\nuse std::io;\r\nuse std::path::{Path, PathBuf};\r\n\r\nuse serde::{Deserialize, Serialize};\r\nuse spt_store::serving::{ServedEntry, ServingRegistry};\r\n\r\nuse crate::codec::{read_frame, write_frame};\r\nuse crate::frame::{Envelope, Role};\r\nuse crate::transport::{send_hello, LocalSocketTransport};\r\n\r\npub const KIND_SERVE_REQUEST: &str = \"serve_request\";\r\npub const KIND_SERVE_RESULT: &str = \"serve_result\";\r\n\r\n/// An explicit local registry operation; no HTTP request can mutate exposure.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\n#[serde(tag = \"op\", rename_all = \"snake_case\")]\r\npub enum ServeRequest {\r\n    Add {\r\n        path: PathBuf,\r\n        as_name: Option<String>,\r\n        origin: Option<String>,\r\n    },\r\n    /// Snapshot `path` into the node's attachment store and serve the COPY\r\n    /// (ADR-0058). The snapshot is taken HERE rather than by the caller for the\r\n    /// reason the registry has one writer at all: the bytes and the entry that\r\n    /// names them are created under the same lock, so there is no window in\r\n    /// which one exists without the other.\r\n    // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n    Attach {\r\n        path: PathBuf,\r\n        as_name: Option<String>,\r\n        origin: Option<String>,\r\n        ttl_ms: u64,\r\n        audience: Option<String>,\r\n    },\r\n    /// Register `path` as an ordinary REFERENCE and narrow it in the same\r\n    /// locked pass (ADR-0058 Amendment 1): the FILE_ACCESS_HELPER's entry is a\r\n    /// live reference — edits visible, 404 once deleted — that merely expires\r\n    /// and is addressed to one endpoint.\r\n    ///\r\n    /// Two ops would leave a window in which the file is registered and NOT yet\r\n    /// narrowed, which is the one state this request must never produce.\r\n    // [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\n    AddScoped {\r\n        path: PathBuf,\r\n        as_name: Option<String>,\r\n        origin: Option<String>,\r\n        ttl_ms: u64,\r\n        audience: String,\r\n    },\r\n    /// Only the locally held remote-controller viewport requests this operation.\r\n    /// The audience is its established target, never a field supplied by the peer.\r\n    AddInputReference {\r\n        path: PathBuf,\r\n        receipt_id: String,\r\n        audience: String,\r\n        received_at_ms: u64,\r\n    },\r\n    Remove {\r\n        name_or_id: String,\r\n    },\r\n    List,\r\n    Reconcile,\r\n    /// Query the broker-owned listener, never the caller's bind configuration.\r\n    // [impl->REQ-WEB-URL-BOUND-PORT]\r\n    DocsStatus,\r\n    /// Start the LAN bootstrap listener (ADR-0059, releases#266). The listener\r\n    /// lives HERE, in the daemon, because the set gate anchors on the daemon's\r\n    /// OWN executable captured at t=0 and the listener must outlive the CLI\r\n    /// process that asked for it.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanBootstrap {\r\n        /// An explicit port for this start; `None` = config, env, default.\r\n        port: Option<u16>,\r\n    },\r\n    /// Tear the LAN bootstrap listener down.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanStop,\r\n    /// Read the listener's state without changing it — the source the\r\n    /// LAN-EXPOSED now-signal line reads.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanStatus,\r\n}\r\n\r\n/// The daemon's persisted outcome, or its actual refusal. An empty list is\r\n/// distinct from an unreadable registry.\r\n#[derive(Debug, Serialize, Deserialize)]\r\n#[serde(tag = \"outcome\", rename_all = \"snake_case\")]\r\npub enum ServeResult {\r\n    Entry { entry: ServedEntry },\r\n    /// An input reference resolved to no source. No diagnostic is attached.\r\n    Missing,\r\n    /// An attachment, with the SNAPSHOT's size — the sender needs it for the\r\n    /// envelope, and the daemon is the only party that ever saw the copy.\r\n    // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n    Attached { entry: ServedEntry, bytes: u64 },\r\n    Removed { entry: ServedEntry },\r\n    Listed { entries: Vec<ServedEntry> },\r\n    Refused { error: String },\r\n    // [impl->REQ-WEB-URL-BOUND-PORT]\r\n    DocsStatus { port: Option<u16> },\r\n    /// The LAN bootstrap listener is up: the URL a stranger opens and one\r\n    /// `(triple, sha256)` anchor per SERVED triple, host triple first.\r\n    /// `already_up` marks the idempotent answer — the SAME url and shas, with\r\n    /// no second listener and no re-run of the gate.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanUp {\r\n        url: String,\r\n        shas: Vec<(String, String)>,\r\n        port: u16,\r\n        already_up: bool,\r\n        /// Absent on older brokers: admission cannot infer the CLI is the binder.\r\n        // [impl->REQ-WEB-LAN-BOOTSTRAP-FIREWALL]\r\n        #[serde(default)]\r\n        binder: Option<PathBuf>,\r\n    },\r\n    /// The listener is down. `code` is `LAN_BOOTSTRAP_NOT_UP` when the stop\r\n    /// found nothing to stop — an outcome, not an error.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    LanStopped { code: Option<String> },\r\n    /// The set gate refused the start BY NAME, and nothing is being served.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-INTEGRITY]\r\n    LanRefused { code: String },\r\n}\r\n\r\n/// Apply a request at an explicit home. Publication completes before success\r\n/// is returned, and a failed publication cannot advance an in-memory counter.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\npub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\r\n    // [impl->REQ-WEB-URL-BOUND-PORT]\r\n    if matches!(&request, ServeRequest::DocsStatus) {\r\n        return Ok(ServeResult::DocsStatus { port: crate::docshost::bound_docs_port() });\r\n    }\r\n    // The LAN listener is PROCESS state, not registry state: it lives in this\r\n    // daemon's memory and nothing about it is written to disk. It is answered\r\n    // BEFORE the registry writer lock so a bootstrap can never be serialized\r\n    // behind — or worse, blocked by — an unrelated registry mutation.\r\n    // [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    if let Some(result) = lan_request(home, &request) {\r\n        return Ok(result);\r\n    }\r\n    let path = spt_store::perch::serving_registry_file_in(home);\r\n    let _writer = spt_store::serving::lock_registry_at(&path)?;\r\n    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\r\n        if matches!(&request, ServeRequest::AddInputReference { .. }) {\r\n            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\r\n        } else {\r\n            error\r\n        }\r\n    })?;\r\n    match request {\r\n        ServeRequest::Add { path: source, as_name, origin } => {\r\n            let entry = registry.add_reference(\r\n                &source,\r\n                as_name.as_deref(),\r\n                origin.as_deref(),\r\n                now_ms,\r\n            )?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Entry { entry })\r\n        }\r\n        // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n        ServeRequest::Attach { path: source, as_name, origin, ttl_ms, audience } => {\r\n            let (snapshot, bytes) = spt_store::serving::write_snapshot_in(home, &source)?;\r\n            // The requested name defaults to the SOURCE's basename; the registry\r\n            // disambiguates a repeat, so two sends of one file get two stable\r\n            // distinct URLs and neither link ever changes meaning.\r\n            let requested = match as_name {\r\n                Some(name) => name,\r\n                None => source\r\n                    .file_name()\r\n                    .and_then(|name| name.to_str())\r\n                    .map(str::to_owned)\r\n                    .ok_or_else(|| {\r\n                        io::Error::new(\r\n                            io::ErrorKind::InvalidInput,\r\n                            \"SERVE_ATTACH_NAME: the attachment path has no usable file name\",\r\n                        )\r\n                    })?,\r\n            };\r\n            let entry = match registry.add_attachment(\r\n                &snapshot,\r\n                &requested,\r\n                origin.as_deref(),\r\n                ttl_ms,\r\n                audience.as_deref(),\r\n                now_ms,\r\n            ) {\r\n                Ok(entry) => entry,\r\n                Err(error) => {\r\n                    // A refused registration must not leave bytes nothing points\r\n                    // at: the reaper only ever deletes what an ENTRY named, so an\r\n                    // orphan snapshot would live until the store is wiped by hand.\r\n                    let _ = std::fs::remove_file(&snapshot);\r\n                    return Err(error);\r\n                }\r\n            };\r\n            if let Err(error) = registry.save_at(&path) {\r\n                // Same reasoning as the refused registration above: an entry that\r\n                // did not persist names nothing, so its bytes must not survive.\r\n                let _ = std::fs::remove_file(&snapshot);\r\n                return Err(error);\r\n            }\r\n            Ok(ServeResult::Attached { entry, bytes })\r\n        }\r\n        // [impl->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\n        ServeRequest::AddScoped { path: source, as_name, origin, ttl_ms, audience } => {\r\n            let entry = registry.add_reference(&source, as_name.as_deref(), origin.as_deref(), now_ms)?;\r\n            let scoped = registry\r\n                .scope_entry(&entry.id, Some(ttl_ms), Some(&audience), origin.as_deref())\r\n                .ok_or_else(|| io::Error::other(\"SERVE_SCOPE_LOST: the entry vanished between add and scope\"))?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Entry { entry: scoped })\r\n        }\r\n        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\r\n            // Resolve the submitter's home HERE, never on the receiving node.\r\n            let source = if let Some(relative) = source.to_str().and_then(|p| p.strip_prefix(\"~/\")) {\r\n                std::env::home_dir()\r\n                    .ok_or_else(|| io::Error::other(\"INPUT_PATH_HOME_UNAVAILABLE\"))?\r\n                    .join(relative)\r\n            } else {\r\n                source\r\n            };\r\n            let previous_id = registry.entries().find(|entry| entry.path == source)\r\n                .map(|entry| entry.id.clone());\r\n            match registry.add_input_reference(&source, &receipt_id, &audience, received_at_ms, now_ms)? {\r\n                None => Ok(ServeResult::Missing),\r\n                Some(entry) => {\r\n                    if previous_id.as_deref() != Some(entry.id.as_str()) {\r\n                        registry.save_at(&path)\r\n                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\r\n                    }\r\n                    Ok(ServeResult::Entry { entry })\r\n                }\r\n            }\r\n        }\r\n        ServeRequest::Remove { name_or_id } => {\r\n            let entry = registry.remove(&name_or_id).ok_or_else(|| {\r\n                io::Error::new(io::ErrorKind::NotFound, format!(\"SERVE_NOT_FOUND: {name_or_id}\"))\r\n            })?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Removed { entry })\r\n        }\r\n        ServeRequest::List => Ok(ServeResult::Listed {\r\n            entries: registry.entries().cloned().collect(),\r\n        }),\r\n        // Answered above, before the writer lock.\r\n        ServeRequest::DocsStatus => unreachable!(\"docs status is answered before the registry writer\"),\r\n        ServeRequest::LanBootstrap { .. } | ServeRequest::LanStop | ServeRequest::LanStatus => {\r\n            unreachable!(\"LAN requests are answered before the registry writer\")\r\n        }\r\n        ServeRequest::Reconcile => {\r\n            reconcile_registry(home, &mut registry, now_ms)?;\r\n            registry.save_at(&path)?;\r\n            Ok(ServeResult::Listed {\r\n                entries: registry.entries().cloned().collect(),\r\n            })\r\n        }\r\n    }\r\n}\r\n\r\n/// Answer the three LAN requests, or `None` for a registry request.\r\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\nfn lan_request(home: &Path, request: &ServeRequest) -> Option<ServeResult> {\r\n    match request {\r\n        ServeRequest::LanBootstrap { port } => Some(lan_bootstrap(home, *port)),\r\n        ServeRequest::LanStop => Some(match crate::lanhost::stop() {\r\n            Ok(true) => ServeResult::LanStopped { code: None },\r\n            Ok(false) => ServeResult::LanStopped {\r\n                code: Some(crate::lanhost::NOT_UP.to_string()),\r\n            },\r\n            Err(error) => ServeResult::Refused { error },\r\n        }),\r\n        ServeRequest::LanStatus => Some(match crate::lanhost::status() {\r\n            Some(up) => ServeResult::LanUp {\r\n                url: up.url,\r\n                shas: up.shas,\r\n                port: up.port,\r\n                already_up: true,\r\n                binder: Some(up.binder),\r\n            },\r\n            None => ServeResult::LanStopped {\r\n                code: Some(crate::lanhost::NOT_UP.to_string()),\r\n            },\r\n        }),\r\n        _ => None,\r\n    }\r\n}\r\n\r\n/// Start the LAN bootstrap listener at `home`, resolving the port the same way\r\n/// the daemon resolves every advisory port (explicit > env > config > default,\r\n/// with the rig's ephemeral posture on top).\r\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\nfn lan_bootstrap(home: &Path, port: Option<u16>) -> ServeResult {\r\n    let resolved = port.unwrap_or_else(|| {\r\n        crate::lanhost::resolve_daemon_lan_port(\r\n            crate::config::DaemonConfig::load().lan_bootstrap_port,\r\n            std::env::var(crate::lanhost::LAN_BOOTSTRAP_PORT_ENV).ok().as_deref(),\r\n            std::env::var_os(crate::docshost::TEST_EPHEMERAL_ADVISORY_PORTS_ENV)\r\n                .is_some_and(|value| value == \"1\"),\r\n        )\r\n    });\r\n    match crate::lanhost::start_at(home, resolved) {\r\n        Ok(up) => ServeResult::LanUp {\r\n            url: up.url,\r\n            shas: up.shas,\r\n            port: up.port,\r\n            already_up: up.already_up,\r\n            binder: Some(up.binder),\r\n        },\r\n        Err(code) => ServeResult::LanRefused { code },\r\n    }\r\n}\r\n\r\n/// Reconcile active base adapters at daemon startup under the same serialized\r\n/// writer as explicit registrations. Never call this from a client process.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub fn reconcile_at(home: &Path, now_ms: u64) -> io::Result<()> {\r\n    apply_at(home, ServeRequest::Reconcile, now_ms).map(|_| ())\r\n}\r\n\r\nfn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\r\n    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\r\n    let retired: Vec<_> = registry\r\n        .entries()\r\n        .filter(|entry| entry.adapter.as_ref().is_some_and(|adapter| {\r\n            !registered.iter().any(|(record, _)| &record.name == adapter)\r\n        }))\r\n        .map(|entry| entry.id.clone())\r\n        .collect();\r\n    for id in retired {\r\n        registry.remove(&id);\r\n    }\r\n    // Base registrations, not profile options: every adapter gets exactly one\r\n    // root, and an optional alias names that entry rather than another source.\r\n    for (record, manifest) in registered {\r\n        let root = spt_store::perch::create_adapter_web_dir_at(&home.join(\"adapters\"), &record.name)?;\r\n        registry.add_adapter(&root, &record.name, manifest.adapter.web_short_path.as_deref(), now_ms)?;\r\n    }\r\n    Ok(())\r\n}\r\n\r\n/// Ask the broker's single writer to reconcile after an adapter mutation.\r\n/// Transport timeouts remain unconfirmed outcomes, never client-side writes.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub fn reconcile(name: &str) -> io::Result<()> {\r\n    match call(name, ServeRequest::Reconcile)? {\r\n        ServeResult::Listed { .. } => Ok(()),\r\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\r\n        _ => Err(io::Error::new(io::ErrorKind::InvalidData, \"SERVE_UNEXPECTED_REPLY: reconcile\")),\r\n    }\r\n}\r\n\r\n/// Discover a running broker's listener. An old broker or unavailable listener\r\n/// is an error, not permission to guess the configured port.\r\n// [impl->REQ-WEB-URL-BOUND-PORT]\r\npub fn docs_port(name: &str) -> io::Result<u16> {\r\n    port_from_status(call(name, ServeRequest::DocsStatus)?)\r\n}\r\n\r\n/// The whole port decision over an ALREADY-RECEIVED reply, split out of\r\n/// [`docs_port`] so a bound port, an unavailable listener, a refusal and an old\r\n/// broker's unrelated reply can each be reached without a running daemon (doyle\r\n/// ruled the split 2026-09-11, the same decision-inside-IO shape hertz measured\r\n/// in `bootstrap_firewall::windows::verify`). Behaviour is unchanged: this is\r\n/// the match `docs_port` used to inline.\r\n// [impl->REQ-WEB-URL-BOUND-PORT]\r\npub fn port_from_status(result: ServeResult) -> io::Result<u16> {\r\n    match result {\r\n        ServeResult::DocsStatus { port: Some(port) } if port != 0 => Ok(port),\r\n        ServeResult::DocsStatus { .. } => Err(io::Error::other(\r\n            \"DOCS_LISTENER_UNAVAILABLE: the running daemon has no bound docs listener\",\r\n        )),\r\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\r\n        _ => Err(io::Error::other(\r\n            \"DOCS_LISTENER_UNREPORTED: the running daemon did not report its bound docs port\",\r\n        )),\r\n    }\r\n}\r\n\r\n/// Dispatch only after the local control transport's version handshake.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub(crate) fn handle(payload: serde_json::Value) -> Envelope {\r\n    let result = serde_json::from_value(payload)\r\n        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))\r\n        .and_then(|request| {\r\n            let now_ms = std::time::SystemTime::now()\r\n                .duration_since(std::time::UNIX_EPOCH)\r\n                .map_err(io::Error::other)?\n                .as_millis()\n                .try_into()\n                .map_err(io::Error::other)?;\n…\n        })\n        .unwrap_or_else(|e| ServeResult::Refused { error: e.to_string() });\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,null,399,400,null,405]},"fileSize":40844,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\src\\servehost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":851,"totalBytes":17820,"outputLines":393,"outputBytes":17820,"shownRange":{"start":1,"end":393},"nextOffset":394}}},"isError":false,"timestamp":1789522902764}}
{"type":"message","id":"63170dae","parentId":"0387d56b","timestamp":"2026-09-16T01:41:42.770Z","message":{"role":"toolResult","toolCallId":"call_Kth1ScXOJJCMslNe0uiXsbIc|fc_0cdf94810c888815016aa9f3d6a92087d18e489e4bed00b22e","toolName":"read","content":[{"type":"text","text":"diff --git a/crates/spt-store/src/serving.rs b/crates/spt-store/src/serving.rs\nindex 52317a20..a9e7c147 100644\n--- a/crates/spt-store/src/serving.rs\n+++ b/crates/spt-store/src/serving.rs\n@@ -3,7 +3,8 @@\n //! Schema 1 stores live entries, per-family suffix counters, and the absolute\n //! path and kind each name last identified. Removal changes only live entries;\n //! the same path and kind may reclaim their names (ADR-0057 Amendment 2).\n-//! The daemon is the single writer; corruption is never an empty store.\n+//! Broker mutations and brain reaping share a cross-process writer guard;\n+//! corruption is never an empty store.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n \n use std::collections::{BTreeMap, BTreeSet};\n@@ -132,6 +133,36 @@ pub struct ServingRegistry {\n     name_owners: BTreeMap<String, NameOwner>,\n }\n \n+/// Serialize a registry transaction across threads and processes.\n+///\n+/// Acquire BEFORE loading and keep the returned file alive through mutation,\n+/// snapshot cleanup/rollback, and explicit publication. This does not load or\n+/// save anything itself, so no-op transactions need not publish a registry.\n+///\n+/// The stable sibling `registry.lock` is never renamed or removed: locking the\n+/// registry itself would stop coordinating writers after atomic replacement.\n+/// Closing the returned file (including process exit) releases the lock. Do not\n+/// acquire it again while holding it; independently opened handles contend.\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+// [impl->REQ-WEB-SERVING-REGISTRY]\n+// [impl->REQ-WEB-ATTACHMENT-PULL]\n+pub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n+    use fs2::FileExt;\n+\n+    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\n+        fs::create_dir_all(parent)?;\n+    }\n+    let mut lock_path = registry_path.as_os_str().to_os_string();\n+    lock_path.push(\".lock\");\n+    let lock = fs::OpenOptions::new()\n+        .create(true)\n+        .write(true)\n+        .truncate(false)\n+        .open(PathBuf::from(lock_path))?;\n+    lock.lock_exclusive()?;\n+    Ok(lock)\n+}\n+\n impl ServingRegistry {\n     /// Missing is empty. Unreadable, malformed, or inconsistent history is an\n     /// error: never recover by resetting the allocator and reusing stale URLs.\n@@ -161,7 +192,8 @@ impl ServingRegistry {\n     }\n \n     /// Publish a durable old-or-new snapshot, creating its parent directory.\n-    /// The caller must serialize mutations and retain the previous state on error.\n+    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\n+    /// through this publication; locking only this save cannot repair stale state.\n     pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\n         self.validate()?;\n         let bytes = serde_json::to_vec_pretty(self).map_err(invalid_data)?;"}],"details":{"resolvedPath":"C:\\Users\\decid\\.omp\\agent\\sessions\\-Documents-projects-spt-core\\2026-09-13T10-23-45-921Z_01a09a4b-5f01-7000-9118-23ce74a49636\\494.bash-original.log","contentType":"text/plain","totalLines":360,"displayContent":{"text":"diff --git a/crates/spt-store/src/serving.rs b/crates/spt-store/src/serving.rs\nindex 52317a20..a9e7c147 100644\n--- a/crates/spt-store/src/serving.rs\n+++ b/crates/spt-store/src/serving.rs\n@@ -3,7 +3,8 @@\n //! Schema 1 stores live entries, per-family suffix counters, and the absolute\n //! path and kind each name last identified. Removal changes only live entries;\n //! the same path and kind may reclaim their names (ADR-0057 Amendment 2).\n-//! The daemon is the single writer; corruption is never an empty store.\n+//! Broker mutations and brain reaping share a cross-process writer guard;\n+//! corruption is never an empty store.\n // [impl->REQ-WEB-SERVING-REGISTRY]\n \n use std::collections::{BTreeMap, BTreeSet};\n@@ -132,6 +133,36 @@ pub struct ServingRegistry {\n     name_owners: BTreeMap<String, NameOwner>,\n }\n \n+/// Serialize a registry transaction across threads and processes.\n+///\n+/// Acquire BEFORE loading and keep the returned file alive through mutation,\n+/// snapshot cleanup/rollback, and explicit publication. This does not load or\n+/// save anything itself, so no-op transactions need not publish a registry.\n+///\n+/// The stable sibling `registry.lock` is never renamed or removed: locking the\n+/// registry itself would stop coordinating writers after atomic replacement.\n+/// Closing the returned file (including process exit) releases the lock. Do not\n+/// acquire it again while holding it; independently opened handles contend.\n+// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+// [impl->REQ-WEB-SERVING-REGISTRY]\n+// [impl->REQ-WEB-ATTACHMENT-PULL]\n+pub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n+    use fs2::FileExt;\n+\n+    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\n+        fs::create_dir_all(parent)?;\n+    }\n+    let mut lock_path = registry_path.as_os_str().to_os_string();\n+    lock_path.push(\".lock\");\n+    let lock = fs::OpenOptions::new()\n+        .create(true)\n+        .write(true)\n+        .truncate(false)\n+        .open(PathBuf::from(lock_path))?;\n+    lock.lock_exclusive()?;\n+    Ok(lock)\n+}\n+\n impl ServingRegistry {\n     /// Missing is empty. Unreadable, malformed, or inconsistent history is an\n     /// error: never recover by resetting the allocator and reusing stale URLs.\n@@ -161,7 +192,8 @@ impl ServingRegistry {\n     }\n \n     /// Publish a durable old-or-new snapshot, creating its parent directory.\n-    /// The caller must serialize mutations and retain the previous state on error.\n+    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\n+    /// through this publication; locking only this save cannot repair stale state.\n     pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\n         self.validate()?;\n         let bytes = serde_json::to_vec_pretty(self).map_err(invalid_data)?;","startLine":300,"lineNumbers":[300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360]},"meta":{"source":{"type":"internal","value":"artifact://494"}}},"isError":false,"timestamp":1789522902770}}
{"type":"message","id":"07466f1b","parentId":"63170dae","timestamp":"2026-09-16T01:41:42.770Z","message":{"role":"toolResult","toolCallId":"call_woxh4DstF0F2A7K21HPrCq9H|fc_0cdf94810c888815016aa9f3d6a93087d1a386fd2fa1cd57cc","toolName":"read","content":[{"type":"text","text":"+        let next = registry.add_reference(&third_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"new source gets next suffix\");\n+        assert_eq!(next.served_name, \"report~3.txt\", \"neither expired nor retired names may be reassigned\");\n+        registry.save_at(&registry_path).expect(\"persist continued history\");\n+    }\n+    let published = ServingRegistry::load_at(&registry_path).expect(\"read continued assignments\");\n+    assert_eq!(published.get(\"report~1.txt\").expect(\"old owner\").path, retired_path);\n+    assert_eq!(published.get(\"report~2.txt\").expect(\"recent owner\").path, registered_path);\n+    assert_eq!(published.get(\"report~3.txt\").expect(\"new owner\").path, third_path);\n+}\n+\n+// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n+#[test]\n+fn loaded_reaper_serializes_registration_without_resurrecting_expiry() {\n+    overlap(\"reap\", false);\n+}\n+\n+// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n+#[test]\n+fn later_reap_preserves_a_registration_published_while_it_was_waiting() {\n+    overlap(\"register\", false);\n+}\n+\n+// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+#[test]\n+fn abrupt_holder_exit_releases_the_sentinel_for_the_waiting_reaper() {\n+    overlap(\"register\", true);\n+}\ndiff --git a/crates/spt/tests/webserve_attachment_e2e.rs b/crates/spt/tests/webserve_attachment_e2e.rs\nindex c1516a82..8c235cb7 100644\n--- a/crates/spt/tests/webserve_attachment_e2e.rs\n+++ b/crates/spt/tests/webserve_attachment_e2e.rs\n@@ -661,43 +661,30 @@ registry: {}\",\n     //\n     // `scope_entry` also sets `audience` unconditionally from its argument, so\n     // passing None CLEARS any audience. reap-me.md has none, which makes that a\n-    // no-op HERE — but do not lift this loop onto an audienced entry without\n+    // no-op HERE — but do not lift this mutation onto an audienced entry without\n     // passing its audience back in, or the scope goes with the ttl.\n     //\n-    // The loop re-reads because the daemon is the registry's other writer. Be\n-    // precise about what that buys: it detects the entry being ABSENT, not a\n-    // clobber — a daemon save landing between this load and this save would be\n-    // overwritten by this snapshot and nothing here would notice. That hazard is\n-    // idle in this arm by arithmetic rather than by vigilance: every other live\n-    // entry carries 30 d or 3600 s, so the only concurrent writer has nothing to\n-    // retire and nothing to write, and this snapshot equals its state.\n-    let mut retired = false;\n-    for attempt in 0..5 {\n+    // This fixture is another PROCESS writing the live daemon's registry.\n+    // Hold its shared guard across the fresh read and publication, not a retry\n+    // after a stale write. An already-retired entry needs no further mutation.\n+    // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+    {\n+        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n         let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n         if registry\n             .scope_entry(\"reap-me.md\", Some(1), None, None)\n             .is_some()\n         {\n             registry.save_at(&registry_path).unwrap();\n-            retired = true;\n-            break;\n-        }\n-        // Gone already means the daemon retired it between our read and now,\n-        // which is the outcome this loop is trying to cause — not a failure.\n-        if snapshot_of(\"reap-me.md\").is_none() {\n-            retired = true;\n-            break;\n         }\n-        assert!(attempt < 4, \"could not backdate the doomed entry's ttl\");\n     }\n-    assert!(retired, \"the doomed entry was given an already-expired lifetime\");\n \n-    // A reference entry, already expired, planted for the guard. Re-read after\n-    // planting: the daemon is the registry's other writer and its own tick could\n-    // land between this load and this save.\n+    // Plant the already-expired reference under the same transaction guard.\n+    // Release it before waiting for the brain to reap either entry.\n     let kept_source = work.join(\"kept-by-the-guard.md\");\n     std::fs::write(&kept_source, b\"the user's own file\").unwrap();\n-    for attempt in 0..5 {\n+    {\n+        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n         let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n         let planted = registry\n             .add_reference(&kept_source, Some(\"kept-by-the-guard.md\"), None, 1)\n@@ -706,10 +693,6 @@ registry: {}\",\n             .scope_entry(&planted.id, Some(1), None, None)\n             .expect(\"give it an already-expired ttl\");\n         registry.save_at(&registry_path).unwrap();\n-        if snapshot_of(\"kept-by-the-guard.md\").is_some() {\n-            break;\n-        }\n-        assert!(attempt < 4, \"the planted reference entry kept being clobbered\");\n     }\n \n     // One pulse is LIVE_RECONCILE_INTERVAL_MS (5 s); wait past two.\ndiff --git a/crates/spt/tests/webserve_cross_node_e2e.rs b/crates/spt/tests/webserve_cross_node_e2e.rs\nindex a4a5a48b..be23c819 100644\n--- a/crates/spt/tests/webserve_cross_node_e2e.rs\n+++ b/crates/spt/tests/webserve_cross_node_e2e.rs\n@@ -659,11 +659,15 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n         .expect(\"seed B's view of who hosts the audience\");\n \n         let registry_path = spt_store::perch::serving_registry_file_in(home_b.path());\n-        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n-        serving\n-            .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n-            .expect(\"the served entry is there to narrow\");\n-        serving.save_at(&registry_path).expect(\"persist the narrowing\");\n+        // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+        {\n+            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n+            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n+            serving\n+                .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n+                .expect(\"the served entry is there to narrow\");\n+            serving.save_at(&registry_path).expect(\"persist the narrowing\");\n+        }\n \n         let (status, _, body) = http(port_a, \"GET\", &url, &[]);\n         assert_eq!(\n@@ -673,11 +677,14 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n         );\n \n         // Now the SAME entry addressed to an endpoint B cannot place on A.\n-        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n-        serving\n-            .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n-            .expect(\"the entry is still there\");\n-        serving.save_at(&registry_path).expect(\"persist the second narrowing\");\n+        {\n+            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n+            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n+            serving\n+                .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n+                .expect(\"the entry is still there\");\n+            serving.save_at(&registry_path).expect(\"persist the second narrowing\");\n+        }\n \n         let (status, _, body) = http(port_a, \"GET\", &url, &[]);\n         let body = String::from_utf8(body).unwrap();\n@@ -757,9 +764,12 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n         );\n \n         // Restore the open entry for the arms that follow.\n-        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n-        serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n-        serving.save_at(&registry_path).expect(\"persist the restore\");\n+        {\n+            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n+            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n+            serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n+            serving.save_at(&registry_path).expect(\"persist the restore\");\n+        }\n         let (status, _, _) = http(port_a, \"GET\", &url, &[]);\n         assert_eq!(status, 200, \"audience cleared, the fetch is open again\");\n     }"}],"details":{"resolvedPath":"C:\\Users\\decid\\.omp\\agent\\sessions\\-Documents-projects-spt-core\\2026-09-13T10-23-45-921Z_01a09a4b-5f01-7000-9118-23ce74a49636\\495.bash-original.log","contentType":"text/plain","totalLines":460,"displayContent":{"text":"+        let next = registry.add_reference(&third_path, Some(\"report.txt\"), None, 2_000)\n+            .expect(\"new source gets next suffix\");\n+        assert_eq!(next.served_name, \"report~3.txt\", \"neither expired nor retired names may be reassigned\");\n+        registry.save_at(&registry_path).expect(\"persist continued history\");\n+    }\n+    let published = ServingRegistry::load_at(&registry_path).expect(\"read continued assignments\");\n+    assert_eq!(published.get(\"report~1.txt\").expect(\"old owner\").path, retired_path);\n+    assert_eq!(published.get(\"report~2.txt\").expect(\"recent owner\").path, registered_path);\n+    assert_eq!(published.get(\"report~3.txt\").expect(\"new owner\").path, third_path);\n+}\n+\n+// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n+#[test]\n+fn loaded_reaper_serializes_registration_without_resurrecting_expiry() {\n+    overlap(\"reap\", false);\n+}\n+\n+// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n+#[test]\n+fn later_reap_preserves_a_registration_published_while_it_was_waiting() {\n+    overlap(\"register\", false);\n+}\n+\n+// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+#[test]\n+fn abrupt_holder_exit_releases_the_sentinel_for_the_waiting_reaper() {\n+    overlap(\"register\", true);\n+}\ndiff --git a/crates/spt/tests/webserve_attachment_e2e.rs b/crates/spt/tests/webserve_attachment_e2e.rs\nindex c1516a82..8c235cb7 100644\n--- a/crates/spt/tests/webserve_attachment_e2e.rs\n+++ b/crates/spt/tests/webserve_attachment_e2e.rs\n@@ -661,43 +661,30 @@ registry: {}\",\n     //\n     // `scope_entry` also sets `audience` unconditionally from its argument, so\n     // passing None CLEARS any audience. reap-me.md has none, which makes that a\n-    // no-op HERE — but do not lift this loop onto an audienced entry without\n+    // no-op HERE — but do not lift this mutation onto an audienced entry without\n     // passing its audience back in, or the scope goes with the ttl.\n     //\n-    // The loop re-reads because the daemon is the registry's other writer. Be\n-    // precise about what that buys: it detects the entry being ABSENT, not a\n-    // clobber — a daemon save landing between this load and this save would be\n-    // overwritten by this snapshot and nothing here would notice. That hazard is\n-    // idle in this arm by arithmetic rather than by vigilance: every other live\n-    // entry carries 30 d or 3600 s, so the only concurrent writer has nothing to\n-    // retire and nothing to write, and this snapshot equals its state.\n-    let mut retired = false;\n-    for attempt in 0..5 {\n+    // This fixture is another PROCESS writing the live daemon's registry.\n+    // Hold its shared guard across the fresh read and publication, not a retry\n+    // after a stale write. An already-retired entry needs no further mutation.\n+    // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+    {\n+        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n         let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n         if registry\n             .scope_entry(\"reap-me.md\", Some(1), None, None)\n             .is_some()\n         {\n             registry.save_at(&registry_path).unwrap();\n-            retired = true;\n-            break;\n-        }\n-        // Gone already means the daemon retired it between our read and now,\n-        // which is the outcome this loop is trying to cause — not a failure.\n-        if snapshot_of(\"reap-me.md\").is_none() {\n-            retired = true;\n-            break;\n         }\n-        assert!(attempt < 4, \"could not backdate the doomed entry's ttl\");\n     }\n-    assert!(retired, \"the doomed entry was given an already-expired lifetime\");\n \n-    // A reference entry, already expired, planted for the guard. Re-read after\n-    // planting: the daemon is the registry's other writer and its own tick could\n-    // land between this load and this save.\n+    // Plant the already-expired reference under the same transaction guard.\n+    // Release it before waiting for the brain to reap either entry.\n     let kept_source = work.join(\"kept-by-the-guard.md\");\n     std::fs::write(&kept_source, b\"the user's own file\").unwrap();\n-    for attempt in 0..5 {\n+    {\n+        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n         let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n         let planted = registry\n             .add_reference(&kept_source, Some(\"kept-by-the-guard.md\"), None, 1)\n@@ -706,10 +693,6 @@ registry: {}\",\n             .scope_entry(&planted.id, Some(1), None, None)\n             .expect(\"give it an already-expired ttl\");\n         registry.save_at(&registry_path).unwrap();\n-        if snapshot_of(\"kept-by-the-guard.md\").is_some() {\n-            break;\n-        }\n-        assert!(attempt < 4, \"the planted reference entry kept being clobbered\");\n     }\n \n     // One pulse is LIVE_RECONCILE_INTERVAL_MS (5 s); wait past two.\ndiff --git a/crates/spt/tests/webserve_cross_node_e2e.rs b/crates/spt/tests/webserve_cross_node_e2e.rs\nindex a4a5a48b..be23c819 100644\n--- a/crates/spt/tests/webserve_cross_node_e2e.rs\n+++ b/crates/spt/tests/webserve_cross_node_e2e.rs\n@@ -659,11 +659,15 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n         .expect(\"seed B's view of who hosts the audience\");\n \n         let registry_path = spt_store::perch::serving_registry_file_in(home_b.path());\n-        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n-        serving\n-            .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n-            .expect(\"the served entry is there to narrow\");\n-        serving.save_at(&registry_path).expect(\"persist the narrowing\");\n+        // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n+        {\n+            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n+            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n+            serving\n+                .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n+                .expect(\"the served entry is there to narrow\");\n+            serving.save_at(&registry_path).expect(\"persist the narrowing\");\n+        }\n \n         let (status, _, body) = http(port_a, \"GET\", &url, &[]);\n         assert_eq!(\n@@ -673,11 +677,14 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n         );\n \n         // Now the SAME entry addressed to an endpoint B cannot place on A.\n-        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n-        serving\n-            .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n-            .expect(\"the entry is still there\");\n-        serving.save_at(&registry_path).expect(\"persist the second narrowing\");\n+        {\n+            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n+            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n+            serving\n+                .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n+                .expect(\"the entry is still there\");\n+            serving.save_at(&registry_path).expect(\"persist the second narrowing\");\n+        }\n \n         let (status, _, body) = http(port_a, \"GET\", &url, &[]);\n         let body = String::from_utf8(body).unwrap();\n@@ -757,9 +764,12 @@ fn a_peers_url_is_served_by_its_owner_through_the_local_listener() {\n         );\n \n         // Restore the open entry for the arms that follow.\n-        let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n-        serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n-        serving.save_at(&registry_path).expect(\"persist the restore\");\n+        {\n+            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n+            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n+            serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n+            serving.save_at(&registry_path).expect(\"persist the restore\");\n+        }\n         let (status, _, _) = http(port_a, \"GET\", &url, &[]);\n         assert_eq!(status, 200, \"audience cleared, the fetch is open again\");\n     }","startLine":300,"lineNumbers":[300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460]},"meta":{"source":{"type":"internal","value":"artifact://495"}}},"isError":false,"timestamp":1789522902770}}
{"type":"message","id":"a5481615","parentId":"07466f1b","timestamp":"2026-09-16T01:41:42.772Z","message":{"role":"toolResult","toolCallId":"call_d0jkfv1aavQfBPNYdUyRSL7p|fc_0cdf94810c888815016aa9f3d6a93887d18686ce5412ede1f6","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\\308-registry-process-lock\\AGENTS.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789522902771}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CGja2YGtWHdLNsdo9gT50jbH|fc_0cdf94810c888815016aa9f3e2434887d1bc6eadeedceed1eb","toolName":"grep","startedAt":"2026-09-16T01:41:54.331Z","args":{"path":".worktrees/308-registry-process-lock/crates"},"intent":"Finding serving registry writers"},"id":"2a316888","parentId":"a5481615","timestamp":"2026-09-16T01:41:54.331Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_enszuFAoJslQCLp2koOhaxNh|fc_0cdf94810c888815016aa9f3e2435887d1a67be3cf2a332343","toolName":"read","startedAt":"2026-09-16T01:41:54.331Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs:1210-1420"},"intent":"Reading reaper lifetime context"},"id":"e481f390","parentId":"2a316888","timestamp":"2026-09-16T01:41:54.331Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1D5xJZrfYezMmqsOKH9wY55z|fc_0cdf94810c888815016aa9f3e2436087d1a1e7986238a13e29","toolName":"read","startedAt":"2026-09-16T01:41:54.332Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs:375-851"},"intent":"Reading serving host tests"},"id":"17cc6e4a","parentId":"e481f390","timestamp":"2026-09-16T01:41:54.332Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gy0bNxrxoPMKkuY6PXNPhluP|fc_0cdf94810c888815016aa9f3e2436887d1bae04ec73b06f320","toolName":"glob","startedAt":"2026-09-16T01:41:54.332Z","args":{"path":"C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/windows.rs;C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/lib.rs"},"intent":"Locating Windows lock implementation"},"id":"669807a8","parentId":"17cc6e4a","timestamp":"2026-09-16T01:41:54.332Z"}
{"type":"message","id":"82fe374b","parentId":"669807a8","timestamp":"2026-09-16T01:41:54.332Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Tracing writer gaps**"},{"type":"toolCall","id":"call_CGja2YGtWHdLNsdo9gT50jbH|fc_0cdf94810c888815016aa9f3e2434887d1bc6eadeedceed1eb","name":"grep","arguments":{"i":"Finding serving registry writers","pattern":"ServingRegistry|serving_registry_file|registry\\.lock|lock_registry_at|with_registry_write|SERVE_REGISTRY_POISONED","path":".worktrees/308-registry-process-lock/crates","case":true,"gitignore":true,"skip":null},"intent":"Finding serving registry writers"},{"type":"toolCall","id":"call_enszuFAoJslQCLp2koOhaxNh|fc_0cdf94810c888815016aa9f3e2435887d1a67be3cf2a332343","name":"read","arguments":{"i":"Reading reaper lifetime context","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs:1210-1420"},"intent":"Reading reaper lifetime context"},{"type":"toolCall","id":"call_1D5xJZrfYezMmqsOKH9wY55z|fc_0cdf94810c888815016aa9f3e2436087d1a1e7986238a13e29","name":"read","arguments":{"i":"Reading serving host tests","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs:375-851"},"intent":"Reading serving host tests"},{"type":"toolCall","id":"call_gy0bNxrxoPMKkuY6PXNPhluP|fc_0cdf94810c888815016aa9f3e2436887d1bae04ec73b06f320","name":"glob","arguments":{"i":"Locating Windows lock implementation","path":"C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/windows.rs;C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/lib.rs","hidden":true,"gitignore":false,"limit":10},"intent":"Locating Windows lock implementation"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":12515,"output":346,"cacheRead":29312,"cacheWrite":0,"totalTokens":42173,"reasoningTokens":82,"cost":{"input":0.12515,"output":0.0173,"cacheRead":0.029311999999999998,"cacheWrite":0,"total":0.17176200000000003}},"stopReason":"toolUse","timestamp":1789522902801,"responseId":"resp_0cdf94810c888815016aa9f3d6fef887d1aa19c33ef0cd6f9a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cdf94810c888815016aa9f3d7e5fc87d1a96da31cef5b4e0e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqfPi8Xu-xzE6wHr_SF7aCxycj-oDYE38DMJij9YH43PRLA44LZideGsSsiIpPQb88VjBkKP8gGOC_pE_7z0xp4SKfM-fDpBqrk8fMtaMU-hLhV6-yiJiN-SkR5DSw_nfovQqYIFgWqGiGC42tEZmP024bqlwVeN5ZTdDz3FlGpua9ti-73pwRE6pv-uhPz64jvxJmtA3rF1c7x_lDI3MeVaCD4k3fYjsn2QPD7nh6czfuN-E1KbY8L3NXeeiU7AdVtPk-E2Uv1cIptQ3ZNQ3SlnDSPEbrNWREtlZ59c8vBXU3AHcE3B07rzEvRVF3hlwWN5UGKFb7OP1tPOA18A63j6jPJATNPap-aGEjcJnU5lLzCo_JXQMm7AxPxr11jlOXpsl-jStp9YmjMyNXOvP8g7qUWGCmtSTjHH-789Ycjfz8mKk4iGaS1ws0hXiFcyV__PFiA68Cz2wVQf7n-kFM5sTJ0iLuTyfIllhQ_iJU2ApbzKZuD5ZwuB_WqMLT5a53ZexSZjGgk3DR1FTa5_qAjDk3RMNWTJkClkzqsPDEm63J0_qjGn6N5h8oDsAi8GcDyrl17X7HvTMLL0fBVJ4htg3cd8jBN6ILbdbW0YEYCLLxVZ-Q44QkcWo077hXUhja5RYkmtKN-DIpyhyV79xSbBFSfQUGrq_DgnFC6u6UeH7XmkKrrJWwrvBshSiZBjhkBRLiiLgzxqIU1jCriP-gsc9XlKBe25q6ZXeUYbNri1SGBTqeBSpAj-j8Jhcsg1jCzxCL4_3bpUq4l6HfrPWCs8QCEmdsmm2g7kaOVjTq3lnWqdT_GzTTKJq7Btz8MD0jjguvDBERxi1ca1WSdyrupxxa7CUE28caFO0r890blIEL7_6dbJEVFvsoGWQgnzPEzgdhJZmZU4MbOQ4F56IAeTcd9IXw-QgGMfH8E1My7Pi_DIF6IujGgo_r4G662y49S0DsI-vFlVVlsI5qQD0iDbbQSbsK1sqvRDvzHa4RkcQVXIb7NZKOLDHaymKpaVqijpJXwqVqyvmuHgaDElM69bJHykpZycYlkLY6-oAp-7XphidraH748faOGN_pBUnDPM6EX3c6sS95qT0kitrHYgxMv-vMnoQ4rhLlobDuH1R56ecJ3acw2XlpXlO83kVZTHTDw6BSQuSjhy09mxI_qG1QMu_tkBa7E9YqKscgD5RQUqtNV3fF8hhP4iyXXgd902J3L83L2Q9EHBzi_jPi0nR1r39mTEqWeXzqDDj3HynW9oGvfOmnMevlwC1EYcOnRtdy__0PQQFtUUaHH-zTofg8jYKd9Rl52hE_L9cLp3Bbs4LSwVKd8QYf26LB-i3eVnqoNCZ3VFGexZwEQ2-8uR_nsmP_yjSb15ewBtbV8d021PGQWue6qll_--Qd9VVDkAQt6-VElAw-U2YWtRoPYgCHNGoZuT50x5gEtL1ZBrlRqb93gQfsAC3oO-zNNI9TdsG_Jte0fTeNDs0_gGP1NOpcfi_rGeU67LcQVYxf7CD9rjg7tW4UZTRO04kRm1w9iTBUCiIpE9dJKO-9sgihVqco72hI0s9_6R0FNNXtlgJOFc02cMqCb6UcdfvKNK9jpUUOTAaGDQzndwjrtJZV_LQSywqLhZPuZn3C1WoMVidMFhOsKBW-yT_PlPnVQI-skAYW7-3X_SrKOsJ_V2xwbQyCXEFXMYdg5MyV150xmDdv8WLvwkEkLLV6_FMeLWkOtUWAY9Aomxp928phFvI4XW62G8E_KOsTqIwwi7-nz_hkzgOnyxMBdlk8_dpNA2dBpUx8dntDpjvxdYaTb32pDtXKEZLCVwI7oo6jMnK6J_CejEd7srij6q87NJZnXBK-CZ6","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"summary":[{"type":"summary_text","text":"**Tracing writer gaps**"}],"metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3e2434887d1bc6eadeedceed1eb","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding serving registry writers\",\"pattern\":\"ServingRegistry|serving_registry_file|registry\\\\.lock|lock_registry_at|with_registry_write|SERVE_REGISTRY_POISONED\",\"path\":\".worktrees/308-registry-process-lock/crates\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_CGja2YGtWHdLNsdo9gT50jbH","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3e2435887d1a67be3cf2a332343","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading reaper lifetime context\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs:1210-1420\"}","call_id":"call_enszuFAoJslQCLp2koOhaxNh","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3e2436087d1a1e7986238a13e29","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading serving host tests\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs:375-851\"}","call_id":"call_1D5xJZrfYezMmqsOKH9wY55z","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3e2436887d1bae04ec73b06f320","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating Windows lock implementation\",\"path\":\"C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/windows.rs;C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/lib.rs\",\"hidden\":true,\"gitignore\":false,\"limit\":10}","call_id":"call_gy0bNxrxoPMKkuY6PXNPhluP","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"glob","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":11527.38810000004,"ttft":1117.998900000006,"completedAt":1789522914329,"contextSnapshot":{"promptTokens":41827,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"message","id":"06f7f816","parentId":"82fe374b","timestamp":"2026-09-16T01:41:54.346Z","message":{"role":"toolResult","toolCallId":"call_1D5xJZrfYezMmqsOKH9wY55z|fc_0cdf94810c888815016aa9f3e2436087d1a1e7986238a13e29","toolName":"read","content":[{"type":"text","text":"pub fn port_from_status(result: ServeResult) -> io::Result<u16> {\n    match result {\n        ServeResult::DocsStatus { port: Some(port) } if port != 0 => Ok(port),\n        ServeResult::DocsStatus { .. } => Err(io::Error::other(\n            \"DOCS_LISTENER_UNAVAILABLE: the running daemon has no bound docs listener\",\n        )),\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\n        _ => Err(io::Error::other(\n            \"DOCS_LISTENER_UNREPORTED: the running daemon did not report its bound docs port\",\n        )),\n    }\n}\n\n/// Dispatch only after the local control transport's version handshake.\n// [impl->REQ-WEB-SERVING-REGISTRY]\npub(crate) fn handle(payload: serde_json::Value) -> Envelope {\n    let result = serde_json::from_value(payload)\n        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))\n        .and_then(|request| {\n            let now_ms = std::time::SystemTime::now()\n                .duration_since(std::time::UNIX_EPOCH)\n                .map_err(io::Error::other)?\n                .as_millis()\n                .try_into()\n                .map_err(io::Error::other)?;\n            apply_at(&spt_store::perch::spt_home(), request, now_ms)\n        })\n        .unwrap_or_else(|e| ServeResult::Refused { error: e.to_string() });\n    Envelope::new(\n        KIND_SERVE_RESULT,\n        serde_json::to_value(result).expect(\"serving result serializes\"),\n    )\n}\n\n/// Call the daemon, bounded because an older daemon ignores unknown control\n/// kinds. A timeout is an unconfirmed outcome, never permission to retry a write.\n// [impl->REQ-WEB-SERVING-REGISTRY]\npub fn call(name: &str, request: ServeRequest) -> io::Result<ServeResult> {\n    call_with_timeout(name, request, std::time::Duration::from_secs(10))\n}\n\n/// [`call`] with an explicit bound. The turn-boundary now-signal poll uses a\n/// SHORT one: a wedged daemon must cost that hook milliseconds, not the write\n/// path's ten seconds, and absence is silence there rather than an error.\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\npub fn call_with_timeout(\n    name: &str,\n    request: ServeRequest,\n    timeout: std::time::Duration,\n) -> io::Result<ServeResult> {\n    let name = name.to_owned();\n    let (tx, rx) = std::sync::mpsc::sync_channel(1);\n    std::thread::spawn(move || {\n        let result = (|| {\n            let mut conn = LocalSocketTransport::connect(&name)?;\n            send_hello(&mut conn, Role::Brain)?;\n            write_frame(\n                &mut conn,\n                &Envelope::new(\n                    KIND_SERVE_REQUEST,\n                    serde_json::to_value(request).expect(\"serving request serializes\"),\n                ),\n            )?;\n            let reply = read_frame(&mut conn)?;\n            if reply.kind != KIND_SERVE_RESULT {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\"SERVE_UNEXPECTED_REPLY: {}\", reply.kind),\n                ));\n            }\n            serde_json::from_value(reply.payload)\n                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))\n        })();\n        let _ = tx.send(result);\n    });\n    rx.recv_timeout(timeout)\n        .map_err(|e| match e {\n            std::sync::mpsc::RecvTimeoutError::Timeout => io::Error::new(\n                io::ErrorKind::TimedOut,\n                \"SERVE_UNCONFIRMED: daemon did not answer; inspect serve list before retrying; update/restart an older daemon\",\n            ),\n            std::sync::mpsc::RecvTimeoutError::Disconnected => {\n                io::Error::other(\"SERVE_UNCONFIRMED: control worker disconnected\")\n            }\n        })?\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn add(path: &Path) -> ServeRequest {\n        ServeRequest::Add { path: path.to_owned(), as_name: None, origin: Some(\"alice\".into()) }\n    }\n\n    fn input_reference(path: &Path, receipt: &str, audience: &str, received_at_ms: u64) -> ServeRequest {\n        ServeRequest::AddInputReference {\n            path: path.to_owned(),\n            receipt_id: receipt.into(),\n            audience: audience.into(),\n            received_at_ms,\n        }\n    }\n\n    #[test]\n    fn input_reference_missing_is_a_silent_skip_without_registry_publication() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let result = apply_at(&home, input_reference(&tmp.path().join(\"missing\"), \"r1\", \"agent\", 1), 1)\n            .unwrap();\n        assert!(matches!(result, ServeResult::Missing));\n        assert!(!spt_store::perch::serving_registry_file_in(&home).exists());\n    }\n\n    #[test]\n    fn input_reference_replay_preserves_the_live_entry_and_deadline() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"report.txt\");\n        std::fs::write(&source, b\"before\").unwrap();\n        let ServeResult::Entry { entry } = apply_at(&home, input_reference(&source, \"r1\", \"agent\", 10), 20)\n            .unwrap() else { panic!(\"expected reference\") };\n        let ServeResult::Entry { entry: replay } =\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", 50), 50).unwrap()\n            else { panic!(\"expected replay\") };\n        assert_eq!(replay, entry);\n        // A second submission can reuse the same narrow exposure, not renew it.\n        let ServeResult::Entry { entry: second } =\n            apply_at(&home, input_reference(&source, \"r2\", \"agent\", 60), 60).unwrap()\n            else { panic!(\"expected shared live reference\") };\n        assert_eq!(second, entry);\n        assert_eq!(list(&home), vec![entry]);\n    }\n\n    #[test]\n    fn input_reference_cannot_rescope_another_audience_or_unrelated_entry() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"report.txt\");\n        let ordinary = tmp.path().join(\"ordinary.txt\");\n        std::fs::write(&source, b\"scoped\").unwrap();\n        std::fs::write(&ordinary, b\"ordinary\").unwrap();\n        apply_at(&home, input_reference(&source, \"r1\", \"agent\", 10), 10).unwrap();\n        apply_at(&home, add(&ordinary), 10).unwrap();\n        let before = list(&home);\n        for request in [\n            input_reference(&source, \"r1\", \"other\", 20),\n            input_reference(&ordinary, \"r2\", \"agent\", 20),\n        ] {\n            let error = apply_at(&home, request, 20).unwrap_err();\n            assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);\n            assert!(error.to_string().contains(\"INPUT_PATH_SCOPE_CONFLICT\"));\n        }\n        assert_eq!(list(&home), before);\n    }\n\n    #[test]\n    fn input_reference_expiry_cannot_be_renewed_by_replay_before_or_after_reaping() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"report.txt\");\n        std::fs::write(&source, b\"live\").unwrap();\n        let request = input_reference(&source, \"r1\", \"agent\", 10);\n        let ServeResult::Entry { entry } = apply_at(&home, request.clone(), 10).unwrap()\n            else { panic!(\"expected reference\") };\n        let expires = 10 + spt_store::serving::HELPER_ENTRY_TTL_MS;\n        assert!(apply_at(&home, request.clone(), expires).unwrap_err().to_string()\n            .contains(\"INPUT_PATH_EXPIRED\"));\n        apply_at(&home, ServeRequest::Remove { name_or_id: entry.id }, expires).unwrap();\n        assert!(apply_at(&home, request, expires).unwrap_err().to_string()\n            .contains(\"INPUT_PATH_EXPIRED\"));\n        let ServeResult::Entry { entry: renewed } =\n            apply_at(&home, input_reference(&source, \"r2\", \"agent\", expires), expires).unwrap()\n            else { panic!(\"a new receipt may register after expiry\") };\n        assert_eq!(renewed.registered_at_ms, expires);\n        let next_expiry = expires + spt_store::serving::HELPER_ENTRY_TTL_MS;\n        let ServeResult::Entry { entry: replaced } =\n            apply_at(&home, input_reference(&source, \"r3\", \"agent\", next_expiry), next_expiry).unwrap()\n            else { panic!(\"a fresh receipt must not wait for the periodic reaper\") };\n        assert_ne!(replaced.id, renewed.id);\n        assert_eq!(list(&home), vec![replaced]);\n    }\n\n    #[test]\n    fn input_reference_clock_skew_never_extends_either_nodes_lifetime() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"report.txt\");\n        std::fs::write(&source, b\"live\").unwrap();\n        let ServeResult::Entry { entry } =\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", 500), 100).unwrap()\n            else { panic!(\"a future remote clock does not refuse a live source\") };\n        assert_eq!(entry.registered_at_ms, 100);\n        let ServeResult::Entry { entry: replay } =\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", 500), 600).unwrap()\n            else { panic!(\"expected unchanged replay\") };\n        assert_eq!(replay, entry);\n    }\n\n    #[test]\n    fn input_reference_http_reads_observe_edits_and_deletion() {\n        use http_body_util::BodyExt;\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"report.txt\");\n        std::fs::write(&source, b\"before\").unwrap();\n        let now = crate::brain::now_ms();\n        let ServeResult::Entry { entry } =\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", now), now).unwrap()\n            else { panic!(\"expected reference\") };\n        let uri = format!(\"/local/f/{}\", entry.served_name);\n        let get = || crate::webserve::handle_path(&home, &home.join(\"docs\"), \"LOCAL\", &uri, None, false);\n        let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();\n        assert_eq!(runtime.block_on(get().into_body().collect()).unwrap().to_bytes().as_ref(), b\"before\");\n        std::fs::write(&source, b\"edited\").unwrap();\n        assert_eq!(runtime.block_on(get().into_body().collect()).unwrap().to_bytes().as_ref(), b\"edited\");\n        std::fs::remove_file(&source).unwrap();\n        assert_eq!(get().status(), hyper::StatusCode::NOT_FOUND);\n        assert!(matches!(\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", now), now).unwrap(),\n            ServeResult::Missing,\n        ));\n    }\n\n    /// LanStatus is a QUESTION: it writes nothing, and it does not re-run the gate.\n    ///\n    /// Both halves are load-bearing, and both are asserted against a home the\n    /// gate would REFUSE. The write half: this request is answered before the\n    /// registry writer lock, so asking must not create the serving registry —\n    /// a turn-boundary hook that polls every turn would otherwise mint a file\n    /// on a node that never served anything. The gate half: the discriminator\n    /// is the ANSWER SHAPE. `set-not-applied` is what this empty home earns\n    /// from the set gate, so a `LanRefused` here would prove the status query\n    /// had run it; `LanStopped { NOT_UP }` is the read of process state that a\n    /// question is allowed to make. Asked twice for the same reason — a side\n    /// effect that only shows on the second ask is still a side effect.\n    // [unit->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\n    #[test]\n    fn a_lan_status_writes_nothing_and_does_not_run_the_set_gate() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let registry = spt_store::perch::serving_registry_file_in(&home);\n\n        for ask in 1..=2 {\n            let answer = apply_at(&home, ServeRequest::LanStatus, ask).unwrap();\n            match answer {\n                ServeResult::LanStopped { ref code } => assert_eq!(\n                    code.as_deref(),\n                    Some(crate::lanhost::NOT_UP),\n                    \"down answers by name, ask {ask}\"\n                ),\n                other => panic!(\n                    \"ask {ask}: a status query answered {other:?} — a LanRefused would mean it ran the gate\"\n                ),\n            }\n            assert!(\n                !registry.exists(),\n                \"ask {ask} created the serving registry: a question must not take the writer path\"\n            );\n        }\n    }\n\n    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n    #[test]\n    fn unavailable_registry_lock_cannot_publish_an_add() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"report.md\");\n        std::fs::write(&source, \"must not be registered\").unwrap();\n        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n        // A directory at the sentinel path deterministically refuses acquisition\n        // on both supported platforms, without permission or timing assumptions.\n        let mut lock_path = registry_path.as_os_str().to_os_string();\n        lock_path.push(\".lock\");\n        std::fs::create_dir_all(PathBuf::from(lock_path)).unwrap();\n\n        assert!(apply_at(&home, add(&source), 1).is_err());\n        assert!(!registry_path.exists(), \"a failed lock must not permit publication\");\n    }\n\n    // [unit->REQ-WEB-SERVING-REGISTRY]\n    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n    #[test]\n    fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let first = tmp.path().join(\"first\");\n        let second = tmp.path().join(\"second\");\n        std::fs::create_dir_all(&first).unwrap();\n        std::fs::create_dir_all(&second).unwrap();\n        let a = first.join(\"report.md\");\n        let b = second.join(\"report.md\");\n        std::fs::write(&a, \"first\").unwrap();\n        std::fs::write(&b, \"second\").unwrap();\n        std::thread::scope(|scope| {\n            let one = scope.spawn(|| apply_at(&home, add(&a), 1).unwrap());\n            let two = scope.spawn(|| apply_at(&home, add(&b), 2).unwrap());\n            assert!(matches!(one.join().unwrap(), ServeResult::Entry { .. }));\n            assert!(matches!(two.join().unwrap(), ServeResult::Entry { .. }));\n        });\n        let ServeResult::Listed { entries } = apply_at(&home, ServeRequest::List, 3).unwrap() else {\n            panic!(\"list did not return entries\");\n        };\n        let paths: std::collections::BTreeSet<_> = entries.iter().map(|e| &e.path).collect();\n        assert_eq!(paths, [&a, &b].into_iter().collect());\n        let names: std::collections::BTreeSet<_> = entries.iter().map(|e| e.served_name.as_str()).collect();\n        assert_eq!(names, [\"report.md\", \"report~1.md\"].into_iter().collect());\n    }\n\n    /// `AddScoped` is the FILE_ACCESS_HELPER's registration, and it had no cell\n    /// at all — neither for the file case nor this one. A quoted path can name a\n    /// DIRECTORY, and the helper must register it as one: the two halves (add a\n    /// reference, then narrow it) happen under one lock precisely so no window\n    /// exists in which the directory is exposed and not yet addressed to anyone.\n    ///\n    /// The kind is the part worth asserting. If a directory silently registered\n    /// as a `File` the surface would serve nothing and the failure would look\n    /// like a missing file rather than a wrong kind.\n    // [unit->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\n    #[test]\n    fn a_quoted_directory_registers_as_a_narrowed_directory_entry() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let quoted = tmp.path().join(\"shared\");\n        std::fs::create_dir_all(quoted.join(\"nested\")).unwrap();\n        std::fs::write(quoted.join(\"index.html\"), \"an index\").unwrap();\n\n        let request = ServeRequest::AddScoped {\n            path: quoted.clone(),\n            as_name: None,\n            origin: Some(\"ABCDEFGH\".into()),\n            ttl_ms: spt_store::serving::HELPER_ENTRY_TTL_MS,\n            audience: \"todlando\".into(),\n        };\n        let ServeResult::Entry { entry } = apply_at(&home, request, 1).unwrap() else {\n            panic!(\"AddScoped did not return an entry\");\n        };\n\n        assert_eq!(entry.kind, spt_store::serving::ServedKind::Dir, \"a quoted directory is a DIR entry\");\n        assert_eq!(entry.path, quoted);\n        assert_eq!(entry.audience.as_deref(), Some(\"todlando\"), \"addressed to the one endpoint\");\n        assert_eq!(entry.ttl_ms, Some(spt_store::serving::HELPER_ENTRY_TTL_MS), \"the helper's 24h lifetime\");\n        assert_eq!(entry.origin.as_deref(), Some(\"ABCDEFGH\"), \"origin is the message short-ID\");\n\n        // And it PERSISTED narrowed — the add and the scope are one locked pass,\n        // so a reader must never observe the un-narrowed half.\n        let ServeResult::Listed { entries } = apply_at(&home, ServeRequest::List, 2).unwrap() else {\n            panic!(\"list did not return entries\");\n        };\n        assert_eq!(entries.len(), 1);\n        assert_eq!(entries[0].audience.as_deref(), Some(\"todlando\"));\n        assert_eq!(entries[0].kind, spt_store::serving::ServedKind::Dir);\n    }\n\n    // [unit->REQ-WEB-SERVING-REGISTRY]\n    #[test]\n    fn removal_persists_and_does_not_delete_reference_bytes() {\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let file = tmp.path().join(\"report.md\");\n        std::fs::write(&file, \"keep me\").unwrap();\n        let ServeResult::Entry { entry } = apply_at(&home, add(&file), 1).unwrap() else {\n            panic!(\"add did not return an entry\");\n        };\n        apply_at(&home, ServeRequest::Remove { name_or_id: entry.id }, 2).unwrap();\n        assert_eq!(std::fs::read(&file).unwrap(), b\"keep me\");\n        let ServeResult::Entry { entry } = apply_at(&home, add(&file), 3).unwrap() else {\n            panic!(\"re-add did not return an entry\");\n        };\n        assert_eq!(entry.served_name, \"report.md\");\n    }\n\n    fn register_adapter(home: &Path, source: &Path, alias: Option<&str>, now_ms: u64) {\n        std::fs::create_dir_all(source).unwrap();\n        let mut manifest = String::from(\n            \"[adapter]\\nname = 'reports'\\nversion = '1'\\nmin_spt_core_version = '0'\\n\"\n        );\n        if let Some(alias) = alias {\n            manifest.push_str(&format!(\"web_short_path = {alias:?}\\n\"));\n        }\n        manifest.push_str(\"\\n[profiles.work.adapter]\\nweb_short_path = 'profile-alias'\\n\");\n        std::fs::write(source.join(\"manifest.toml\"), manifest).unwrap();\n        spt_runtime::registry::register(&home.join(\"adapters\"), source, now_ms).unwrap();\n    }\n\n    fn list(home: &Path) -> Vec<ServedEntry> {\n        let ServeResult::Listed { entries } = apply_at(home, ServeRequest::List, 0).unwrap() else {\n            panic!(\"list did not return entries\");\n        };\n        entries\n    }\n\n    // [unit->REQ-WEB-SERVING-REGISTRY] [unit->REQ-WEB-URL-NODE-PREFIX]\n    #[test]\n    fn adapter_lifecycle_has_one_root_and_retains_output_and_live_assignments() {\n        use spt_store::serving::{alias_url, entry_url, ServedKind};\n        let tmp = tempfile::tempdir().unwrap();\n        let home = tmp.path().join(\"home\");\n        let source = tmp.path().join(\"install\");\n        register_adapter(&home, &source, Some(\"cspt\"), 1);\n        let root = home.join(\"adapters\").join(\"reports\").join(\"web\");\n        let output = root.join(\"index.html\");\n        // Output can be produced by install before the daemon has ever run.\n        std::fs::write(&output, b\"retained output\").unwrap();\n        let collision = tmp.path().join(\"cspt\");\n        std::fs::write(&collision, b\"explicit reference\").unwrap();\n        apply_at(&home, add(&collision), 2).unwrap();\n        reconcile_at(&home, 3).unwrap();\n        let entries = list(&home);\n        assert_eq!(entries.len(), 2);\n        let entry = entries.iter().find(|entry| entry.adapter.is_some()).unwrap().clone();\n        assert_eq!(entry.path, root);\n        assert_eq!(entry.kind, ServedKind::Dir);\n        assert_eq!(entry_url(\"node\", 5474, &entry), \"http://localhost:5474/node/a/reports/\");\n        assert_eq!(alias_url(\"node\", 5474, &entry).as_deref(), Some(\"http://localhost:5474/node/cspt~1/\"));\n        assert!(!entry.path.join(\"manifest.toml\").exists());\n        assert!(source.join(\"manifest.toml\").is_file());\n        assert!(spt_runtime::registry::resolve_option(&home.join(\"adapters\"), \"reports:work\").is_ok());\n\n        // An update, a reconcile, and a restart preserve the live URL even if\n        // the newly declared alias differs. Profiles never create another row.\n        register_adapter(&home, &source, Some(\"changed\"), 4);\n        apply_at(&home, ServeRequest::Reconcile, 5).unwrap();\n        reconcile_at(&home, 6).unwrap();\n        assert_eq!(list(&home).iter().find(|row| row.adapter.is_some()), Some(&entry));\n        assert_eq!(list(&home).len(), 2);\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\n\n        spt_runtime::registry::deregister(&home.join(\"adapters\"), \"reports\").unwrap();\n        reconcile_at(&home, 7).unwrap();\n        assert!(list(&home).iter().all(|row| row.adapter.is_none()));\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\n        register_adapter(&home, &source, Some(\"cspt\"), 8);\n        reconcile_at(&home, 9).unwrap();\n        let reactivated = list(&home).into_iter().find(|row| row.adapter.is_some()).unwrap();\n        assert_eq!(reactivated.served_name, \"cspt~1\");\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\n\n        // A vanished registration is retired too; no cleanup deletes output.\n        std::fs::remove_file(home.join(\"adapters/reports/record.toml\")).unwrap();\n        reconcile_at(&home, 10).unwrap();\n        assert!(list(&home).iter().all(|row| row.adapter.is_none()));\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\n        register_adapter(&home, &source, None, 11);\n        reconcile_at(&home, 12).unwrap();\n        let facet_only = list(&home).into_iter().find(|row| row.adapter.is_some()).unwrap();\n        assert_eq!(entry_url(\"node\", 5474, &facet_only), \"http://localhost:5474/node/a/reports/\");\n        assert_eq!(alias_url(\"node\", 5474, &facet_only), None);\n    }\n\n    // [unit->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\n    #[test]\n    fn input_reference_tilde_resolves_on_the_owners_machine() {\n        let _env = crate::test_home::env_lock();\n        let tmp = tempfile::tempdir().unwrap();\n        let user_home = tmp.path().join(\"human\");\n        std::fs::create_dir_all(&user_home).unwrap();\n        let source = user_home.join(\"report.txt\");\n        std::fs::write(&source, \"owner's live file\").unwrap();\n        let key = if cfg!(windows) { \"USERPROFILE\" } else { \"HOME\" };\n        let previous = std::env::var_os(key);\n        std::env::set_var(key, &user_home);\n        let result = std::panic::catch_unwind(|| apply_at(\n            &tmp.path().join(\"spt\"),\n            input_reference(Path::new(\"~/report.txt\"), \"r1\", \"receiver\", 100),\n            100,\n        ));\n        match previous {\n            Some(value) => std::env::set_var(key, value),\n            None => std::env::remove_var(key),\n        }\n        let ServeResult::Entry { entry } = result.unwrap().unwrap() else {\n            panic!(\"an existing owner-home file must register\");\n        };\n        assert_eq!(std::fs::read_to_string(&entry.path).unwrap(), \"owner's live file\");\n        assert_eq!(entry.audience.as_deref(), Some(\"receiver\"));\n        assert_eq!(entry.ttl_ms, Some(spt_store::serving::HELPER_ENTRY_TTL_MS));\n    }\n}"}],"details":{"totalLines":851,"displayContent":{"text":"pub fn port_from_status(result: ServeResult) -> io::Result<u16> {\n    match result {\r\n        ServeResult::DocsStatus { port: Some(port) } if port != 0 => Ok(port),\r\n        ServeResult::DocsStatus { .. } => Err(io::Error::other(\r\n            \"DOCS_LISTENER_UNAVAILABLE: the running daemon has no bound docs listener\",\r\n        )),\r\n        ServeResult::Refused { error } => Err(io::Error::other(error)),\r\n        _ => Err(io::Error::other(\r\n            \"DOCS_LISTENER_UNREPORTED: the running daemon did not report its bound docs port\",\r\n        )),\r\n    }\r\n}\r\n\r\n/// Dispatch only after the local control transport's version handshake.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub(crate) fn handle(payload: serde_json::Value) -> Envelope {\r\n    let result = serde_json::from_value(payload)\r\n        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))\r\n        .and_then(|request| {\r\n            let now_ms = std::time::SystemTime::now()\r\n                .duration_since(std::time::UNIX_EPOCH)\r\n                .map_err(io::Error::other)?\r\n                .as_millis()\r\n                .try_into()\r\n                .map_err(io::Error::other)?;\r\n            apply_at(&spt_store::perch::spt_home(), request, now_ms)\r\n        })\r\n        .unwrap_or_else(|e| ServeResult::Refused { error: e.to_string() });\r\n    Envelope::new(\r\n        KIND_SERVE_RESULT,\r\n        serde_json::to_value(result).expect(\"serving result serializes\"),\r\n    )\r\n}\r\n\r\n/// Call the daemon, bounded because an older daemon ignores unknown control\r\n/// kinds. A timeout is an unconfirmed outcome, never permission to retry a write.\r\n// [impl->REQ-WEB-SERVING-REGISTRY]\r\npub fn call(name: &str, request: ServeRequest) -> io::Result<ServeResult> {\r\n    call_with_timeout(name, request, std::time::Duration::from_secs(10))\r\n}\r\n\r\n/// [`call`] with an explicit bound. The turn-boundary now-signal poll uses a\r\n/// SHORT one: a wedged daemon must cost that hook milliseconds, not the write\r\n/// path's ten seconds, and absence is silence there rather than an error.\r\n// [impl->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\npub fn call_with_timeout(\r\n    name: &str,\r\n    request: ServeRequest,\r\n    timeout: std::time::Duration,\r\n) -> io::Result<ServeResult> {\r\n    let name = name.to_owned();\r\n    let (tx, rx) = std::sync::mpsc::sync_channel(1);\r\n    std::thread::spawn(move || {\r\n        let result = (|| {\r\n            let mut conn = LocalSocketTransport::connect(&name)?;\r\n            send_hello(&mut conn, Role::Brain)?;\r\n            write_frame(\r\n                &mut conn,\r\n                &Envelope::new(\r\n                    KIND_SERVE_REQUEST,\r\n                    serde_json::to_value(request).expect(\"serving request serializes\"),\r\n                ),\r\n            )?;\r\n            let reply = read_frame(&mut conn)?;\r\n            if reply.kind != KIND_SERVE_RESULT {\r\n                return Err(io::Error::new(\r\n                    io::ErrorKind::InvalidData,\r\n                    format!(\"SERVE_UNEXPECTED_REPLY: {}\", reply.kind),\r\n                ));\r\n            }\r\n            serde_json::from_value(reply.payload)\r\n                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))\r\n        })();\r\n        let _ = tx.send(result);\r\n    });\r\n    rx.recv_timeout(timeout)\r\n        .map_err(|e| match e {\r\n            std::sync::mpsc::RecvTimeoutError::Timeout => io::Error::new(\r\n                io::ErrorKind::TimedOut,\r\n                \"SERVE_UNCONFIRMED: daemon did not answer; inspect serve list before retrying; update/restart an older daemon\",\r\n            ),\r\n            std::sync::mpsc::RecvTimeoutError::Disconnected => {\r\n                io::Error::other(\"SERVE_UNCONFIRMED: control worker disconnected\")\r\n            }\r\n        })?\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    fn add(path: &Path) -> ServeRequest {\r\n        ServeRequest::Add { path: path.to_owned(), as_name: None, origin: Some(\"alice\".into()) }\r\n    }\r\n\r\n    fn input_reference(path: &Path, receipt: &str, audience: &str, received_at_ms: u64) -> ServeRequest {\r\n        ServeRequest::AddInputReference {\r\n            path: path.to_owned(),\r\n            receipt_id: receipt.into(),\r\n            audience: audience.into(),\r\n            received_at_ms,\r\n        }\r\n    }\r\n\r\n    #[test]\r\n    fn input_reference_missing_is_a_silent_skip_without_registry_publication() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let result = apply_at(&home, input_reference(&tmp.path().join(\"missing\"), \"r1\", \"agent\", 1), 1)\r\n            .unwrap();\r\n        assert!(matches!(result, ServeResult::Missing));\r\n        assert!(!spt_store::perch::serving_registry_file_in(&home).exists());\r\n    }\r\n\r\n    #[test]\r\n    fn input_reference_replay_preserves_the_live_entry_and_deadline() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"report.txt\");\r\n        std::fs::write(&source, b\"before\").unwrap();\r\n        let ServeResult::Entry { entry } = apply_at(&home, input_reference(&source, \"r1\", \"agent\", 10), 20)\r\n            .unwrap() else { panic!(\"expected reference\") };\r\n        let ServeResult::Entry { entry: replay } =\r\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", 50), 50).unwrap()\r\n            else { panic!(\"expected replay\") };\r\n        assert_eq!(replay, entry);\r\n        // A second submission can reuse the same narrow exposure, not renew it.\r\n        let ServeResult::Entry { entry: second } =\r\n            apply_at(&home, input_reference(&source, \"r2\", \"agent\", 60), 60).unwrap()\r\n            else { panic!(\"expected shared live reference\") };\r\n        assert_eq!(second, entry);\r\n        assert_eq!(list(&home), vec![entry]);\r\n    }\r\n\r\n    #[test]\r\n    fn input_reference_cannot_rescope_another_audience_or_unrelated_entry() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"report.txt\");\r\n        let ordinary = tmp.path().join(\"ordinary.txt\");\r\n        std::fs::write(&source, b\"scoped\").unwrap();\r\n        std::fs::write(&ordinary, b\"ordinary\").unwrap();\r\n        apply_at(&home, input_reference(&source, \"r1\", \"agent\", 10), 10).unwrap();\r\n        apply_at(&home, add(&ordinary), 10).unwrap();\r\n        let before = list(&home);\r\n        for request in [\r\n            input_reference(&source, \"r1\", \"other\", 20),\r\n            input_reference(&ordinary, \"r2\", \"agent\", 20),\r\n        ] {\r\n            let error = apply_at(&home, request, 20).unwrap_err();\r\n            assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);\r\n            assert!(error.to_string().contains(\"INPUT_PATH_SCOPE_CONFLICT\"));\r\n        }\r\n        assert_eq!(list(&home), before);\r\n    }\r\n\r\n    #[test]\r\n    fn input_reference_expiry_cannot_be_renewed_by_replay_before_or_after_reaping() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"report.txt\");\r\n        std::fs::write(&source, b\"live\").unwrap();\r\n        let request = input_reference(&source, \"r1\", \"agent\", 10);\r\n        let ServeResult::Entry { entry } = apply_at(&home, request.clone(), 10).unwrap()\r\n            else { panic!(\"expected reference\") };\r\n        let expires = 10 + spt_store::serving::HELPER_ENTRY_TTL_MS;\r\n        assert!(apply_at(&home, request.clone(), expires).unwrap_err().to_string()\r\n            .contains(\"INPUT_PATH_EXPIRED\"));\r\n        apply_at(&home, ServeRequest::Remove { name_or_id: entry.id }, expires).unwrap();\r\n        assert!(apply_at(&home, request, expires).unwrap_err().to_string()\r\n            .contains(\"INPUT_PATH_EXPIRED\"));\r\n        let ServeResult::Entry { entry: renewed } =\r\n            apply_at(&home, input_reference(&source, \"r2\", \"agent\", expires), expires).unwrap()\r\n            else { panic!(\"a new receipt may register after expiry\") };\r\n        assert_eq!(renewed.registered_at_ms, expires);\r\n        let next_expiry = expires + spt_store::serving::HELPER_ENTRY_TTL_MS;\r\n        let ServeResult::Entry { entry: replaced } =\r\n            apply_at(&home, input_reference(&source, \"r3\", \"agent\", next_expiry), next_expiry).unwrap()\r\n            else { panic!(\"a fresh receipt must not wait for the periodic reaper\") };\r\n        assert_ne!(replaced.id, renewed.id);\r\n        assert_eq!(list(&home), vec![replaced]);\r\n    }\r\n\r\n    #[test]\r\n    fn input_reference_clock_skew_never_extends_either_nodes_lifetime() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"report.txt\");\r\n        std::fs::write(&source, b\"live\").unwrap();\r\n        let ServeResult::Entry { entry } =\r\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", 500), 100).unwrap()\r\n            else { panic!(\"a future remote clock does not refuse a live source\") };\r\n        assert_eq!(entry.registered_at_ms, 100);\r\n        let ServeResult::Entry { entry: replay } =\r\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", 500), 600).unwrap()\r\n            else { panic!(\"expected unchanged replay\") };\r\n        assert_eq!(replay, entry);\r\n    }\r\n\r\n    #[test]\r\n    fn input_reference_http_reads_observe_edits_and_deletion() {\r\n        use http_body_util::BodyExt;\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"report.txt\");\r\n        std::fs::write(&source, b\"before\").unwrap();\r\n        let now = crate::brain::now_ms();\r\n        let ServeResult::Entry { entry } =\r\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", now), now).unwrap()\r\n            else { panic!(\"expected reference\") };\r\n        let uri = format!(\"/local/f/{}\", entry.served_name);\r\n        let get = || crate::webserve::handle_path(&home, &home.join(\"docs\"), \"LOCAL\", &uri, None, false);\r\n        let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();\r\n        assert_eq!(runtime.block_on(get().into_body().collect()).unwrap().to_bytes().as_ref(), b\"before\");\r\n        std::fs::write(&source, b\"edited\").unwrap();\r\n        assert_eq!(runtime.block_on(get().into_body().collect()).unwrap().to_bytes().as_ref(), b\"edited\");\r\n        std::fs::remove_file(&source).unwrap();\r\n        assert_eq!(get().status(), hyper::StatusCode::NOT_FOUND);\r\n        assert!(matches!(\r\n            apply_at(&home, input_reference(&source, \"r1\", \"agent\", now), now).unwrap(),\r\n            ServeResult::Missing,\r\n        ));\r\n    }\r\n\r\n    /// LanStatus is a QUESTION: it writes nothing, and it does not re-run the gate.\r\n    ///\r\n    /// Both halves are load-bearing, and both are asserted against a home the\r\n    /// gate would REFUSE. The write half: this request is answered before the\r\n    /// registry writer lock, so asking must not create the serving registry —\r\n    /// a turn-boundary hook that polls every turn would otherwise mint a file\r\n    /// on a node that never served anything. The gate half: the discriminator\r\n    /// is the ANSWER SHAPE. `set-not-applied` is what this empty home earns\r\n    /// from the set gate, so a `LanRefused` here would prove the status query\r\n    /// had run it; `LanStopped { NOT_UP }` is the read of process state that a\r\n    /// question is allowed to make. Asked twice for the same reason — a side\r\n    /// effect that only shows on the second ask is still a side effect.\r\n    // [unit->REQ-WEB-LAN-BOOTSTRAP-LISTENER]\r\n    #[test]\r\n    fn a_lan_status_writes_nothing_and_does_not_run_the_set_gate() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let registry = spt_store::perch::serving_registry_file_in(&home);\r\n\r\n        for ask in 1..=2 {\r\n            let answer = apply_at(&home, ServeRequest::LanStatus, ask).unwrap();\r\n            match answer {\r\n                ServeResult::LanStopped { ref code } => assert_eq!(\r\n                    code.as_deref(),\r\n                    Some(crate::lanhost::NOT_UP),\r\n                    \"down answers by name, ask {ask}\"\r\n                ),\r\n                other => panic!(\r\n                    \"ask {ask}: a status query answered {other:?} — a LanRefused would mean it ran the gate\"\r\n                ),\r\n            }\r\n            assert!(\r\n                !registry.exists(),\r\n                \"ask {ask} created the serving registry: a question must not take the writer path\"\r\n            );\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\n    #[test]\r\n    fn unavailable_registry_lock_cannot_publish_an_add() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"report.md\");\r\n        std::fs::write(&source, \"must not be registered\").unwrap();\r\n        let registry_path = spt_store::perch::serving_registry_file_in(&home);\r\n        // A directory at the sentinel path deterministically refuses acquisition\r\n        // on both supported platforms, without permission or timing assumptions.\r\n        let mut lock_path = registry_path.as_os_str().to_os_string();\r\n        lock_path.push(\".lock\");\r\n        std::fs::create_dir_all(PathBuf::from(lock_path)).unwrap();\r\n\r\n        assert!(apply_at(&home, add(&source), 1).is_err());\r\n        assert!(!registry_path.exists(), \"a failed lock must not permit publication\");\r\n    }\r\n\r\n    // [unit->REQ-WEB-SERVING-REGISTRY]\r\n    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\n    #[test]\r\n    fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let first = tmp.path().join(\"first\");\r\n        let second = tmp.path().join(\"second\");\r\n        std::fs::create_dir_all(&first).unwrap();\r\n        std::fs::create_dir_all(&second).unwrap();\r\n        let a = first.join(\"report.md\");\r\n        let b = second.join(\"report.md\");\r\n        std::fs::write(&a, \"first\").unwrap();\r\n        std::fs::write(&b, \"second\").unwrap();\r\n        std::thread::scope(|scope| {\r\n            let one = scope.spawn(|| apply_at(&home, add(&a), 1).unwrap());\r\n            let two = scope.spawn(|| apply_at(&home, add(&b), 2).unwrap());\r\n            assert!(matches!(one.join().unwrap(), ServeResult::Entry { .. }));\r\n            assert!(matches!(two.join().unwrap(), ServeResult::Entry { .. }));\r\n        });\r\n        let ServeResult::Listed { entries } = apply_at(&home, ServeRequest::List, 3).unwrap() else {\r\n            panic!(\"list did not return entries\");\r\n        };\r\n        let paths: std::collections::BTreeSet<_> = entries.iter().map(|e| &e.path).collect();\r\n        assert_eq!(paths, [&a, &b].into_iter().collect());\r\n        let names: std::collections::BTreeSet<_> = entries.iter().map(|e| e.served_name.as_str()).collect();\r\n        assert_eq!(names, [\"report.md\", \"report~1.md\"].into_iter().collect());\r\n    }\r\n\r\n    /// `AddScoped` is the FILE_ACCESS_HELPER's registration, and it had no cell\r\n    /// at all — neither for the file case nor this one. A quoted path can name a\r\n    /// DIRECTORY, and the helper must register it as one: the two halves (add a\r\n    /// reference, then narrow it) happen under one lock precisely so no window\r\n    /// exists in which the directory is exposed and not yet addressed to anyone.\r\n    ///\r\n    /// The kind is the part worth asserting. If a directory silently registered\r\n    /// as a `File` the surface would serve nothing and the failure would look\r\n    /// like a missing file rather than a wrong kind.\r\n    // [unit->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER]\r\n    #[test]\r\n    fn a_quoted_directory_registers_as_a_narrowed_directory_entry() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let quoted = tmp.path().join(\"shared\");\r\n        std::fs::create_dir_all(quoted.join(\"nested\")).unwrap();\r\n        std::fs::write(quoted.join(\"index.html\"), \"an index\").unwrap();\r\n\r\n        let request = ServeRequest::AddScoped {\r\n            path: quoted.clone(),\r\n            as_name: None,\r\n            origin: Some(\"ABCDEFGH\".into()),\r\n            ttl_ms: spt_store::serving::HELPER_ENTRY_TTL_MS,\r\n            audience: \"todlando\".into(),\r\n        };\r\n        let ServeResult::Entry { entry } = apply_at(&home, request, 1).unwrap() else {\r\n            panic!(\"AddScoped did not return an entry\");\r\n        };\r\n\r\n        assert_eq!(entry.kind, spt_store::serving::ServedKind::Dir, \"a quoted directory is a DIR entry\");\r\n        assert_eq!(entry.path, quoted);\r\n        assert_eq!(entry.audience.as_deref(), Some(\"todlando\"), \"addressed to the one endpoint\");\r\n        assert_eq!(entry.ttl_ms, Some(spt_store::serving::HELPER_ENTRY_TTL_MS), \"the helper's 24h lifetime\");\r\n        assert_eq!(entry.origin.as_deref(), Some(\"ABCDEFGH\"), \"origin is the message short-ID\");\r\n\r\n        // And it PERSISTED narrowed — the add and the scope are one locked pass,\r\n        // so a reader must never observe the un-narrowed half.\r\n        let ServeResult::Listed { entries } = apply_at(&home, ServeRequest::List, 2).unwrap() else {\r\n            panic!(\"list did not return entries\");\r\n        };\r\n        assert_eq!(entries.len(), 1);\r\n        assert_eq!(entries[0].audience.as_deref(), Some(\"todlando\"));\r\n        assert_eq!(entries[0].kind, spt_store::serving::ServedKind::Dir);\r\n    }\r\n\r\n    // [unit->REQ-WEB-SERVING-REGISTRY]\r\n    #[test]\r\n    fn removal_persists_and_does_not_delete_reference_bytes() {\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let file = tmp.path().join(\"report.md\");\r\n        std::fs::write(&file, \"keep me\").unwrap();\r\n        let ServeResult::Entry { entry } = apply_at(&home, add(&file), 1).unwrap() else {\r\n            panic!(\"add did not return an entry\");\r\n        };\r\n        apply_at(&home, ServeRequest::Remove { name_or_id: entry.id }, 2).unwrap();\r\n        assert_eq!(std::fs::read(&file).unwrap(), b\"keep me\");\r\n        let ServeResult::Entry { entry } = apply_at(&home, add(&file), 3).unwrap() else {\r\n            panic!(\"re-add did not return an entry\");\r\n        };\r\n        assert_eq!(entry.served_name, \"report.md\");\r\n    }\r\n\r\n    fn register_adapter(home: &Path, source: &Path, alias: Option<&str>, now_ms: u64) {\r\n        std::fs::create_dir_all(source).unwrap();\r\n        let mut manifest = String::from(\r\n            \"[adapter]\\nname = 'reports'\\nversion = '1'\\nmin_spt_core_version = '0'\\n\"\r\n        );\r\n        if let Some(alias) = alias {\r\n            manifest.push_str(&format!(\"web_short_path = {alias:?}\\n\"));\r\n        }\r\n        manifest.push_str(\"\\n[profiles.work.adapter]\\nweb_short_path = 'profile-alias'\\n\");\r\n        std::fs::write(source.join(\"manifest.toml\"), manifest).unwrap();\r\n        spt_runtime::registry::register(&home.join(\"adapters\"), source, now_ms).unwrap();\r\n    }\r\n\r\n    fn list(home: &Path) -> Vec<ServedEntry> {\r\n        let ServeResult::Listed { entries } = apply_at(home, ServeRequest::List, 0).unwrap() else {\r\n            panic!(\"list did not return entries\");\r\n        };\r\n        entries\r\n    }\r\n\r\n    // [unit->REQ-WEB-SERVING-REGISTRY] [unit->REQ-WEB-URL-NODE-PREFIX]\r\n    #[test]\r\n    fn adapter_lifecycle_has_one_root_and_retains_output_and_live_assignments() {\r\n        use spt_store::serving::{alias_url, entry_url, ServedKind};\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let home = tmp.path().join(\"home\");\r\n        let source = tmp.path().join(\"install\");\r\n        register_adapter(&home, &source, Some(\"cspt\"), 1);\r\n        let root = home.join(\"adapters\").join(\"reports\").join(\"web\");\r\n        let output = root.join(\"index.html\");\r\n        // Output can be produced by install before the daemon has ever run.\r\n        std::fs::write(&output, b\"retained output\").unwrap();\r\n        let collision = tmp.path().join(\"cspt\");\r\n        std::fs::write(&collision, b\"explicit reference\").unwrap();\r\n        apply_at(&home, add(&collision), 2).unwrap();\r\n        reconcile_at(&home, 3).unwrap();\r\n        let entries = list(&home);\r\n        assert_eq!(entries.len(), 2);\r\n        let entry = entries.iter().find(|entry| entry.adapter.is_some()).unwrap().clone();\r\n        assert_eq!(entry.path, root);\r\n        assert_eq!(entry.kind, ServedKind::Dir);\r\n        assert_eq!(entry_url(\"node\", 5474, &entry), \"http://localhost:5474/node/a/reports/\");\r\n        assert_eq!(alias_url(\"node\", 5474, &entry).as_deref(), Some(\"http://localhost:5474/node/cspt~1/\"));\r\n        assert!(!entry.path.join(\"manifest.toml\").exists());\r\n        assert!(source.join(\"manifest.toml\").is_file());\r\n        assert!(spt_runtime::registry::resolve_option(&home.join(\"adapters\"), \"reports:work\").is_ok());\r\n\r\n        // An update, a reconcile, and a restart preserve the live URL even if\r\n        // the newly declared alias differs. Profiles never create another row.\r\n        register_adapter(&home, &source, Some(\"changed\"), 4);\r\n        apply_at(&home, ServeRequest::Reconcile, 5).unwrap();\r\n        reconcile_at(&home, 6).unwrap();\r\n        assert_eq!(list(&home).iter().find(|row| row.adapter.is_some()), Some(&entry));\r\n        assert_eq!(list(&home).len(), 2);\r\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\r\n\r\n        spt_runtime::registry::deregister(&home.join(\"adapters\"), \"reports\").unwrap();\r\n        reconcile_at(&home, 7).unwrap();\r\n        assert!(list(&home).iter().all(|row| row.adapter.is_none()));\r\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\r\n        register_adapter(&home, &source, Some(\"cspt\"), 8);\r\n        reconcile_at(&home, 9).unwrap();\r\n        let reactivated = list(&home).into_iter().find(|row| row.adapter.is_some()).unwrap();\r\n        assert_eq!(reactivated.served_name, \"cspt~1\");\r\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\r\n\r\n        // A vanished registration is retired too; no cleanup deletes output.\r\n        std::fs::remove_file(home.join(\"adapters/reports/record.toml\")).unwrap();\r\n        reconcile_at(&home, 10).unwrap();\r\n        assert!(list(&home).iter().all(|row| row.adapter.is_none()));\r\n        assert_eq!(std::fs::read(&output).unwrap(), b\"retained output\");\r\n        register_adapter(&home, &source, None, 11);\r\n        reconcile_at(&home, 12).unwrap();\r\n        let facet_only = list(&home).into_iter().find(|row| row.adapter.is_some()).unwrap();\r\n        assert_eq!(entry_url(\"node\", 5474, &facet_only), \"http://localhost:5474/node/a/reports/\");\r\n        assert_eq!(alias_url(\"node\", 5474, &facet_only), None);\r\n    }\r\n\r\n    // [unit->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\r\n    #[test]\r\n    fn input_reference_tilde_resolves_on_the_owners_machine() {\r\n        let _env = crate::test_home::env_lock();\r\n        let tmp = tempfile::tempdir().unwrap();\r\n        let user_home = tmp.path().join(\"human\");\r\n        std::fs::create_dir_all(&user_home).unwrap();\r\n        let source = user_home.join(\"report.txt\");\r\n        std::fs::write(&source, \"owner's live file\").unwrap();\r\n        let key = if cfg!(windows) { \"USERPROFILE\" } else { \"HOME\" };\r\n        let previous = std::env::var_os(key);\r\n        std::env::set_var(key, &user_home);\r\n        let result = std::panic::catch_unwind(|| apply_at(\r\n            &tmp.path().join(\"spt\"),\r\n            input_reference(Path::new(\"~/report.txt\"), \"r1\", \"receiver\", 100),\r\n            100,\r\n        ));\r\n        match previous {\r\n            Some(value) => std::env::set_var(key, value),\r\n            None => std::env::remove_var(key),\r\n        }\r\n        let ServeResult::Entry { entry } = result.unwrap().unwrap() else {\r\n            panic!(\"an existing owner-home file must register\");\r\n        };\r\n        assert_eq!(std::fs::read_to_string(&entry.path).unwrap(), \"owner's live file\");\r\n        assert_eq!(entry.audience.as_deref(), Some(\"receiver\"));\r\n        assert_eq!(entry.ttl_ms, Some(spt_store::serving::HELPER_ENTRY_TTL_MS));\r\n    }\r\n}\r","startLine":373,"lineNumbers":[373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851]},"fileSize":40844,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\src\\servehost.rs"}}},"isError":false,"timestamp":1789522914346}}
{"type":"message","id":"13ea122a","parentId":"06f7f816","timestamp":"2026-09-16T01:41:54.387Z","message":{"role":"toolResult","toolCallId":"call_enszuFAoJslQCLp2koOhaxNh|fc_0cdf94810c888815016aa9f3e2435887d1a67be3cf2a332343","toolName":"read","content":[{"type":"text","text":"fn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\n    for id in perch::list_self_perch_ids(owlery) {\n…\n        let Ok(entries) = std::fs::read_dir(&perch) else {\n…\n        };\n        for entry in entries.flatten() {\n            let name = entry.file_name();\n            let name = name.to_string_lossy();\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\n                continue;\n            }\n            let dir = entry.path();\n            if !dir.is_dir() {\n                continue;\n            }\n            if remove(&dir).is_ok() {\n                continue;\n            }\n            // Still image-locked. An already-displaced `.old*` just waits for the next\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\n            // perch no longer carries the stale own-copy in place. NEVER abort.\n            if name == \".live-bin\" {\n                let displaced = fresh_live_bin_old(&dir);\n                match std::fs::rename(&dir, &displaced) {\n                    Ok(()) => {\n                        let _ = remove(&displaced);\n                    }\n                    Err(e) => spt_proto::emit_line_err!(\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\n                    ),\n                }\n            }\n        }\n    }\n}\n\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\n    let base = live_bin.with_file_name(\".live-bin.old\");\n    if !base.exists() {\n        return base;\n    }\n    for n in 1u64.. {\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\n        if !candidate.exists() {\n            return candidate;\n        }\n    }\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\n}\n\n/// Delete every EXPIRED attachment snapshot and its registry entry together, on\n/// the live host's existing tick.\n///\n/// Serve-time masking already refuses an expired entry, so this is not what\n/// stops an expired link from working — it is what stops\n/// `$SPT_HOME/serve/snapshots/` from growing without bound, which masking alone\n/// never does.\n///\n/// **The order is deliberate: bytes first, then the entry.** Neither step is\n/// atomic with the other, so the question is which crash heals. Deleting the\n/// file and then failing to save leaves an expired entry naming a file that is\n/// gone: the next tick finds it expired again and `remove_file` is idempotent,\n/// so the pair completes on its own. Saving first and then failing to delete\n/// orphans the bytes with no entry left to name them, and no later tick can\n/// repair that — it is exactly the unbounded growth this exists to prevent.\n///\n/// **Only a snapshot is ever deleted.** `ServedKind::Attachment` marks an entry\n/// whose `path` this node minted into its own snapshot store; a `File` or `Dir`\n/// entry's `path` is the USER'S OWN file, which a reaper must never touch. The\n/// containment check is belt-and-braces on that same point: a hand-edited or\n/// corrupted registry must not be able to aim a delete at an arbitrary path.\n// [impl->REQ-WEB-ATTACHMENT-PULL]\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\nfn reap_expired_attachments() {\n    let registry_path = perch::serving_registry_file();\n    if !registry_path.exists() {\n        return; // nothing has ever been served on this node\n    }\n    // The broker is a DIFFERENT process. Hold the shared sentinel from before\n    // loading until snapshot cleanup and publication have both finished.\n    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\n        Ok(writer) => writer,\n        Err(err) => {\n            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n            return; // never reap outside the lock\n        }\n    };\n    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n        Ok(registry) => registry,\n        Err(err) => {\n            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n            return;\n        }\n    };\n    let reaped = registry.reap_expired(now_ms());\n    if reaped.is_empty() {\n        return;\n    }\n    let snapshots = perch::serve_snapshots_dir();\n    let mut bytes_removed: u64 = 0;\n    for entry in &reaped {\n        if entry.kind != spt_store::serving::ServedKind::Attachment {\n            continue; // a reference entry's path is the user's own file\n        }\n        if !entry.path.starts_with(&snapshots) {\n            eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n            continue;\n        }\n        let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n        match std::fs::remove_file(&entry.path) {\n            Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n            Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n        }\n    }\n    if let Err(err) = registry.save_at(&registry_path) {\n        // The bytes are gone and the entries are not. Say so rather than\n        // reporting a reap that only half happened; the next tick retries.\n        eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n        return;\n    }\n    eprintln!(\n        \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n        reaped.len(),\n        bytes_removed\n    );\n}\n\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\n/// config are re-read each sweep (adapter / pulse-period changes land between\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\n/// for symmetry — the threads die with the brain process and the next brain\n/// rehydrates from disk.\n// [impl->REQ-DAEMON-1]\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\n    std::thread::spawn(move || {\n        let set = Arc::new(LiveSet::new());\n        {\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\n            // residency machinery's corpse-burial, not its resurrection.\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\n            // running FROM the own-copy image is killed first (its dir then unlocks).\n            gc_live_bin_dirs(&owlery);\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\n            // (its sessions are live, not orphaned). Belts inside the gate.\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\n        }\n        let mut session_poll = SessionPoll::default();\n        while !stop.load(Ordering::SeqCst) {\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\n            // unset = normal production, untouched): skip the reconcile poll+body so an\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\n            // reap-driver in ISOLATION. This loop's own `SessionPoll::query`\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\n                std::thread::sleep(Duration::from_millis(100));\n                continue;\n            }\n            let owlery = perch::owlery_dir();\n            let adapters_dir = perch::adapters_dir();\n            let registered = spt_runtime::registry::registered(&adapters_dir);\n            let cfg = DaemonConfig::load();\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\n            // latch for any spt-hosted endpoint whose broker session is gone — and\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\n            // so the same-tick stop-side un-hosts the now-offline perch and the\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\n            // cold start after an unclean stop from spawning N phantom Psyches for N\n            // dead-but-latched perches. Skipped only when the broker is unreachable\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\n            // run_brain connects to the broker BEFORE this host spawns, so the query\n            // returns the REAL session set: empty at a cold start (every online\n            // controllable perch is a phantom → offlined), the surviving sessions at\n            // a brain restart (live agents stay, only the dead are offlined). The\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\n            if let Some(live) = session_poll.query(&crate::endpoint::broker_socket_name()) {\n                reconcile_hosted_liveness(&owlery, &live);\n            }\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\n            // W2: the attachment TTL reaper rides this same 5 s pulse — snapshot\n            // bytes and the entry that names them are retired together, so an\n            // expired attachment stops costing disk as well as stops serving.\n            // [impl->REQ-WEB-ATTACHMENT-PULL]\n            reap_expired_attachments();\n            // Sleep in slices so a stop lands promptly.\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\n            while left > 0 && !stop.load(Ordering::SeqCst) {\n                let step = left.min(100);\n                std::thread::sleep(Duration::from_millis(step));\n…\n            }\n        }\n    })\n}\n\n[Showing lines 1209-1423 of 2915. Use :1424 to continue]"}],"details":{"truncation":{"content":"        };\r\n        for entry in entries.flatten() {\r\n            let name = entry.file_name();\r\n            let name = name.to_string_lossy();\r\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\r\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\r\n                continue;\r\n            }\r\n            let dir = entry.path();\r\n            if !dir.is_dir() {\r\n                continue;\r\n            }\r\n            if remove(&dir).is_ok() {\r\n                continue;\r\n            }\r\n            // Still image-locked. An already-displaced `.old*` just waits for the next\r\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\r\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\r\n            // perch no longer carries the stale own-copy in place. NEVER abort.\r\n            if name == \".live-bin\" {\r\n                let displaced = fresh_live_bin_old(&dir);\r\n                match std::fs::rename(&dir, &displaced) {\r\n                    Ok(()) => {\r\n                        let _ = remove(&displaced);\r\n                    }\r\n                    Err(e) => spt_proto::emit_line_err!(\r\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\r\n                    ),\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\r\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\r\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\r\n    let base = live_bin.with_file_name(\".live-bin.old\");\r\n    if !base.exists() {\r\n        return base;\r\n    }\r\n    for n in 1u64.. {\r\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\r\n        if !candidate.exists() {\r\n            return candidate;\r\n        }\r\n    }\r\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\r\n}\r\n\r\n/// Delete every EXPIRED attachment snapshot and its registry entry together, on\r\n/// the live host's existing tick.\r\n///\r\n/// Serve-time masking already refuses an expired entry, so this is not what\r\n/// stops an expired link from working — it is what stops\r\n/// `$SPT_HOME/serve/snapshots/` from growing without bound, which masking alone\r\n/// never does.\r\n///\r\n/// **The order is deliberate: bytes first, then the entry.** Neither step is\r\n/// atomic with the other, so the question is which crash heals. Deleting the\r\n/// file and then failing to save leaves an expired entry naming a file that is\r\n/// gone: the next tick finds it expired again and `remove_file` is idempotent,\r\n/// so the pair completes on its own. Saving first and then failing to delete\r\n/// orphans the bytes with no entry left to name them, and no later tick can\r\n/// repair that — it is exactly the unbounded growth this exists to prevent.\r\n///\r\n/// **Only a snapshot is ever deleted.** `ServedKind::Attachment` marks an entry\r\n/// whose `path` this node minted into its own snapshot store; a `File` or `Dir`\r\n/// entry's `path` is the USER'S OWN file, which a reaper must never touch. The\r\n/// containment check is belt-and-braces on that same point: a hand-edited or\r\n/// corrupted registry must not be able to aim a delete at an arbitrary path.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\nfn reap_expired_attachments() {\r\n    let registry_path = perch::serving_registry_file();\r\n    if !registry_path.exists() {\r\n        return; // nothing has ever been served on this node\r\n    }\r\n    // The broker is a DIFFERENT process. Hold the shared sentinel from before\r\n    // loading until snapshot cleanup and publication have both finished.\r\n    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\r\n        Ok(writer) => writer,\r\n        Err(err) => {\r\n            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\r\n            return; // never reap outside the lock\r\n        }\r\n    };\r\n    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\r\n        Ok(registry) => registry,\r\n        Err(err) => {\r\n            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\r\n            return;\r\n        }\r\n    };\r\n    let reaped = registry.reap_expired(now_ms());\r\n    if reaped.is_empty() {\r\n        return;\r\n    }\r\n    let snapshots = perch::serve_snapshots_dir();\r\n    let mut bytes_removed: u64 = 0;\r\n    for entry in &reaped {\r\n        if entry.kind != spt_store::serving::ServedKind::Attachment {\r\n            continue; // a reference entry's path is the user's own file\r\n        }\r\n        if !entry.path.starts_with(&snapshots) {\r\n            eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\r\n            continue;\r\n        }\r\n        let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\r\n        match std::fs::remove_file(&entry.path) {\r\n            Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\r\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\r\n            Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\r\n        }\r\n    }\r\n    if let Err(err) = registry.save_at(&registry_path) {\r\n        // The bytes are gone and the entries are not. Say so rather than\r\n        // reporting a reap that only half happened; the next tick retries.\r\n        eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\r\n        return;\r\n    }\r\n    eprintln!(\r\n        \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\r\n        reaped.len(),\r\n        bytes_removed\r\n    );\r\n}\r\n\r\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\r\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\r\n/// config are re-read each sweep (adapter / pulse-period changes land between\r\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\r\n/// for symmetry — the threads die with the brain process and the next brain\r\n/// rehydrates from disk.\r\n// [impl->REQ-DAEMON-1]\r\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        let set = Arc::new(LiveSet::new());\r\n        {\r\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\r\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\r\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\r\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\r\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\r\n            // residency machinery's corpse-burial, not its resurrection.\r\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\r\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\r\n            // running FROM the own-copy image is killed first (its dir then unlocks).\r\n            gc_live_bin_dirs(&owlery);\r\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\r\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\r\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\r\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\r\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\r\n            // (its sessions are live, not orphaned). Belts inside the gate.\r\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n        }\r\n        let mut session_poll = SessionPoll::default();\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\r\n            // unset = normal production, untouched): skip the reconcile poll+body so an\r\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\r\n            // reap-driver in ISOLATION. This loop's own `SessionPoll::query`\r\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\r\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\r\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\r\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\r\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\r\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\r\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\r\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\r\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\r\n                std::thread::sleep(Duration::from_millis(100));\r\n                continue;\r\n            }\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            let cfg = DaemonConfig::load();\r\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\r\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\r\n            // latch for any spt-hosted endpoint whose broker session is gone — and\r\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\r\n            // so the same-tick stop-side un-hosts the now-offline perch and the\r\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\r\n            // cold start after an unclean stop from spawning N phantom Psyches for N\r\n            // dead-but-latched perches. Skipped only when the broker is unreachable\r\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\r\n            // run_brain connects to the broker BEFORE this host spawns, so the query\r\n            // returns the REAL session set: empty at a cold start (every online\r\n            // controllable perch is a phantom → offlined), the surviving sessions at\r\n            // a brain restart (live agents stay, only the dead are offlined). The\r\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\r\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\r\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\r\n            if let Some(live) = session_poll.query(&crate::endpoint::broker_socket_name()) {\r\n                reconcile_hosted_liveness(&owlery, &live);\r\n            }\r\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\r\n            // W2: the attachment TTL reaper rides this same 5 s pulse — snapshot\r\n            // bytes and the entry that names them are retired together, so an\r\n            // expired attachment stops costing disk as well as stops serving.\r\n            // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n            reap_expired_attachments();\r\n            // Sleep in slices so a stop lands promptly.\r\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r","truncated":true,"truncatedBy":"lines","totalLines":1707,"totalBytes":11841,"outputLines":215,"outputBytes":11841,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":2915,"displayContent":{"text":"fn gc_live_bin_dirs_with(owlery: &Path, remove: &dyn Fn(&Path) -> std::io::Result<()>) {\n    for id in perch::list_self_perch_ids(owlery) {\n…\n        let Ok(entries) = std::fs::read_dir(&perch) else {\n…\n        };\r\n        for entry in entries.flatten() {\r\n            let name = entry.file_name();\r\n            let name = name.to_string_lossy();\r\n            // The live own-copy dir + any already-displaced `.live-bin.old*` litter.\r\n            if name != \".live-bin\" && !name.starts_with(\".live-bin.old\") {\r\n                continue;\r\n            }\r\n            let dir = entry.path();\r\n            if !dir.is_dir() {\r\n                continue;\r\n            }\r\n            if remove(&dir).is_ok() {\r\n                continue;\r\n            }\r\n            // Still image-locked. An already-displaced `.old*` just waits for the next\r\n            // boot (uniquely named, never a brick); the LIVE `.live-bin` is displaced now\r\n            // (a locked exe can be MOVED even when it can't be unlinked) so the endpoint's\r\n            // perch no longer carries the stale own-copy in place. NEVER abort.\r\n            if name == \".live-bin\" {\r\n                let displaced = fresh_live_bin_old(&dir);\r\n                match std::fs::rename(&dir, &displaced) {\r\n                    Ok(()) => {\r\n                        let _ = remove(&displaced);\r\n                    }\r\n                    Err(e) => spt_proto::emit_line_err!(\r\n                        \"LIVE_BIN_GC_DECLINE:{id}: {e} (image-locked, left in place — retried next boot)\"\r\n                    ),\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// A fresh, non-colliding `.live-bin.old[-N]` sibling of a `.live-bin` dir (mirrors\r\n/// crc_swap's `fresh_old_path` so a still-mapped prior `.old` is never renamed OVER).\r\nfn fresh_live_bin_old(live_bin: &Path) -> PathBuf {\r\n    let base = live_bin.with_file_name(\".live-bin.old\");\r\n    if !base.exists() {\r\n        return base;\r\n    }\r\n    for n in 1u64.. {\r\n        let candidate = live_bin.with_file_name(format!(\".live-bin.old-{n}\"));\r\n        if !candidate.exists() {\r\n            return candidate;\r\n        }\r\n    }\r\n    unreachable!(\"an unbounded search for a free .live-bin.old-N name always terminates\")\r\n}\r\n\r\n/// Delete every EXPIRED attachment snapshot and its registry entry together, on\r\n/// the live host's existing tick.\r\n///\r\n/// Serve-time masking already refuses an expired entry, so this is not what\r\n/// stops an expired link from working — it is what stops\r\n/// `$SPT_HOME/serve/snapshots/` from growing without bound, which masking alone\r\n/// never does.\r\n///\r\n/// **The order is deliberate: bytes first, then the entry.** Neither step is\r\n/// atomic with the other, so the question is which crash heals. Deleting the\r\n/// file and then failing to save leaves an expired entry naming a file that is\r\n/// gone: the next tick finds it expired again and `remove_file` is idempotent,\r\n/// so the pair completes on its own. Saving first and then failing to delete\r\n/// orphans the bytes with no entry left to name them, and no later tick can\r\n/// repair that — it is exactly the unbounded growth this exists to prevent.\r\n///\r\n/// **Only a snapshot is ever deleted.** `ServedKind::Attachment` marks an entry\r\n/// whose `path` this node minted into its own snapshot store; a `File` or `Dir`\r\n/// entry's `path` is the USER'S OWN file, which a reaper must never touch. The\r\n/// containment check is belt-and-braces on that same point: a hand-edited or\r\n/// corrupted registry must not be able to aim a delete at an arbitrary path.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\n// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\nfn reap_expired_attachments() {\r\n    let registry_path = perch::serving_registry_file();\r\n    if !registry_path.exists() {\r\n        return; // nothing has ever been served on this node\r\n    }\r\n    // The broker is a DIFFERENT process. Hold the shared sentinel from before\r\n    // loading until snapshot cleanup and publication have both finished.\r\n    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\r\n        Ok(writer) => writer,\r\n        Err(err) => {\r\n            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\r\n            return; // never reap outside the lock\r\n        }\r\n    };\r\n    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\r\n        Ok(registry) => registry,\r\n        Err(err) => {\r\n            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\r\n            return;\r\n        }\r\n    };\r\n    let reaped = registry.reap_expired(now_ms());\r\n    if reaped.is_empty() {\r\n        return;\r\n    }\r\n    let snapshots = perch::serve_snapshots_dir();\r\n    let mut bytes_removed: u64 = 0;\r\n    for entry in &reaped {\r\n        if entry.kind != spt_store::serving::ServedKind::Attachment {\r\n            continue; // a reference entry's path is the user's own file\r\n        }\r\n        if !entry.path.starts_with(&snapshots) {\r\n            eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\r\n            continue;\r\n        }\r\n        let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\r\n        match std::fs::remove_file(&entry.path) {\r\n            Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\r\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\r\n            Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\r\n        }\r\n    }\r\n    if let Err(err) = registry.save_at(&registry_path) {\r\n        // The bytes are gone and the entries are not. Say so rather than\r\n        // reporting a reap that only half happened; the next tick retries.\r\n        eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\r\n        return;\r\n    }\r\n    eprintln!(\r\n        \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\r\n        reaped.len(),\r\n        bytes_removed\r\n    );\r\n}\r\n\r\n/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot\r\n/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +\r\n/// config are re-read each sweep (adapter / pulse-period changes land between\r\n/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held\r\n/// for symmetry — the threads die with the brain process and the next brain\r\n/// rehydrates from disk.\r\n// [impl->REQ-DAEMON-1]\r\npub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {\r\n    std::thread::spawn(move || {\r\n        let set = Arc::new(LiveSet::new());\r\n        {\r\n            // F-030 W5 ONE-SHOT legacy-resident sweep (REQ-PSYCHE-LEGACY-RESIDENT-SWEEP):\r\n            // BEFORE the first reconcile, bury any pre-W3 psyche WRAPPER a cross-version\r\n            // upgrade left ALIVE-but-hung (it registered its `<id>-psyche` ready perch then\r\n            // blocked forever on `spt ready --once`; the per-event driver never delivers to\r\n            // it). Fires exactly once at brain start (never per-reconcile) — the retired\r\n            // residency machinery's corpse-burial, not its resurrection.\r\n            // [impl->REQ-PSYCHE-LEGACY-RESIDENT-SWEEP]\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            sweep_legacy_resident_psyches(&owlery, &registered, &adapters_dir);\r\n            // F-030 W5 ONE-SHOT `.live-bin` GC — ORDERED AFTER the sweep so any wrapper\r\n            // running FROM the own-copy image is killed first (its dir then unlocks).\r\n            gc_live_bin_dirs(&owlery);\r\n            // LIFECYCLE-TRUTH W3 ONE-SHOT daemon-restart endpoint survival\r\n            // (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL): re-run the harness of every\r\n            // previously-ONLINE spt-hosted endpoint this (re)start orphaned, BEFORE the\r\n            // reconcile loop's phantom-clear offlines the stale-online records (the \"was\r\n            // online\" signal). A brain restart with a surviving broker re-runs nothing\r\n            // (its sessions are live, not orphaned). Belts inside the gate.\r\n            // [impl->REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL]\r\n            resume_restart_orphaned_endpoints(&owlery, &registered, &adapters_dir);\r\n        }\r\n        let mut session_poll = SessionPoll::default();\r\n        while !stop.load(Ordering::SeqCst) {\r\n            // TEST-ONLY gate `SPT_LIVEHOST_RECONCILE_DISABLE` (runtime env, DEFAULT\r\n            // unset = normal production, untouched): skip the reconcile poll+body so an\r\n            // int rig can prove REQ-UPDATE-TRIAL-DRAIN-DRIVE's core-loop KIND_SESSIONS\r\n            // reap-driver in ISOLATION. This loop's own `SessionPoll::query`\r\n            // → `brain.sessions()` (every LIVE_RECONCILE_INTERVAL_MS) would OTHERWISE\r\n            // drive the same broker reap and mask the fix (todlando 2026-07-09). Setting\r\n            // it faithfully REPRODUCES THE FIELD'S livehost-silent update-trial condition\r\n            // (PIN Q2: the trial-brain livehost's first reconcile tick landed around the\r\n            // 30s kill, so it never drove the reap) — the exact failure mode the fix\r\n            // makes the wedge-reap independent of. Runtime env (not cfg(test)): the rig\r\n            // spawns a real release `spt daemon brain` child cfg(test) can't reach — the\r\n            // SPT_BRAIN_WRITE_DEADLINE_MS precedent. Still honors `stop` on a tight tick.\r\n            // [impl->REQ-UPDATE-TRIAL-DRAIN-DRIVE]\r\n            if std::env::var_os(\"SPT_LIVEHOST_RECONCILE_DISABLE\").is_some() {\r\n                std::thread::sleep(Duration::from_millis(100));\r\n                continue;\r\n            }\r\n            let owlery = perch::owlery_dir();\r\n            let adapters_dir = perch::adapters_dir();\r\n            let registered = spt_runtime::registry::registered(&adapters_dir);\r\n            let cfg = DaemonConfig::load();\r\n            // B2 keystone (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE) + B5 boot-gate\r\n            // (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE): clear the status=online\r\n            // latch for any spt-hosted endpoint whose broker session is gone — and\r\n            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once\r\n            // so the same-tick stop-side un-hosts the now-offline perch and the\r\n            // start-side never REVIVES its Psyche. The boot-gate is what stops a\r\n            // cold start after an unclean stop from spawning N phantom Psyches for N\r\n            // dead-but-latched perches. Skipped only when the broker is unreachable\r\n            // (None) — never mass-offline on a hiccup; safe at tick 1 because\r\n            // run_brain connects to the broker BEFORE this host spawns, so the query\r\n            // returns the REAL session set: empty at a cold start (every online\r\n            // controllable perch is a phantom → offlined), the surviving sessions at\r\n            // a brain restart (live agents stay, only the dead are offlined). The\r\n            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.\r\n            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]\r\n            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]\r\n            if let Some(live) = session_poll.query(&crate::endpoint::broker_socket_name()) {\r\n                reconcile_hosted_liveness(&owlery, &live);\r\n            }\r\n            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);\r\n            // W2: the attachment TTL reaper rides this same 5 s pulse — snapshot\r\n            // bytes and the entry that names them are retired together, so an\r\n            // expired attachment stops costing disk as well as stops serving.\r\n            // [impl->REQ-WEB-ATTACHMENT-PULL]\r\n            reap_expired_attachments();\r\n            // Sleep in slices so a stop lands promptly.\r\n            let mut left = LIVE_RECONCILE_INTERVAL_MS;\r\n            while left > 0 && !stop.load(Ordering::SeqCst) {\r\n                let step = left.min(100);\r\n                std::thread::sleep(Duration::from_millis(step));\r\n…\n            }\n        }\n    })\n}","startLine":1204,"lineNumbers":[1204,1205,null,1207,null,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,null,1425,1426,1427,1428]},"fileSize":149949,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\src\\livehost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2915,"totalBytes":11841,"outputLines":215,"outputBytes":11841,"shownRange":{"start":1209,"end":1423},"nextOffset":1424}}},"isError":false,"timestamp":1789522914386}}
{"type":"message","id":"e9a00a1b","parentId":"13ea122a","timestamp":"2026-09-16T01:41:55.122Z","message":{"role":"toolResult","toolCallId":"call_gy0bNxrxoPMKkuY6PXNPhluP|fc_0cdf94810c888815016aa9f3e2436887d1bae04ec73b06f320","toolName":"glob","content":[{"type":"text","text":"# C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/\nlib.rs\nwindows.rs"}],"details":{"scopePath":"C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/windows.rs, C:/Users/decid/.cargo/registry/src/*/fs2-0.4*/src/lib.rs","fileCount":2,"files":["C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/lib.rs","C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/windows.rs"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789522915121}}
{"type":"message","id":"768d2d74","parentId":"e9a00a1b","timestamp":"2026-09-16T01:41:55.505Z","message":{"role":"toolResult","toolCallId":"call_CGja2YGtWHdLNsdo9gT50jbH|fc_0cdf94810c888815016aa9f3e2434887d1bc6eadeedceed1eb","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-daemon/\n\n### src/\n#### livehost.rs\n 1282|fn reap_expired_attachments() {\n*1283|    let registry_path = perch::serving_registry_file();\n 1284|    if !registry_path.exists() {\n 1285|        return; // nothing has ever been served on this node\n 1286|    }\n...\n 1288|    // loading until snapshot cleanup and publication have both finished.\n*1289|    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\n 1290|        Ok(writer) => writer,\n 1291|        Err(err) => {\n 1292|            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n...\n 1295|    };\n*1296|    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n 1297|        Ok(registry) => registry,\n 1298|        Err(err) => {\n 1299|            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n#### servehost.rs\n 7|use serde::{Deserialize, Serialize};\n*8|use spt_store::serving::{ServedEntry, ServingRegistry};\n 9|\n 10|use crate::codec::{read_frame, write_frame};\n 11|use crate::frame::{Envelope, Role};\n...\n 146|    }\n*147|    let path = spt_store::perch::serving_registry_file_in(home);\n*148|    let _writer = spt_store::serving::lock_registry_at(&path)?;\n*149|    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n 150|        if matches!(&request, ServeRequest::AddInputReference { .. }) {\n 151|            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n 152|        } else {\n...\n 326|\n*327|fn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\n 328|    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\n 329|    let retired: Vec<_> = registry\n 330|        .entries()\n...\n 483|        assert!(matches!(result, ServeResult::Missing));\n*484|        assert!(!spt_store::perch::serving_registry_file_in(&home).exists());\n 485|    }\n 486|\n 487|    #[test]\n...\n 613|        let home = tmp.path().join(\"home\");\n*614|        let registry = spt_store::perch::serving_registry_file_in(&home);\n 615|\n 616|        for ask in 1..=2 {\n 617|            let answer = apply_at(&home, ServeRequest::LanStatus, ask).unwrap();\n...\n 641|        std::fs::write(&source, \"must not be registered\").unwrap();\n*642|        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n 643|        // A directory at the sentinel path deterministically refuses acquisition\n 644|        // on both supported platforms, without permission or timing assumptions.\n 645|        let mut lock_path = registry_path.as_os_str().to_os_string();\n#### webserve.rs\n 25|use hyper::{Response, StatusCode};\n*26|use spt_store::serving::{encode_url_segment, ServedEntry, ServedKind, ServingRegistry};\n 27|\n 28|use crate::docshost::{content_type_for, locate_path, sanitize_request_path, serve_path};\n 29|\n...\n 334|// [impl->REQ-WEB-SERVING-REGISTRY]\n*335|fn registry_at(home: &Path) -> Result<ServingRegistry, String> {\n*336|    let path = spt_store::perch::serving_registry_file_in(home);\n*337|    ServingRegistry::load_at(&path)\n 338|        .map_err(|error| format!(\"SERVING_REGISTRY_LOAD_FAIL: {}: {error}\\n\", path.display()))\n 339|}\n 340|\n...\n 343|    home: &Path,\n*344|    registry: &ServingRegistry,\n 345|    remainder: &str,\n 346|    uri_path: &str,\n 347|    query: Option<&str>,\n...\n 523|    home: &Path,\n*524|    registry: &ServingRegistry,\n 525|    remainder: &str,\n 526|    uri_path: &str,\n 527|    query: Option<&str>,\n...\n 837|    fn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\n*838|        let registry_path = spt_store::perch::serving_registry_file_in(home);\n*839|        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 840|        let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\n 841|        registry.save_at(&registry_path).unwrap();\n 842|        entry\n...\n 847|        std::fs::create_dir_all(&root).unwrap();\n*848|        let registry_path = spt_store::perch::serving_registry_file_in(home);\n*849|        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 850|        let entry = registry.add_adapter(&root, name, alias, 1).unwrap();\n 851|        registry.save_at(&registry_path).unwrap();\n 852|        entry\n...\n 877|\n*878|        let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*879|        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 880|        // NO ttl. `add` registers at `registered_at_ms = 1`, so a 24h lifetime\n 881|        // here expires in 1970 and serve-time masking 404s the entry before the\n 882|        // audience is ever consulted — measured, and it would have made this\n...\n 1066|        assert_eq!(body(get(home.path(), uri)), b\"recreated\");\n*1067|        let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*1068|        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 1069|        registry.remove(&entry.id).unwrap();\n 1070|        registry.save_at(&registry_path).unwrap();\n 1071|        assert_eq!(get(home.path(), uri).status(), StatusCode::NOT_FOUND);\n...\n 1243|        let home = tempfile::tempdir().unwrap();\n*1244|        let path = spt_store::perch::serving_registry_file_in(home.path());\n 1245|        std::fs::create_dir_all(path.parent().unwrap()).unwrap();\n 1246|        std::fs::write(path, b\"not json\").unwrap();\n 1247|        for uri in [\"/local/f/file.txt\", \"/local/a/example/file.txt\", \"/local/short/file.txt\"] {\n...\n 1394|        std::fs::write(&source, b\"x\").unwrap();\n*1395|        let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*1396|        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 1397|        registry.add_reference(&source, Some(\"report.md\"), Some(\"ling\"), 1).unwrap();\n 1398|        registry.save_at(&registry_path).unwrap();\n 1399|        let entry = add_adapter(home.path(), \"example\", Some(\"short\"));\n\n### tests/\n#### twohost_web.rs\n 71|use spt_store::roster::{RosterEntry, RosterStore};\n*72|use spt_store::serving::ServingRegistry;\n 73|use spt_store::subnet::SubnetStore;\n 74|use tempfile::TempDir;\n 75|\n...\n 709|    std::fs::write(&locked, LOCKED_BYTES).expect(\"write locked.md\");\n*710|    let registry_path = perch::serving_registry_file_in(home.path());\n*711|    let mut registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\n 712|    registry.add_reference(&open, Some(\"open.md\"), Some(OPEN_ENDPOINT), 1).expect(\"register open.md\");\n 713|    registry.add_reference(&locked, Some(\"locked.md\"), Some(LOCKED_ENDPOINT), 1).expect(\"register locked.md\");\n 714|    registry.save_at(&registry_path).expect(\"save registry\");\n...\n 982|    let helper_entry = loop {\n*983|        let registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\n 984|        let found = registry\n 985|            .entries()\n 986|            .find(|e| e.origin.as_deref() == Some(HELPER_MSG_ID))\n#### webserve_e2e.rs\n 9|\n*10|use spt_store::serving::{ServedEntry, ServingRegistry};\n 11|\n 12|fn http(port: u16, method: &str, path: &str) -> (u16, String, Vec<u8>) {\n 13|    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\n...\n 25|fn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\n*26|    let path = spt_store::perch::serving_registry_file_in(home);\n*27|    let mut registry = ServingRegistry::load_at(&path).unwrap();\n 28|    let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\n 29|    registry.save_at(&path).unwrap();\n 30|    entry\n...\n 114|    assert_eq!(http(port, \"GET\", uri).2, b\"recreated\");\n*115|    let path = spt_store::perch::serving_registry_file_in(home.path());\n*116|    let mut registry = ServingRegistry::load_at(&path).unwrap();\n 117|    registry.remove(&entry.id).unwrap();\n 118|    registry.save_at(&path).unwrap();\n 119|    assert_eq!(http(port, \"GET\", uri).0, 404);\n...\n 314|    std::fs::write(root.parent().unwrap().join(\"private.txt\"), b\"not served\").unwrap();\n*315|    let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*316|    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 317|    let entry = registry.add_adapter(&root, \"example\", Some(\"short\"), 1).unwrap();\n 318|    registry.save_at(&registry_path).unwrap();\n 319|    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), home.path().join(\"docs\"), \"LOCAL\".to_owned(), 0)\n...\n 360|    std::fs::write(docs.join(\"chapter/index.html\"), index).unwrap();\n*361|    let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*362|    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 363|    registry.add_adapter(&adapter_root, \"example\", Some(\"short\"), 1).unwrap();\n 364|    registry.save_at(&registry_path).unwrap();\n 365|    let port = spt_daemon::docshost::start_serving(\n...\n 426|    std::fs::write(web_root.join(\"report.txt\"), b\"core web root report\").unwrap();\n*427|    let registry_path = spt_store::perch::serving_registry_file_in(home);\n*428|    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 429|    registry.add_adapter(&web_root, \"cc\", None, 1).unwrap();\n 430|    registry.save_at(&registry_path).unwrap();\n 431|\n\n## spt-runtime/src/\n### registry.rs\n 959|        std::fs::write(root.join(\"index.html\"), b\"output before daemon startup\").unwrap();\n*960|        assert!(!spt_store::perch::serving_registry_file_in(tmp.path()).exists());\n 961|        assert!(matches!(\n 962|            create_local_profile(&adapters, \"mock-harness\", \"bad\", \"[adapter]\\nweb_short_path = 'docs'\"),\n 963|            Err(RegistryError::Manifest(_))\n\n## spt-store/\n\n### src/\n#### perch.rs\n 117|// [impl->REQ-WEB-SERVING-REGISTRY] [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n*118|pub fn serving_registry_file_in(home: &Path) -> PathBuf {\n 119|    home.join(\"serve\").join(\"registry\")\n 120|}\n 121|\n...\n 123|// [impl->REQ-WEB-SERVING-REGISTRY]\n*124|pub fn serving_registry_file() -> PathBuf {\n*125|    serving_registry_file_in(&spt_home())\n 126|}\n 127|\n 128|/// Attachment bytes copied at send time. A snapshot lives beside the registry\n#### serving.rs\n 126|#[serde(deny_unknown_fields)]\n*127|pub struct ServingRegistry {\n 128|    schema_version: u32,\n 129|    entries: Vec<ServedEntry>,\n 130|    // The key is the requested name, not an assigned suffix. Splitting at the\n...\n 141|///\n*142|/// The stable sibling `registry.lock` is never renamed or removed: locking the\n 143|/// registry itself would stop coordinating writers after atomic replacement.\n 144|/// Closing the returned file (including process exit) releases the lock. Do not\n 145|/// acquire it again while holding it; independently opened handles contend.\n...\n 148|// [impl->REQ-WEB-ATTACHMENT-PULL]\n*149|pub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n 150|    use fs2::FileExt;\n 151|\n 152|    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\n...\n 165|\n*166|impl ServingRegistry {\n 167|    /// Missing is empty. Unreadable, malformed, or inconsistent history is an\n 168|    /// error: never recover by resetting the allocator and reusing stale URLs.\n 169|    pub fn load_at(registry_path: &Path) -> io::Result<Self> {\n...\n 194|    /// Publish a durable old-or-new snapshot, creating its parent directory.\n*195|    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\n 196|    /// through this publication; locking only this save cannot repair stale state.\n 197|    pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\n 198|        self.validate()?;\n...\n 877|\n*878|    fn registry(home: &TempDir) -> ServingRegistry {\n*879|        ServingRegistry::load_at(&crate::perch::serving_registry_file_in(home.path())).unwrap()\n 880|    }\n 881|\n 882|    fn source(home: &TempDir, relative: &str) -> PathBuf {\n...\n 888|\n*889|    fn reload(home: &TempDir, store: &ServingRegistry) -> ServingRegistry {\n*890|        store.save_at(&crate::perch::serving_registry_file_in(home.path())).unwrap();\n 891|        registry(home)\n 892|    }\n 893|\n...\n 933|        }\n*934|        fs::write(crate::perch::serving_registry_file_in(home.path()), serde_json::to_vec(&older).unwrap()).unwrap();\n 935|        let older = registry(&home);\n 936|        for name in [\"live-file\", \"live-dir\", \"snapshot\"] {\n 937|            let entry = older.get(name).unwrap();\n...\n 1103|        let home = tempdir().unwrap();\n*1104|        let path = crate::perch::serving_registry_file_in(home.path());\n 1105|        let mut store = registry(&home);\n 1106|        let first = store.add_reference(&source(&home, \"one/report.md\"), None, None, 1).unwrap();\n 1107|        store.remove(&first.id).unwrap();\n...\n 1121|            let transaction = (|| -> io::Result<()> {\n*1122|                let mut loaded = ServingRegistry::load_at(&path)?;\n 1123|                loaded.add_reference(&next_path, None, None, 2)?;\n 1124|                loaded.save_at(&path)\n 1125|            })();\n...\n 1137|        let home = tempdir().unwrap();\n*1138|        let path = crate::perch::serving_registry_file_in(home.path());\n 1139|        fs::create_dir_all(&path).unwrap();\n*1140|        assert!(ServingRegistry::load_at(&path).is_err());\n 1141|    }\n 1142|\n 1143|    // [unit->REQ-WEB-SERVING-REGISTRY]\n\n### tests/\n#### serving_registry_two_process_int.rs\n 10|\n*11|use spt_store::serving::{lock_registry_at, ServingRegistry};\n 12|\n 13|const CHILD_BOUND: Duration = Duration::from_secs(120);\n 14|const REAP_BOUND: Duration = Duration::from_secs(10);\n...\n 116|    {\n*117|        let _guard = lock_registry_at(&registry_path).expect(\"seed guard\");\n*118|        let mut seed = ServingRegistry::load_at(&registry_path).expect(\"empty registry\");\n 119|        expired = seed.add_attachment(&expired_path, \"report.txt\", None, 10, None, 100)\n 120|            .expect(\"expiring bare name\");\n 121|        retired = seed.add_reference(&retired_path, Some(\"report.txt\"), None, 100)\n...\n 157|    // a stale registrar resurrects the expired entry after its snapshot is gone.\n*158|    let final_registry = ServingRegistry::load_at(&registry_path).expect(\"read durable final state\");\n 159|    assert!(final_registry.get(&expired.id).is_none(), \"expired entry was resurrected by a stale writer\");\n 160|    assert!(!expired_path.try_exists().expect(\"snapshot existence\"), \"reap must remove expired snapshot bytes\");\n 161|    let registration = final_registry.get(\"report~2.txt\").expect(\"acknowledged registration survives later publication\");\n...\n 168|    {\n*169|        let _guard = lock_registry_at(&registry_path).expect(\"retirement guard\");\n*170|        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"fresh retirement load\");\n 171|        registry.remove(&registration_id).expect(\"retire acknowledged registration\");\n 172|        registry.save_at(&registry_path).expect(\"persist retirement\");\n 173|    }\n 174|    {\n*175|        let _guard = lock_registry_at(&registry_path).expect(\"history guard\");\n*176|        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"reload allocation history\");\n 177|        let reclaimed = registry.add_reference(&registered_path, Some(\"report.txt\"), None, 2_000)\n 178|            .expect(\"reclaim recent owner\");\n 179|        assert_eq!(reclaimed.served_name, \"report~2.txt\", \"retiring must preserve the new owner's name\");\n...\n 187|    }\n*188|    let published = ServingRegistry::load_at(&registry_path).expect(\"read continued assignments\");\n 189|    assert_eq!(published.get(\"report~1.txt\").expect(\"old owner\").path, retired_path);\n 190|    assert_eq!(published.get(\"report~2.txt\").expect(\"recent owner\").path, registered_path);\n 191|    assert_eq!(published.get(\"report~3.txt\").expect(\"new owner\").path, third_path);\n\n#### fixtures/\n##### serving_registry_worker.rs\n 9|\n*10|use spt_store::serving::{lock_registry_at, ServedKind, ServingRegistry};\n 11|\n 12|const COMMAND_BOUND: Duration = Duration::from_secs(120);\n 13|\n...\n 57|    }\n*58|    lock_registry_at(registry_path)\n 59|}\n 60|\n 61|fn run() -> io::Result<()> {\n...\n 79|    let guard = acquire(registry_path, barriers, wait)?;\n*80|    let mut registry = ServingRegistry::load_at(registry_path)?;\n 81|    announce(barriers, \"loaded\")?;\n 82|    await_command(barriers, \"mutate\")?;\n 83|    if mode == \"reap\" {\n\n## spt/\n\n### src/\n#### serveverb.rs\n 584|        let registry_path = temp.path().join(\"serving.json\");\n*585|        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 586|        let entry = registry.add_reference(&source, Some(\"quoted.md\"), None, 1).unwrap();\n 587|        let scoped = registry\n 588|            .scope_entry(&entry.id, Some(86_400_000), Some(\"todlando\"), Some(\"ABCDEFGH\"))\n\n### tests/\n#### webserve_attachment_e2e.rs\n 593|    // [int->REQ-WEB-ATTACHMENT-PULL]\n*594|    let registry_path = spt_store::perch::serving_registry_file_in(home);\n 595|    let snapshot_of = |name: &str| -> Option<PathBuf> {\n*596|        let registry = spt_store::serving::ServingRegistry::load_at(&registry_path).ok()?;\n 597|        let found = registry\n 598|            .entries()\n 599|            .find(|entry| entry.served_name == name)\n...\n 671|    {\n*672|        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*673|        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 674|        if registry\n 675|            .scope_entry(\"reap-me.md\", Some(1), None, None)\n 676|            .is_some()\n...\n 686|    {\n*687|        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*688|        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 689|        let planted = registry\n 690|            .add_reference(&kept_source, Some(\"kept-by-the-guard.md\"), None, 1)\n 691|            .expect(\"plant a reference entry\");\n#### webserve_cross_node_e2e.rs\n 660|\n*661|        let registry_path = spt_store::perch::serving_registry_file_in(home_b.path());\n 662|        // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 663|        {\n*664|            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*665|            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 666|            serving\n 667|                .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n 668|                .expect(\"the served entry is there to narrow\");\n...\n 680|        {\n*681|            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*682|            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 683|            serving\n 684|                .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n 685|                .expect(\"the entry is still there\");\n...\n 767|        {\n*768|            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*769|            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 770|            serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n 771|            serving.save_at(&registry_path).expect(\"persist the restore\");\n 772|        }\n...\n 819|        let registry_text = || {\n*820|            std::fs::read_to_string(spt_store::perch::serving_registry_file_in(home_b.path()))\n 821|                .unwrap_or_default()\n 822|        };\n 823|        assert!(helper_lines().is_empty(), \"PRECONDITION: no helper record before this arm\");"}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":88,"fileCount":13,"files":[".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/webserve.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/twohost_web.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs",".worktrees/308-registry-process-lock/crates/spt-runtime/src/registry.rs",".worktrees/308-registry-process-lock/crates/spt-store/src/perch.rs",".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs",".worktrees/308-registry-process-lock/crates/spt-store/tests/fixtures/serving_registry_worker.rs",".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs",".worktrees/308-registry-process-lock/crates/spt/src/serveverb.rs",".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs",".worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs","count":8},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/webserve.rs","count":17},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/twohost_web.rs","count":4},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs","count":11},{"path":".worktrees/308-registry-process-lock/crates/spt-runtime/src/registry.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/perch.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs","count":14},{"path":".worktrees/308-registry-process-lock/crates/spt-store/tests/fixtures/serving_registry_worker.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs","count":9},{"path":".worktrees/308-registry-process-lock/crates/spt/src/serveverb.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs","count":6},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs","count":8}],"truncated":false,"displayContent":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-daemon/\n\n### src/\n#### livehost.rs\n 1282│fn reap_expired_attachments() {\n*1283│    let registry_path = perch::serving_registry_file();\n 1284│    if !registry_path.exists() {\n 1285│        return; // nothing has ever been served on this node\n 1286│    }\n     │...\n 1288│    // loading until snapshot cleanup and publication have both finished.\n*1289│    let _writer = match spt_store::serving::lock_registry_at(&registry_path) {\n 1290│        Ok(writer) => writer,\n 1291│        Err(err) => {\n 1292│            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n     │...\n 1295│    };\n*1296│    let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n 1297│        Ok(registry) => registry,\n 1298│        Err(err) => {\n 1299│            eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n#### servehost.rs\n   7│use serde::{Deserialize, Serialize};\n  *8│use spt_store::serving::{ServedEntry, ServingRegistry};\n   9│\n  10│use crate::codec::{read_frame, write_frame};\n  11│use crate::frame::{Envelope, Role};\n    │...\n 146│    }\n*147│    let path = spt_store::perch::serving_registry_file_in(home);\n*148│    let _writer = spt_store::serving::lock_registry_at(&path)?;\n*149│    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n 150│        if matches!(&request, ServeRequest::AddInputReference { .. }) {\n 151│            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n 152│        } else {\n    │...\n 326│\n*327│fn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\n 328│    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\n 329│    let retired: Vec<_> = registry\n 330│        .entries()\n    │...\n 483│        assert!(matches!(result, ServeResult::Missing));\n*484│        assert!(!spt_store::perch::serving_registry_file_in(&home).exists());\n 485│    }\n 486│\n 487│    #[test]\n    │...\n 613│        let home = tmp.path().join(\"home\");\n*614│        let registry = spt_store::perch::serving_registry_file_in(&home);\n 615│\n 616│        for ask in 1..=2 {\n 617│            let answer = apply_at(&home, ServeRequest::LanStatus, ask).unwrap();\n    │...\n 641│        std::fs::write(&source, \"must not be registered\").unwrap();\n*642│        let registry_path = spt_store::perch::serving_registry_file_in(&home);\n 643│        // A directory at the sentinel path deterministically refuses acquisition\n 644│        // on both supported platforms, without permission or timing assumptions.\n 645│        let mut lock_path = registry_path.as_os_str().to_os_string();\n#### webserve.rs\n   25│use hyper::{Response, StatusCode};\n  *26│use spt_store::serving::{encode_url_segment, ServedEntry, ServedKind, ServingRegistry};\n   27│\n   28│use crate::docshost::{content_type_for, locate_path, sanitize_request_path, serve_path};\n   29│\n     │...\n  334│// [impl->REQ-WEB-SERVING-REGISTRY]\n *335│fn registry_at(home: &Path) -> Result<ServingRegistry, String> {\n *336│    let path = spt_store::perch::serving_registry_file_in(home);\n *337│    ServingRegistry::load_at(&path)\n  338│        .map_err(|error| format!(\"SERVING_REGISTRY_LOAD_FAIL: {}: {error}\\n\", path.display()))\n  339│}\n  340│\n     │...\n  343│    home: &Path,\n *344│    registry: &ServingRegistry,\n  345│    remainder: &str,\n  346│    uri_path: &str,\n  347│    query: Option<&str>,\n     │...\n  523│    home: &Path,\n *524│    registry: &ServingRegistry,\n  525│    remainder: &str,\n  526│    uri_path: &str,\n  527│    query: Option<&str>,\n     │...\n  837│    fn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\n *838│        let registry_path = spt_store::perch::serving_registry_file_in(home);\n *839│        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n  840│        let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\n  841│        registry.save_at(&registry_path).unwrap();\n  842│        entry\n     │...\n  847│        std::fs::create_dir_all(&root).unwrap();\n *848│        let registry_path = spt_store::perch::serving_registry_file_in(home);\n *849│        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n  850│        let entry = registry.add_adapter(&root, name, alias, 1).unwrap();\n  851│        registry.save_at(&registry_path).unwrap();\n  852│        entry\n     │...\n  877│\n *878│        let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n *879│        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n  880│        // NO ttl. `add` registers at `registered_at_ms = 1`, so a 24h lifetime\n  881│        // here expires in 1970 and serve-time masking 404s the entry before the\n  882│        // audience is ever consulted — measured, and it would have made this\n     │...\n 1066│        assert_eq!(body(get(home.path(), uri)), b\"recreated\");\n*1067│        let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*1068│        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 1069│        registry.remove(&entry.id).unwrap();\n 1070│        registry.save_at(&registry_path).unwrap();\n 1071│        assert_eq!(get(home.path(), uri).status(), StatusCode::NOT_FOUND);\n     │...\n 1243│        let home = tempfile::tempdir().unwrap();\n*1244│        let path = spt_store::perch::serving_registry_file_in(home.path());\n 1245│        std::fs::create_dir_all(path.parent().unwrap()).unwrap();\n 1246│        std::fs::write(path, b\"not json\").unwrap();\n 1247│        for uri in [\"/local/f/file.txt\", \"/local/a/example/file.txt\", \"/local/short/file.txt\"] {\n     │...\n 1394│        std::fs::write(&source, b\"x\").unwrap();\n*1395│        let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*1396│        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 1397│        registry.add_reference(&source, Some(\"report.md\"), Some(\"ling\"), 1).unwrap();\n 1398│        registry.save_at(&registry_path).unwrap();\n 1399│        let entry = add_adapter(home.path(), \"example\", Some(\"short\"));\n\n### tests/\n#### twohost_web.rs\n  71│use spt_store::roster::{RosterEntry, RosterStore};\n *72│use spt_store::serving::ServingRegistry;\n  73│use spt_store::subnet::SubnetStore;\n  74│use tempfile::TempDir;\n  75│\n    │...\n 709│    std::fs::write(&locked, LOCKED_BYTES).expect(\"write locked.md\");\n*710│    let registry_path = perch::serving_registry_file_in(home.path());\n*711│    let mut registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\n 712│    registry.add_reference(&open, Some(\"open.md\"), Some(OPEN_ENDPOINT), 1).expect(\"register open.md\");\n 713│    registry.add_reference(&locked, Some(\"locked.md\"), Some(LOCKED_ENDPOINT), 1).expect(\"register locked.md\");\n 714│    registry.save_at(&registry_path).expect(\"save registry\");\n    │...\n 982│    let helper_entry = loop {\n*983│        let registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\n 984│        let found = registry\n 985│            .entries()\n 986│            .find(|e| e.origin.as_deref() == Some(HELPER_MSG_ID))\n#### webserve_e2e.rs\n   9│\n *10│use spt_store::serving::{ServedEntry, ServingRegistry};\n  11│\n  12│fn http(port: u16, method: &str, path: &str) -> (u16, String, Vec<u8>) {\n  13│    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\n    │...\n  25│fn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\n *26│    let path = spt_store::perch::serving_registry_file_in(home);\n *27│    let mut registry = ServingRegistry::load_at(&path).unwrap();\n  28│    let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\n  29│    registry.save_at(&path).unwrap();\n  30│    entry\n    │...\n 114│    assert_eq!(http(port, \"GET\", uri).2, b\"recreated\");\n*115│    let path = spt_store::perch::serving_registry_file_in(home.path());\n*116│    let mut registry = ServingRegistry::load_at(&path).unwrap();\n 117│    registry.remove(&entry.id).unwrap();\n 118│    registry.save_at(&path).unwrap();\n 119│    assert_eq!(http(port, \"GET\", uri).0, 404);\n    │...\n 314│    std::fs::write(root.parent().unwrap().join(\"private.txt\"), b\"not served\").unwrap();\n*315│    let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*316│    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 317│    let entry = registry.add_adapter(&root, \"example\", Some(\"short\"), 1).unwrap();\n 318│    registry.save_at(&registry_path).unwrap();\n 319│    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), home.path().join(\"docs\"), \"LOCAL\".to_owned(), 0)\n    │...\n 360│    std::fs::write(docs.join(\"chapter/index.html\"), index).unwrap();\n*361│    let registry_path = spt_store::perch::serving_registry_file_in(home.path());\n*362│    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 363│    registry.add_adapter(&adapter_root, \"example\", Some(\"short\"), 1).unwrap();\n 364│    registry.save_at(&registry_path).unwrap();\n 365│    let port = spt_daemon::docshost::start_serving(\n    │...\n 426│    std::fs::write(web_root.join(\"report.txt\"), b\"core web root report\").unwrap();\n*427│    let registry_path = spt_store::perch::serving_registry_file_in(home);\n*428│    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n 429│    registry.add_adapter(&web_root, \"cc\", None, 1).unwrap();\n 430│    registry.save_at(&registry_path).unwrap();\n 431│\n\n## spt-runtime/src/\n### registry.rs\n 959│        std::fs::write(root.join(\"index.html\"), b\"output before daemon startup\").unwrap();\n*960│        assert!(!spt_store::perch::serving_registry_file_in(tmp.path()).exists());\n 961│        assert!(matches!(\n 962│            create_local_profile(&adapters, \"mock-harness\", \"bad\", \"[adapter]\\nweb_short_path = 'docs'\"),\n 963│            Err(RegistryError::Manifest(_))\n\n## spt-store/\n\n### src/\n#### perch.rs\n 117│// [impl->REQ-WEB-SERVING-REGISTRY] [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]\n*118│pub fn serving_registry_file_in(home: &Path) -> PathBuf {\n 119│    home.join(\"serve\").join(\"registry\")\n 120│}\n 121│\n    │...\n 123│// [impl->REQ-WEB-SERVING-REGISTRY]\n*124│pub fn serving_registry_file() -> PathBuf {\n*125│    serving_registry_file_in(&spt_home())\n 126│}\n 127│\n 128│/// Attachment bytes copied at send time. A snapshot lives beside the registry\n#### serving.rs\n  126│#[serde(deny_unknown_fields)]\n *127│pub struct ServingRegistry {\n  128│    schema_version: u32,\n  129│    entries: Vec<ServedEntry>,\n  130│    // The key is the requested name, not an assigned suffix. Splitting at the\n     │...\n  141│///\n *142│/// The stable sibling `registry.lock` is never renamed or removed: locking the\n  143│/// registry itself would stop coordinating writers after atomic replacement.\n  144│/// Closing the returned file (including process exit) releases the lock. Do not\n  145│/// acquire it again while holding it; independently opened handles contend.\n     │...\n  148│// [impl->REQ-WEB-ATTACHMENT-PULL]\n *149│pub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n  150│    use fs2::FileExt;\n  151│\n  152│    if let Some(parent) = registry_path.parent().filter(|p| !p.as_os_str().is_empty()) {\n     │...\n  165│\n *166│impl ServingRegistry {\n  167│    /// Missing is empty. Unreadable, malformed, or inconsistent history is an\n  168│    /// error: never recover by resetting the allocator and reusing stale URLs.\n  169│    pub fn load_at(registry_path: &Path) -> io::Result<Self> {\n     │...\n  194│    /// Publish a durable old-or-new snapshot, creating its parent directory.\n *195│    /// A concurrent writer must hold [`lock_registry_at`] from BEFORE its load\n  196│    /// through this publication; locking only this save cannot repair stale state.\n  197│    pub fn save_at(&self, registry_path: &Path) -> io::Result<()> {\n  198│        self.validate()?;\n     │...\n  877│\n *878│    fn registry(home: &TempDir) -> ServingRegistry {\n *879│        ServingRegistry::load_at(&crate::perch::serving_registry_file_in(home.path())).unwrap()\n  880│    }\n  881│\n  882│    fn source(home: &TempDir, relative: &str) -> PathBuf {\n     │...\n  888│\n *889│    fn reload(home: &TempDir, store: &ServingRegistry) -> ServingRegistry {\n *890│        store.save_at(&crate::perch::serving_registry_file_in(home.path())).unwrap();\n  891│        registry(home)\n  892│    }\n  893│\n     │...\n  933│        }\n *934│        fs::write(crate::perch::serving_registry_file_in(home.path()), serde_json::to_vec(&older).unwrap()).unwrap();\n  935│        let older = registry(&home);\n  936│        for name in [\"live-file\", \"live-dir\", \"snapshot\"] {\n  937│            let entry = older.get(name).unwrap();\n     │...\n 1103│        let home = tempdir().unwrap();\n*1104│        let path = crate::perch::serving_registry_file_in(home.path());\n 1105│        let mut store = registry(&home);\n 1106│        let first = store.add_reference(&source(&home, \"one/report.md\"), None, None, 1).unwrap();\n 1107│        store.remove(&first.id).unwrap();\n     │...\n 1121│            let transaction = (|| -> io::Result<()> {\n*1122│                let mut loaded = ServingRegistry::load_at(&path)?;\n 1123│                loaded.add_reference(&next_path, None, None, 2)?;\n 1124│                loaded.save_at(&path)\n 1125│            })();\n     │...\n 1137│        let home = tempdir().unwrap();\n*1138│        let path = crate::perch::serving_registry_file_in(home.path());\n 1139│        fs::create_dir_all(&path).unwrap();\n*1140│        assert!(ServingRegistry::load_at(&path).is_err());\n 1141│    }\n 1142│\n 1143│    // [unit->REQ-WEB-SERVING-REGISTRY]\n\n### tests/\n#### serving_registry_two_process_int.rs\n  10│\n *11│use spt_store::serving::{lock_registry_at, ServingRegistry};\n  12│\n  13│const CHILD_BOUND: Duration = Duration::from_secs(120);\n  14│const REAP_BOUND: Duration = Duration::from_secs(10);\n    │...\n 116│    {\n*117│        let _guard = lock_registry_at(&registry_path).expect(\"seed guard\");\n*118│        let mut seed = ServingRegistry::load_at(&registry_path).expect(\"empty registry\");\n 119│        expired = seed.add_attachment(&expired_path, \"report.txt\", None, 10, None, 100)\n 120│            .expect(\"expiring bare name\");\n 121│        retired = seed.add_reference(&retired_path, Some(\"report.txt\"), None, 100)\n    │...\n 157│    // a stale registrar resurrects the expired entry after its snapshot is gone.\n*158│    let final_registry = ServingRegistry::load_at(&registry_path).expect(\"read durable final state\");\n 159│    assert!(final_registry.get(&expired.id).is_none(), \"expired entry was resurrected by a stale writer\");\n 160│    assert!(!expired_path.try_exists().expect(\"snapshot existence\"), \"reap must remove expired snapshot bytes\");\n 161│    let registration = final_registry.get(\"report~2.txt\").expect(\"acknowledged registration survives later publication\");\n    │...\n 168│    {\n*169│        let _guard = lock_registry_at(&registry_path).expect(\"retirement guard\");\n*170│        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"fresh retirement load\");\n 171│        registry.remove(&registration_id).expect(\"retire acknowledged registration\");\n 172│        registry.save_at(&registry_path).expect(\"persist retirement\");\n 173│    }\n 174│    {\n*175│        let _guard = lock_registry_at(&registry_path).expect(\"history guard\");\n*176│        let mut registry = ServingRegistry::load_at(&registry_path).expect(\"reload allocation history\");\n 177│        let reclaimed = registry.add_reference(&registered_path, Some(\"report.txt\"), None, 2_000)\n 178│            .expect(\"reclaim recent owner\");\n 179│        assert_eq!(reclaimed.served_name, \"report~2.txt\", \"retiring must preserve the new owner's name\");\n    │...\n 187│    }\n*188│    let published = ServingRegistry::load_at(&registry_path).expect(\"read continued assignments\");\n 189│    assert_eq!(published.get(\"report~1.txt\").expect(\"old owner\").path, retired_path);\n 190│    assert_eq!(published.get(\"report~2.txt\").expect(\"recent owner\").path, registered_path);\n 191│    assert_eq!(published.get(\"report~3.txt\").expect(\"new owner\").path, third_path);\n\n#### fixtures/\n##### serving_registry_worker.rs\n  9│\n*10│use spt_store::serving::{lock_registry_at, ServedKind, ServingRegistry};\n 11│\n 12│const COMMAND_BOUND: Duration = Duration::from_secs(120);\n 13│\n   │...\n 57│    }\n*58│    lock_registry_at(registry_path)\n 59│}\n 60│\n 61│fn run() -> io::Result<()> {\n   │...\n 79│    let guard = acquire(registry_path, barriers, wait)?;\n*80│    let mut registry = ServingRegistry::load_at(registry_path)?;\n 81│    announce(barriers, \"loaded\")?;\n 82│    await_command(barriers, \"mutate\")?;\n 83│    if mode == \"reap\" {\n\n## spt/\n\n### src/\n#### serveverb.rs\n 584│        let registry_path = temp.path().join(\"serving.json\");\n*585│        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 586│        let entry = registry.add_reference(&source, Some(\"quoted.md\"), None, 1).unwrap();\n 587│        let scoped = registry\n 588│            .scope_entry(&entry.id, Some(86_400_000), Some(\"todlando\"), Some(\"ABCDEFGH\"))\n\n### tests/\n#### webserve_attachment_e2e.rs\n 593│    // [int->REQ-WEB-ATTACHMENT-PULL]\n*594│    let registry_path = spt_store::perch::serving_registry_file_in(home);\n 595│    let snapshot_of = |name: &str| -> Option<PathBuf> {\n*596│        let registry = spt_store::serving::ServingRegistry::load_at(&registry_path).ok()?;\n 597│        let found = registry\n 598│            .entries()\n 599│            .find(|entry| entry.served_name == name)\n    │...\n 671│    {\n*672│        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*673│        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 674│        if registry\n 675│            .scope_entry(\"reap-me.md\", Some(1), None, None)\n 676│            .is_some()\n    │...\n 686│    {\n*687│        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*688│        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 689│        let planted = registry\n 690│            .add_reference(&kept_source, Some(\"kept-by-the-guard.md\"), None, 1)\n 691│            .expect(\"plant a reference entry\");\n#### webserve_cross_node_e2e.rs\n 660│\n*661│        let registry_path = spt_store::perch::serving_registry_file_in(home_b.path());\n 662│        // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 663│        {\n*664│            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*665│            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 666│            serving\n 667│                .scope_entry(\"report.md\", None, Some(AUDIENCE_ENDPOINT), None)\n 668│                .expect(\"the served entry is there to narrow\");\n    │...\n 680│        {\n*681│            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*682│            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 683│            serving\n 684│                .scope_entry(\"report.md\", None, Some(\"somebody-else\"), None)\n 685│                .expect(\"the entry is still there\");\n    │...\n 767│        {\n*768│            let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n*769│            let mut serving = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n 770│            serving.scope_entry(\"report.md\", None, None, None).expect(\"clear the audience\");\n 771│            serving.save_at(&registry_path).expect(\"persist the restore\");\n 772│        }\n    │...\n 819│        let registry_text = || {\n*820│            std::fs::read_to_string(spt_store::perch::serving_registry_file_in(home_b.path()))\n 821│                .unwrap_or_default()\n 822│        };\n 823│        assert!(helper_lines().is_empty(), \"PRECONDITION: no helper record before this arm\");"},"isError":false,"timestamp":1789522915505}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WEO2rRRpMoFwNA5aqMg5rvY3|fc_0cdf94810c888815016aa9f3f058fc87d1840b524603074676","toolName":"read","startedAt":"2026-09-16T01:42:09.243Z","args":{"path":"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/windows.rs:1-180"},"intent":"Reading fs2 Windows locking source"},"id":"bfaeea99","parentId":"768d2d74","timestamp":"2026-09-16T01:42:09.243Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vb9ucnRKRxC6owkyf3s4XBzg|fc_0cdf94810c888815016aa9f3f0590c87d1ba0603099280a66f","toolName":"read","startedAt":"2026-09-16T01:42:09.243Z","args":{"path":"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/lib.rs:1-185"},"intent":"Reading fs2 lock contract"},"id":"aef84f76","parentId":"bfaeea99","timestamp":"2026-09-16T01:42:09.243Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_okTAW3mcWKDQANQiUbHRp4Ox|fc_0cdf94810c888815016aa9f3f0591487d1aed609841598b152","toolName":"grep","startedAt":"2026-09-16T01:42:09.243Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs"},"intent":"Locating atomic publication implementation"},"id":"33eb6a5f","parentId":"aef84f76","timestamp":"2026-09-16T01:42:09.243Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xbpT93xQEEltrfbjVntdXxfk|fc_0cdf94810c888815016aa9f3f0591c87d19bc550e63cc979c0","toolName":"read","startedAt":"2026-09-16T01:42:09.244Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/twohost_web.rs:570-780"},"intent":"Checking other test writer lifecycle"},"id":"da160e75","parentId":"33eb6a5f","timestamp":"2026-09-16T01:42:09.244Z"}
{"type":"message","id":"c7e27f47","parentId":"da160e75","timestamp":"2026-09-16T01:42:09.244Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Checking lock semantics**"},{"type":"toolCall","id":"call_WEO2rRRpMoFwNA5aqMg5rvY3|fc_0cdf94810c888815016aa9f3f058fc87d1840b524603074676","name":"read","arguments":{"i":"Reading fs2 Windows locking source","path":"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/windows.rs:1-180"},"intent":"Reading fs2 Windows locking source"},{"type":"toolCall","id":"call_vb9ucnRKRxC6owkyf3s4XBzg|fc_0cdf94810c888815016aa9f3f0590c87d1ba0603099280a66f","name":"read","arguments":{"i":"Reading fs2 lock contract","path":"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/lib.rs:1-185"},"intent":"Reading fs2 lock contract"},{"type":"toolCall","id":"call_okTAW3mcWKDQANQiUbHRp4Ox|fc_0cdf94810c888815016aa9f3f0591487d1aed609841598b152","name":"grep","arguments":{"i":"Locating atomic publication implementation","pattern":"atomic_write_bytes_durable|replace|rename","path":".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs","case":true,"gitignore":true,"skip":null},"intent":"Locating atomic publication implementation"},{"type":"toolCall","id":"call_xbpT93xQEEltrfbjVntdXxfk|fc_0cdf94810c888815016aa9f3f0591c87d19bc550e63cc979c0","name":"read","arguments":{"i":"Checking other test writer lifecycle","path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/twohost_web.rs:570-780"},"intent":"Checking other test writer lifecycle"},{"type":"toolCall","id":"call_HuidBG8Wf2CvnQAn72FSEa5b|fc_0cdf94810c888815016aa9f3f0592487d1828116d68a324b06","name":"read","arguments":{"i":"Checking attachment fixture lock scope","path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs:580-780"},"intent":"Checking attachment fixture lock scope"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":14605,"output":328,"cacheRead":41728,"cacheWrite":0,"totalTokens":56661,"reasoningTokens":27,"cost":{"input":0.14605,"output":0.0164,"cacheRead":0.041728,"cacheWrite":0,"total":0.20417800000000003}},"stopReason":"toolUse","timestamp":1789522915537,"responseId":"resp_0cdf94810c888815016aa9f3e3f02087d1907f3fa3161ba6ba","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cdf94810c888815016aa9f3e6840087d180cb01831dc04ab5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqfPwyqOhh_KFfMIRP37o9iQh0QZ-vlcZukBr5j2r3I00cw5KWKv0fBG2HsNaybGIv2x8MTMFvaxjTW7tp_0SApEqJYDcZf6xh-Bc9iHrA_CmmJjUZQ7truQy4WfnLHU3ri1u_wthk9aSEwQLZRAHgAjc1JL4B4qN6sJ1wbDc5X7hgYCpIWD2H5mKfWRAT7SKSrqE6J8Z0ON4Qbz-eWCpVc3IrTrFYQYs5Xo35-q4gtc543TFTnhzSLRiBvfLQDckuTsrRJ7sjtsiS4FMpN1PhTSLBhbOh1NwB9s7TH-uhi3oyB9qAj5E42OmwtvNFI-JuxfAjTzgyHFpOUl8PpUdZhXKMuK68gFNA94lVvWkWxPQjoRN8zbYZcr22bC5z_fKkz0SOxziVMifFPqEr1YvEW9z3lDY5TipCOd36lf7gZya12fjS7_2JM63Rpm8ikag3Pq6MchIriaMWOccq25LEY_7EhPs8mJdtMCTQekkJSHF1DJZeLsEdYnbZ8KKXqo4B5EbFrw_o5L_x6UOcooR_dGNdPD1YYTGSBQM8IrdNAEG4xxIPCTlsDGM9IMfU31W8DKWGPmz3-hg5zeMO0Tzyg9C5aL849pCClCOB2kTcbGSJWT19BrHhBPJ3G_pz4Mxb6F-Mxurw5MhkzaFgDbM1r3POEa3xJoVclfOLpzvHoWlKfyOkJabYEi22a0CzucvQUsALMFFCDXL-VEScA9C6uca1SbKJOV697GFV3sLweXl87VFZAKZvReR45uVrwD4aqdfJLeACaH2q6voTdzRdJHcxFBgK3gpW3qalkMesu_E7_y71CN8a7yeao5Wyao_OXZPI7nuTlaBPPSEYtdVQtolXmt6akuTIvQYhL9YU0HRbqWX1NQHp8pc8_Mdt9d8C0gjigo9uMJ5E6P41J5M9zjbNNZIuB7uLyv3LmFr0rXDSD3eFCSq4CwUbPLwHNWZadlzQNN26EGCmiIM5YC7KgVqreIHaj-ty_T5XHXOLA-y8SGbDMuqmufBmx5DC4x17PK-RmSAHduFSRMLK5nv12RlhSlBZG2OIDMMY6_TO2QgkeIbu7ndabtPAaMKjrui3xeCDGIMqUMwsaDd0rNpeu88QqCa5jhpVEQbzUp1Dsi4JWKpOsxVsdsfCQFw4vWe9c3BfQCuSDxaSNHZtwkHfneiCDOxmmzG73vgzcPXJXS0gTsxNbXuylqaLkxcHgX0NVLo6XU6HTDxxiWNEly1BIiC2QiC9tsa4_ygZwEdYCVbQ1JXEUy4oNE3Km-6xAbfbiDEuJ3FNEFpwfwNPiq779cOkKIlxaErKrFTj_67BwI1nHrLVavcvfKtykXM3w9fQXWIpPgVhPU14BAP6Rwt9YziRo8MpwnK3lFhSAWSCdyMLooOIY-vcj9syJk34AMKXHe8zwiOn_ooH_NWfnRYm6T-ud_ncx9hTU2WcNFBZYH-I20=","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"summary":[{"type":"summary_text","text":"**Checking lock semantics**"}],"metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3f058fc87d1840b524603074676","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading fs2 Windows locking source\",\"path\":\"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/windows.rs:1-180\"}","call_id":"call_WEO2rRRpMoFwNA5aqMg5rvY3","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3f0590c87d1ba0603099280a66f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading fs2 lock contract\",\"path\":\"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/lib.rs:1-185\"}","call_id":"call_vb9ucnRKRxC6owkyf3s4XBzg","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3f0591487d1aed609841598b152","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating atomic publication implementation\",\"pattern\":\"atomic_write_bytes_durable|replace|rename\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_okTAW3mcWKDQANQiUbHRp4Ox","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3f0591c87d19bc550e63cc979c0","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking other test writer lifecycle\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/tests/twohost_web.rs:570-780\"}","call_id":"call_xbpT93xQEEltrfbjVntdXxfk","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f3f0592487d1828116d68a324b06","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking attachment fixture lock scope\",\"path\":\".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs:580-780\"}","call_id":"call_HuidBG8Wf2CvnQAn72FSEa5b","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":13684.45689999999,"ttft":3000.913899999985,"completedAt":1789522929241,"contextSnapshot":{"promptTokens":56333,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HuidBG8Wf2CvnQAn72FSEa5b|fc_0cdf94810c888815016aa9f3f0592487d1828116d68a324b06","toolName":"read","startedAt":"2026-09-16T01:42:09.244Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs:580-780"},"intent":"Checking attachment fixture lock scope"},"id":"5674b672","parentId":"c7e27f47","timestamp":"2026-09-16T01:42:09.244Z"}
{"type":"message","id":"daa6824a","parentId":"5674b672","timestamp":"2026-09-16T01:42:09.252Z","message":{"role":"toolResult","toolCallId":"call_okTAW3mcWKDQANQiUbHRp4Ox|fc_0cdf94810c888815016aa9f3f0591487d1aed609841598b152","toolName":"grep","content":[{"type":"text","text":" 4|//!\n*5|//! - **5.2 Windows EBUSY on atomic rename** (REQ-HAZARD-EBUSY-RENAME): a\n*6|//!   `fs::rename` can fail transiently while another process (AV scanner,\n 7|//!   indexer, a just-closed handle) still holds the target. The sister's\n*8|//!   `atomic_write_string` was a bare tmp-write + rename; spt-core adds\n 9|//!   retry-with-backoff that tolerates transient sharing/access errors and\n 10|//!   only surfaces a persistent failure.\n 11|//! - **5.4 Windows UNC prefix in serialized paths** (REQ-HAZARD-UNC-PATH-STRIP):\n...\n 17|//!\n*18|//! The write pattern itself (sibling `{name}.tmp` + rename) is verbatim; same-\n*19|//! volume rename is atomic on both NTFS and POSIX, so a concurrent reader never\n 20|//! observes a half-written file.\n 21|\n 22|use std::fs;\n...\n 28|\n*29|/// Maximum rename attempts before surfacing the error (KNOWN-HAZARDS 5.2).\n 30|const MAX_RENAME_ATTEMPTS: u32 = 10;\n 31|\n 32|/// Process-local counter making each write's tmp sibling unique (KNOWN-HAZARDS\n...\n 44|pub fn to_forward_slash(path: &Path) -> String {\n*45|    let mut s = path.to_string_lossy().replace('\\\\', \"/\");\n 46|    // `\\\\?\\C:\\...` -> `//?/C:/...` -> `C:/...`\n 47|    if let Some(stripped) = s.strip_prefix(\"//?/\") {\n 48|        s = stripped.to_string();\n...\n 52|\n*53|/// Atomically write `body` to `path` via a sibling `{name}.tmp` + rename, with\n 54|/// EBUSY retry/backoff (KNOWN-HAZARDS 5.2). A concurrent reader sees either the\n 55|/// old file or the complete new file — never a torn write. **Non-durable** — see\n 56|/// [`atomic_write_bytes`] for the durability trade-off.\n...\n 64|///\n*65|/// **Non-durable** (no fsync): a plain `fs::write` of the tmp + rename. Still\n*66|/// reader-atomic (old-or-new, never torn), but a hard reset between the rename\n 67|/// and the data flush can resurrect the file at full length filled with NUL. That\n 68|/// is the correct default for RECONSTRUCTIBLE state — registry snapshots, peer\n 69|/// caches, the release cache — where a post-crash NUL husk simply parse-fails to\n...\n 71|/// corrupt record harmless at every seam). For AUTHORITATIVE records whose loss\n*72|/// IS an incident, use [`atomic_write_bytes_durable`].\n 73|///\n 74|/// **The epoch counter was named on that list and no longer is** (KNOWN-HAZARDS\n 75|/// 7.60, WAN-TRUTH #40): it is NOT reconstructible, because nothing regenerates\n...\n 83|pub fn atomic_write_bytes(path: &Path, body: &[u8]) -> io::Result<()> {\n*84|    write_then_rename(path, body, false)\n 85|}\n 86|\n 87|/// **Durable** sibling of [`atomic_write_bytes`]: the tmp file is fsync'd\n*88|/// (`sync_all`) BEFORE the rename publishes the name (KNOWN-HAZARDS 5.13), so a\n 89|/// hard reset yields the complete old OR new file — never a full-length NUL\n*90|/// zero-fill (NTFS keeps the rename metadata but loses un-synced data blocks).\n 91|///\n 92|/// **Scoped, NOT a blanket default.** Durability is opt-in for the few records\n 93|/// whose loss is unrecoverable — the authoritative per-perch record\n...\n 107|// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n*108|pub fn atomic_write_bytes_durable(path: &Path, body: &[u8]) -> io::Result<()> {\n*109|    write_then_rename(path, body, true)\n 110|}\n 111|\n*112|/// String sibling of [`atomic_write_bytes_durable`] (durable tmp+rename).\n 113|// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n 114|pub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\n*115|    atomic_write_bytes_durable(path, body.as_bytes())\n 116|}\n 117|\n*118|/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\n*119|/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\n 120|/// reconstructible-state default, [`atomic_write_bytes`]).\n 121|///\n 122|/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\n...\n 125|/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\n*126|/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\n 127|/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\n 128|/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\n*129|/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\n 130|// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\n*131|fn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\n 132|    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\n 133|    let tmp = path.with_file_name(format!(\n 134|        \"{}.tmp.{}-{}\",\n...\n 141|        f.write_all(body)?;\n*142|        f.sync_all()?; // durable data BEFORE the rename publishes the name\n 143|    } else {\n 144|        fs::write(&tmp, body)?;\n 145|    }\n*146|    match rename_with_retry(&tmp, path) {\n 147|        Ok(()) => Ok(()),\n 148|        Err(e) => {\n 149|            // Best-effort cleanup of the orphaned tmp; never mask the real error.\n...\n 158|// [impl->REQ-HAZARD-EBUSY-RENAME]\n*159|fn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {\n*160|    retry_transient(MAX_RENAME_ATTEMPTS, backoff, || fs::rename(from, to))\n 161|}\n 162|\n 163|/// Linear backoff: attempt 1 → 2ms, attempt 2 → 4ms, … capped modestly. Kept\n...\n 197|///\n*198|/// Windows surfaces a held-handle rename collision as `ERROR_SHARING_VIOLATION`\n 199|/// (32) or `ERROR_ACCESS_DENIED` (5). On other platforms `PermissionDenied`\n 200|/// is the analogue; everything else (NotFound, etc.) is non-transient.\n 201|fn is_transient(e: &io::Error) -> bool {\n...\n 222|    /// No `.tmp` sibling of any kind remains in `dir` (the unique tmp names —\n*223|    /// `{name}.tmp.{pid}-{seq}` — must all be renamed away, not just a fixed\n 224|    /// `{name}.tmp`). Glob-style: any entry whose name contains `.tmp` fails.\n 225|    fn no_tmp_sibling_remains(dir: &Path) {\n 226|        let leftover: Vec<String> = fs::read_dir(dir)\n...\n 246|    // [unit->REQ-HAZARD-EBUSY-RENAME] write creates the file with exact content,\n*247|    // and replaces an existing file; no `.tmp` sibling is left behind.\n 248|    #[test]\n*249|    fn atomic_write_creates_and_replaces() {\n 250|        let dir = tempfile::tempdir().unwrap();\n 251|        let target = dir.path().join(\"cursor\");\n 252|        atomic_write_string(&target, \"first\").unwrap();\n...\n 271|    // [unit->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] the DURABLE path exercises\n*272|    // File::create → write_all → sync_all → rename: bytes (and the string wrapper)\n*273|    // round-trip byte-exact through the fsync-before-rename ordering, and the tmp is\n*274|    // renamed away. This is the opt-in used by the authoritative perch/identity writers;\n 275|    // the blanket-fsync default was reverted (it wedged daemon bringup).\n 276|    #[test]\n*277|    fn atomic_write_durable_round_trips_and_renames_tmp() {\n 278|        let dir = tempfile::tempdir().unwrap();\n 279|        let target = dir.path().join(\"rec.bin\");\n 280|        let data: Vec<u8> = (0..=255u8).collect();\n*281|        atomic_write_bytes_durable(&target, &data).unwrap();\n 282|        assert_eq!(fs::read(&target).unwrap(), data);\n 283|\n 284|        let s_target = dir.path().join(\"rec.json\");\n...\n 291|    // many threads hammering the SAME target concurrently must EACH return Ok — no\n*292|    // writer's rename may consume another's staged tmp (the os-error-2 loser that\n*293|    // wedged bind). A `Barrier` aligns the writers so the create→rename windows\n 294|    // actually overlap (without it the collision is a microsecond fluke and the guard\n 295|    // is decorative — it must red on the old fixed-`{name}.tmp` code). After the\n 296|    // storm the target parses complete (one full winning body, never a torn/partial).\n...\n 314|                    for _ in 0..ITERS {\n*315|                        barrier.wait(); // align create→rename windows across writers\n*316|                        atomic_write_bytes_durable(&target, body.as_bytes())\n 317|                            .expect(\"every concurrent write must succeed (no os-error-2 loser)\");\n 318|                    }\n 319|                })"}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-store\\src\\atomic.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":40,"fileCount":1,"files":[".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs","count":40}],"truncated":false,"displayContent":"   4│//!\n  *5│//! - **5.2 Windows EBUSY on atomic rename** (REQ-HAZARD-EBUSY-RENAME): a\n  *6│//!   `fs::rename` can fail transiently while another process (AV scanner,\n   7│//!   indexer, a just-closed handle) still holds the target. The sister's\n  *8│//!   `atomic_write_string` was a bare tmp-write + rename; spt-core adds\n   9│//!   retry-with-backoff that tolerates transient sharing/access errors and\n  10│//!   only surfaces a persistent failure.\n  11│//! - **5.4 Windows UNC prefix in serialized paths** (REQ-HAZARD-UNC-PATH-STRIP):\n    │...\n  17│//!\n *18│//! The write pattern itself (sibling `{name}.tmp` + rename) is verbatim; same-\n *19│//! volume rename is atomic on both NTFS and POSIX, so a concurrent reader never\n  20│//! observes a half-written file.\n  21│\n  22│use std::fs;\n    │...\n  28│\n *29│/// Maximum rename attempts before surfacing the error (KNOWN-HAZARDS 5.2).\n  30│const MAX_RENAME_ATTEMPTS: u32 = 10;\n  31│\n  32│/// Process-local counter making each write's tmp sibling unique (KNOWN-HAZARDS\n    │...\n  44│pub fn to_forward_slash(path: &Path) -> String {\n *45│    let mut s = path.to_string_lossy().replace('\\\\', \"/\");\n  46│    // `\\\\?\\C:\\...` -> `//?/C:/...` -> `C:/...`\n  47│    if let Some(stripped) = s.strip_prefix(\"//?/\") {\n  48│        s = stripped.to_string();\n    │...\n  52│\n *53│/// Atomically write `body` to `path` via a sibling `{name}.tmp` + rename, with\n  54│/// EBUSY retry/backoff (KNOWN-HAZARDS 5.2). A concurrent reader sees either the\n  55│/// old file or the complete new file — never a torn write. **Non-durable** — see\n  56│/// [`atomic_write_bytes`] for the durability trade-off.\n    │...\n  64│///\n *65│/// **Non-durable** (no fsync): a plain `fs::write` of the tmp + rename. Still\n *66│/// reader-atomic (old-or-new, never torn), but a hard reset between the rename\n  67│/// and the data flush can resurrect the file at full length filled with NUL. That\n  68│/// is the correct default for RECONSTRUCTIBLE state — registry snapshots, peer\n  69│/// caches, the release cache — where a post-crash NUL husk simply parse-fails to\n    │...\n  71│/// corrupt record harmless at every seam). For AUTHORITATIVE records whose loss\n *72│/// IS an incident, use [`atomic_write_bytes_durable`].\n  73│///\n  74│/// **The epoch counter was named on that list and no longer is** (KNOWN-HAZARDS\n  75│/// 7.60, WAN-TRUTH #40): it is NOT reconstructible, because nothing regenerates\n    │...\n  83│pub fn atomic_write_bytes(path: &Path, body: &[u8]) -> io::Result<()> {\n *84│    write_then_rename(path, body, false)\n  85│}\n  86│\n  87│/// **Durable** sibling of [`atomic_write_bytes`]: the tmp file is fsync'd\n *88│/// (`sync_all`) BEFORE the rename publishes the name (KNOWN-HAZARDS 5.13), so a\n  89│/// hard reset yields the complete old OR new file — never a full-length NUL\n *90│/// zero-fill (NTFS keeps the rename metadata but loses un-synced data blocks).\n  91│///\n  92│/// **Scoped, NOT a blanket default.** Durability is opt-in for the few records\n  93│/// whose loss is unrecoverable — the authoritative per-perch record\n    │...\n 107│// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n*108│pub fn atomic_write_bytes_durable(path: &Path, body: &[u8]) -> io::Result<()> {\n*109│    write_then_rename(path, body, true)\n 110│}\n 111│\n*112│/// String sibling of [`atomic_write_bytes_durable`] (durable tmp+rename).\n 113│// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n 114│pub fn atomic_write_string_durable(path: &Path, body: &str) -> io::Result<()> {\n*115│    atomic_write_bytes_durable(path, body.as_bytes())\n 116│}\n 117│\n*118│/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\n*119│/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\n 120│/// reconstructible-state default, [`atomic_write_bytes`]).\n 121│///\n 122│/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\n    │...\n 125│/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\n*126│/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\n 127│/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\n 128│/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\n*129│/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\n 130│// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\n*131│fn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\n 132│    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\n 133│    let tmp = path.with_file_name(format!(\n 134│        \"{}.tmp.{}-{}\",\n    │...\n 141│        f.write_all(body)?;\n*142│        f.sync_all()?; // durable data BEFORE the rename publishes the name\n 143│    } else {\n 144│        fs::write(&tmp, body)?;\n 145│    }\n*146│    match rename_with_retry(&tmp, path) {\n 147│        Ok(()) => Ok(()),\n 148│        Err(e) => {\n 149│            // Best-effort cleanup of the orphaned tmp; never mask the real error.\n    │...\n 158│// [impl->REQ-HAZARD-EBUSY-RENAME]\n*159│fn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {\n*160│    retry_transient(MAX_RENAME_ATTEMPTS, backoff, || fs::rename(from, to))\n 161│}\n 162│\n 163│/// Linear backoff: attempt 1 → 2ms, attempt 2 → 4ms, … capped modestly. Kept\n    │...\n 197│///\n*198│/// Windows surfaces a held-handle rename collision as `ERROR_SHARING_VIOLATION`\n 199│/// (32) or `ERROR_ACCESS_DENIED` (5). On other platforms `PermissionDenied`\n 200│/// is the analogue; everything else (NotFound, etc.) is non-transient.\n 201│fn is_transient(e: &io::Error) -> bool {\n    │...\n 222│    /// No `.tmp` sibling of any kind remains in `dir` (the unique tmp names —\n*223│    /// `{name}.tmp.{pid}-{seq}` — must all be renamed away, not just a fixed\n 224│    /// `{name}.tmp`). Glob-style: any entry whose name contains `.tmp` fails.\n 225│    fn no_tmp_sibling_remains(dir: &Path) {\n 226│        let leftover: Vec<String> = fs::read_dir(dir)\n    │...\n 246│    // [unit->REQ-HAZARD-EBUSY-RENAME] write creates the file with exact content,\n*247│    // and replaces an existing file; no `.tmp` sibling is left behind.\n 248│    #[test]\n*249│    fn atomic_write_creates_and_replaces() {\n 250│        let dir = tempfile::tempdir().unwrap();\n 251│        let target = dir.path().join(\"cursor\");\n 252│        atomic_write_string(&target, \"first\").unwrap();\n    │...\n 271│    // [unit->REQ-HAZARD-PERCH-RECORD-POWER-LOSS] the DURABLE path exercises\n*272│    // File::create → write_all → sync_all → rename: bytes (and the string wrapper)\n*273│    // round-trip byte-exact through the fsync-before-rename ordering, and the tmp is\n*274│    // renamed away. This is the opt-in used by the authoritative perch/identity writers;\n 275│    // the blanket-fsync default was reverted (it wedged daemon bringup).\n 276│    #[test]\n*277│    fn atomic_write_durable_round_trips_and_renames_tmp() {\n 278│        let dir = tempfile::tempdir().unwrap();\n 279│        let target = dir.path().join(\"rec.bin\");\n 280│        let data: Vec<u8> = (0..=255u8).collect();\n*281│        atomic_write_bytes_durable(&target, &data).unwrap();\n 282│        assert_eq!(fs::read(&target).unwrap(), data);\n 283│\n 284│        let s_target = dir.path().join(\"rec.json\");\n    │...\n 291│    // many threads hammering the SAME target concurrently must EACH return Ok — no\n*292│    // writer's rename may consume another's staged tmp (the os-error-2 loser that\n*293│    // wedged bind). A `Barrier` aligns the writers so the create→rename windows\n 294│    // actually overlap (without it the collision is a microsecond fluke and the guard\n 295│    // is decorative — it must red on the old fixed-`{name}.tmp` code). After the\n 296│    // storm the target parses complete (one full winning body, never a torn/partial).\n    │...\n 314│                    for _ in 0..ITERS {\n*315│                        barrier.wait(); // align create→rename windows across writers\n*316│                        atomic_write_bytes_durable(&target, body.as_bytes())\n 317│                            .expect(\"every concurrent write must succeed (no os-error-2 loser)\");\n 318│                    }\n 319│                })"},"isError":false,"timestamp":1789522929252}}
{"type":"message","id":"d85e8232","parentId":"daa6824a","timestamp":"2026-09-16T01:42:09.260Z","message":{"role":"toolResult","toolCallId":"call_xbpT93xQEEltrfbjVntdXxfk|fc_0cdf94810c888815016aa9f3f0591c87d19bc550e63cc979c0","toolName":"read","content":[{"type":"text","text":"fn two_host_inbound_probe_role_b() {\n    let Some(rig) = Rig::from_env(\"b\") else {\n        return;\n    };\n    let listen = rig.port_b + PROBE_OFFSET;\n    let target = SocketAddr::new(rig.peer_ip, rig.port_a + PROBE_OFFSET);\n    // BOUND BEFORE ANYTHING IS SENT: A beacons to this port cold, and a beacon\n    // that arrives before the bind is a beacon that never happened.\n    let socket = UdpSocket::bind(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), listen))\n        .unwrap_or_else(|e| panic!(\"INBOUND_PROBE: role B could not bind udp {listen}: {e}\"));\n    socket\n        .set_read_timeout(Some(Duration::from_millis(250)))\n        .expect(\"probe socket takes a read timeout\");\n    // ⚠ THE PROBES LEAVE A SEPARATE, EPHEMERAL SOCKET — this fixed one only ever\n    // RECEIVES beacons.\n    //\n    // With a fixed source port here, the tuple (B:port_b+9 → A:probe) REPEATS\n    // from run to run, so any earlier outbound from A to that port leaves return\n    // state that carries the next run's probe across a link that is actually\n    // blocked — at either layer, since WFP and Tailscale's filter both track\n    // outbound UDP flows by 4-tuple. An ephemeral source port makes each run's\n    // tuple new, so no run can poison the next BY CONSTRUCTION rather than by a\n    // cold-start convention nobody will remember. This socket only ever SENDS:\n    // the ack comes back to the fixed socket above, because A addresses it to\n    // this host's rig port (the only address either host is reachable on cold).\n    let probe_socket = UdpSocket::bind(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0))\n        .expect(\"probe sender binds an ephemeral port\");\n    println!(\n        \"TWOHOST-WEB probe role B: waiting for rendezvous up to {:?} on udp {listen} for A's beacon, then sending to {target} for {PROBE_WINDOW:?}\",\n        rig.rendezvous\n    );\n\n    let mut buf = [0u8; 64];\n    let beacon_deadline = Instant::now() + rig.rendezvous;\n    let mut heard_beacon = false;\n    while Instant::now() < beacon_deadline {\n        match socket.recv_from(&mut buf) {\n            Ok((n, from)) if buf[..n] == *PROBE_BEACON => {\n                println!(\"TWOHOST-WEB probe role B: beacon from {from} — A is up, starting the clock\");\n                heard_beacon = true;\n                break;\n            }\n            Ok((n, from)) => println!(\"TWOHOST-WEB probe role B: {n} foreign bytes from {from}\"),\n            Err(_) => {}\n        }\n    }\n\n    // THE FOURTH OUTCOME, and it gets its own name. No beacon means this half\n    // never established that the other half exists — which is a RENDEZVOUS\n    // failure, not an inbound block, and calling it INBOUND_BLOCKED would be the\n    // same conflation this whole probe was written to end, one layer up.\n    assert!(\n        heard_beacon,\n        \"PROBE_NO_BEACON: role B heard nothing from A on udp {listen} within {:?}.\nThis is NOT an inbound-block finding and must not be reported as one: it says only that A's beacons\ndid not reach this host, so this half never learned that the other half is running.\n\nLook, in this order: (1) did A's step run at all on the other host, and did it reach its 'listening on\nudp' line; (2) is A->B blocked, which is the direction this rig has always found OPEN (measured 3/3\nfrom A on 2026-09-08 while the reverse read 0/3) — if it has closed, that is a NEW box finding and\nthe more interesting one; (3) is this host's own udp {listen} bound by something else.\nA's own no-probe result is PROBE_RENDEZVOUS_UNVERIFIED: inspect both matching job timelines before\nclassifying its silence as a blocked link.\",\n        rig.rendezvous\n    );\n\n    // The window that actually measures the link, started from evidence.\n    let deadline = Instant::now() + PROBE_WINDOW;\n    let mut sent = 0usize;\n    let mut acked = false;\n    while Instant::now() < deadline {\n        match probe_socket.send_to(PROBE_MAGIC, target) {\n            Ok(_) => sent += 1,\n            Err(e) => println!(\"TWOHOST-WEB probe role B: send error (continuing): {e}\"),\n        }\n        // The ack arrives on the FIXED socket, not the ephemeral sender: A sends\n        // it to this host's rig port so it is addressable cold (see A's cell).\n        match socket.recv_from(&mut buf) {\n            Ok((n, from)) if buf[..n] == *PROBE_ACK => {\n                println!(\"TWOHOST-WEB probe role B: ack from {from} after {sent} datagram(s)\");\n                acked = true;\n                break;\n            }\n            // A beacon is not an ack. Reading one as the other would stop the\n            // clock on the evidence that started it.\n            Ok((n, _)) if buf[..n] == *PROBE_BEACON => {}\n            Ok((n, from)) => println!(\"TWOHOST-WEB probe role B: {n} foreign bytes from {from}\"),\n            Err(_) => {}\n        }\n    }\n    println!(\"TWOHOST-WEB probe role B: {sent} datagrams sent to {target}, acked={acked}\");\n    assert!(sent > 0, \"role B could not send a single probe datagram to {target}\");\n\n    // B's fast RED, and it is the one IR-89 promises: ten seconds, its own name,\n    // with \"A is not up yet\" already excluded by the beacon above.\n    assert!(\n        acked,\n        \"INBOUND_BLOCKED: role B sent {sent} datagram(s) to {target} in {PROBE_WINDOW:?} and A never\nacked — while A's beacons were arriving here the whole time, so A IS UP and the reverse direction is\nopen. B->A is blocked.\n\nTHERE ARE TWO LAYERS AND EITHER ONE ALONE PRODUCES THIS EXACT SILENCE; the receiver's own cell\ncarries the full text and the two operator fixes. In short: LAYER 1 is the receiver's per-executable\nhost firewall (allow INBOUND UDP for the rig's port range from this host, PORT+REMOTE scoped, never\nprogram-scoped), LAYER 2 is the asymmetric tailnet ACL (this sender must appear among the receiver's\nPacketFilter permitted inbound sources — check `tailscale debug netmap` on the RECEIVER). Fixing only\none is the mistake: it is NECESSARY, NOT SUFFICIENT, and the run reds identically afterwards.\n\nDo not spend the ceremony's budget on this: the dial below would fail the same way for the same\nreason, and would report it as a pairing failure.\"\n    );\n}\n\n// ── Role B: the owner ─────────────────────────────────────────────────────\n\n/// Serves for the rig deadline: two registered files and one endpoint-scoped\n/// `WEB` deny for A's node on the locked one. The dispatcher is the\n/// production loop; the gate it runs is the production gate.\n// [int->REQ-WEB-CROSS-NODE-PROXY]\n// [int->REQ-WEB-ACCESS-SURFACE]\n#[test]\nfn two_host_web_role_b() {\n    let Some(rig) = Rig::from_env(\"b\") else {\n        return;\n    };\n    let home = TempDir::new().expect(\"temp home\");\n    std::env::set_var(\"SPT_HOME\", home.path());\n    let scratch = TempDir::new().expect(\"scratch\");\n    println!(\"TWOHOST-WEB role B: node {} serving on udp {}\", rig.b_hex(), rig.port_b);\n\n    let mut subnets = SubnetStore::load();\n    subnets\n        .add_joined(&rig.subnet, *rig.totp.as_bytes(), 1, None, None)\n        .expect(\"seed subnet\");\n    subnets.save().expect(\"save subnets\");\n\n    // The served files, registered as two different endpoints would.\n    let open = home.path().join(\"open.md\");\n    let locked = home.path().join(\"locked.md\");\n    std::fs::write(&open, OPEN_BYTES).expect(\"write open.md\");\n    std::fs::write(&locked, LOCKED_BYTES).expect(\"write locked.md\");\n    let registry_path = perch::serving_registry_file_in(home.path());\n    let mut registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\n    registry.add_reference(&open, Some(\"open.md\"), Some(OPEN_ENDPOINT), 1).expect(\"register open.md\");\n    registry.add_reference(&locked, Some(\"locked.md\"), Some(LOCKED_ENDPOINT), 1).expect(\"register locked.md\");\n    registry.save_at(&registry_path).expect(\"save registry\");\n\n    // The deny: A's node may not read what `w1-locked` serves. Endpoint\n    // scope, so `w1-open`'s file stays open — the sibling the deny cell's\n    // mutation arm must leave intact.\n    let access_path = home.path().join(\"identity\").join(\"trust\").join(\"access.json\");\n    let mut store = AccessStore::load_from(&access_path);\n    store.endpoint_mut(LOCKED_ENDPOINT).rules.push(AccessRule {\n        // The DENY cell's node, named by its offset. With one shared key this\n        // rule happened to cover every A cell; now it covers exactly the cell\n        // whose refusal the arm is about, which is what it always meant.\n        subject: Subject::Node { node: rig.a_hex_for(DENY_OFFSET) },\n        surfaces: vec![\"WEB\".to_owned()],\n        origin: OriginQualifier::Any,\n        provenance: Provenance::Manual,\n        decision: RuleDecision::Deny,\n    });\n    store.save_to(&access_path).expect(\"write the deny\");\n\n    // [int->REQ-WEB-URL-BOUND-PORT]\n    // [int->REQ-SERVING-FIXTURE-CONTROL-PLANE]\n    let (serving, docs_port) = ServingFixture::start(\n        home.path(),\n        B_LABEL,\n        scratch.path().join(\"effects.log\"),\n        Some(rig_net_host(rig.id_b.clone(), rig.port_b)),\n    );\n    let broker_name = serving.broker_name.clone();\n    println!(\n        \"TWOHOST-WEB role B: listener {docs_port}, broker udp {} ({broker_name}), pid {}\",\n        rig.port_b,\n        std::process::id()\n    );\n    let registry_host = Arc::new(RegistryHost::new_at(\n        &rig.b_hex(),\n        EpochSource::load_from(&perch::epoch_file()),\n        spt_daemon::presence::registry_snapshot_dir(),\n    ));\n\n    // ── F19a: A must be a ROSTER MEMBER before its row can be merged at all ──\n    //\n    // `RegistryGatePolicy::admits` is `member subnet ∧ roster.is_member(subnet,\n    // origin)`, fail-closed on both, so without this the merge below answers\n    // `UntrustedOrigin` and writes nothing — MEASURED on the first run of this\n    // fix (`verdicts [UntrustedOrigin]`, and the precondition assert caught it at\n    // the seed rather than 120 s later at the helper deadline, which is the whole\n    // reason the assert reads through the product's own verb).\n    //\n    // This is the SAME direction gap as the row itself, one layer down: A's\n    // `requester` seeds a roster entry for B (and B's peer address), and nothing\n    // ever seeded the reverse. One-way knowledge in a rig that only ever ran one\n    // way — it could not surface until a cell needed B to judge something A said.\n    // Address stays `None`: B dials A by `rig.a_addr(HELPER_OFFSET)` on the WAN\n    // send below, never out of the roster, so seeding one would be an unused\n    // second spelling of A's address.\n    let mut roster = RosterStore::load();\n    roster.merge_entry(RosterEntry {\n        pubkey_hex: rig.a_hex_for(HELPER_OFFSET),\n        subnet: rig.subnet.clone(),\n        label: \"rig-web-a\".to_owned(),\n        machine_id: \"rig-web-a-machine\".to_owned(),\n        address: None,\n        last_seen: \"1\".to_owned(),\n        lease_epoch: 1,\n    });\n    roster.save().expect(\"seed A as a roster member\");\n\n    // ── F19: A's audience row, which `serve_for`'s SECOND conjunct requires ──\n    //\n    // `webproxy::serve_for` refuses unless `node_hosting_endpoint(home, audience)`\n…\n}\n\n[Showing lines 569-783 of 1413. Use :784 to continue]"}],"details":{"truncation":{"content":"fn two_host_inbound_probe_role_b() {\r\n    let Some(rig) = Rig::from_env(\"b\") else {\r\n        return;\r\n    };\r\n    let listen = rig.port_b + PROBE_OFFSET;\r\n    let target = SocketAddr::new(rig.peer_ip, rig.port_a + PROBE_OFFSET);\r\n    // BOUND BEFORE ANYTHING IS SENT: A beacons to this port cold, and a beacon\r\n    // that arrives before the bind is a beacon that never happened.\r\n    let socket = UdpSocket::bind(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), listen))\r\n        .unwrap_or_else(|e| panic!(\"INBOUND_PROBE: role B could not bind udp {listen}: {e}\"));\r\n    socket\r\n        .set_read_timeout(Some(Duration::from_millis(250)))\r\n        .expect(\"probe socket takes a read timeout\");\r\n    // ⚠ THE PROBES LEAVE A SEPARATE, EPHEMERAL SOCKET — this fixed one only ever\r\n    // RECEIVES beacons.\r\n    //\r\n    // With a fixed source port here, the tuple (B:port_b+9 → A:probe) REPEATS\r\n    // from run to run, so any earlier outbound from A to that port leaves return\r\n    // state that carries the next run's probe across a link that is actually\r\n    // blocked — at either layer, since WFP and Tailscale's filter both track\r\n    // outbound UDP flows by 4-tuple. An ephemeral source port makes each run's\r\n    // tuple new, so no run can poison the next BY CONSTRUCTION rather than by a\r\n    // cold-start convention nobody will remember. This socket only ever SENDS:\r\n    // the ack comes back to the fixed socket above, because A addresses it to\r\n    // this host's rig port (the only address either host is reachable on cold).\r\n    let probe_socket = UdpSocket::bind(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0))\r\n        .expect(\"probe sender binds an ephemeral port\");\r\n    println!(\r\n        \"TWOHOST-WEB probe role B: waiting for rendezvous up to {:?} on udp {listen} for A's beacon, then sending to {target} for {PROBE_WINDOW:?}\",\r\n        rig.rendezvous\r\n    );\r\n\r\n    let mut buf = [0u8; 64];\r\n    let beacon_deadline = Instant::now() + rig.rendezvous;\r\n    let mut heard_beacon = false;\r\n    while Instant::now() < beacon_deadline {\r\n        match socket.recv_from(&mut buf) {\r\n            Ok((n, from)) if buf[..n] == *PROBE_BEACON => {\r\n                println!(\"TWOHOST-WEB probe role B: beacon from {from} — A is up, starting the clock\");\r\n                heard_beacon = true;\r\n                break;\r\n            }\r\n            Ok((n, from)) => println!(\"TWOHOST-WEB probe role B: {n} foreign bytes from {from}\"),\r\n            Err(_) => {}\r\n        }\r\n    }\r\n\r\n    // THE FOURTH OUTCOME, and it gets its own name. No beacon means this half\r\n    // never established that the other half exists — which is a RENDEZVOUS\r\n    // failure, not an inbound block, and calling it INBOUND_BLOCKED would be the\r\n    // same conflation this whole probe was written to end, one layer up.\r\n    assert!(\r\n        heard_beacon,\r\n        \"PROBE_NO_BEACON: role B heard nothing from A on udp {listen} within {:?}.\r\nThis is NOT an inbound-block finding and must not be reported as one: it says only that A's beacons\r\ndid not reach this host, so this half never learned that the other half is running.\r\n\r\nLook, in this order: (1) did A's step run at all on the other host, and did it reach its 'listening on\r\nudp' line; (2) is A->B blocked, which is the direction this rig has always found OPEN (measured 3/3\r\nfrom A on 2026-09-08 while the reverse read 0/3) — if it has closed, that is a NEW box finding and\r\nthe more interesting one; (3) is this host's own udp {listen} bound by something else.\r\nA's own no-probe result is PROBE_RENDEZVOUS_UNVERIFIED: inspect both matching job timelines before\r\nclassifying its silence as a blocked link.\",\r\n        rig.rendezvous\r\n    );\r\n\r\n    // The window that actually measures the link, started from evidence.\r\n    let deadline = Instant::now() + PROBE_WINDOW;\r\n    let mut sent = 0usize;\r\n    let mut acked = false;\r\n    while Instant::now() < deadline {\r\n        match probe_socket.send_to(PROBE_MAGIC, target) {\r\n            Ok(_) => sent += 1,\r\n            Err(e) => println!(\"TWOHOST-WEB probe role B: send error (continuing): {e}\"),\r\n        }\r\n        // The ack arrives on the FIXED socket, not the ephemeral sender: A sends\r\n        // it to this host's rig port so it is addressable cold (see A's cell).\r\n        match socket.recv_from(&mut buf) {\r\n            Ok((n, from)) if buf[..n] == *PROBE_ACK => {\r\n                println!(\"TWOHOST-WEB probe role B: ack from {from} after {sent} datagram(s)\");\r\n                acked = true;\r\n                break;\r\n            }\r\n            // A beacon is not an ack. Reading one as the other would stop the\r\n            // clock on the evidence that started it.\r\n            Ok((n, _)) if buf[..n] == *PROBE_BEACON => {}\r\n            Ok((n, from)) => println!(\"TWOHOST-WEB probe role B: {n} foreign bytes from {from}\"),\r\n            Err(_) => {}\r\n        }\r\n    }\r\n    println!(\"TWOHOST-WEB probe role B: {sent} datagrams sent to {target}, acked={acked}\");\r\n    assert!(sent > 0, \"role B could not send a single probe datagram to {target}\");\r\n\r\n    // B's fast RED, and it is the one IR-89 promises: ten seconds, its own name,\r\n    // with \"A is not up yet\" already excluded by the beacon above.\r\n    assert!(\r\n        acked,\r\n        \"INBOUND_BLOCKED: role B sent {sent} datagram(s) to {target} in {PROBE_WINDOW:?} and A never\r\nacked — while A's beacons were arriving here the whole time, so A IS UP and the reverse direction is\r\nopen. B->A is blocked.\r\n\r\nTHERE ARE TWO LAYERS AND EITHER ONE ALONE PRODUCES THIS EXACT SILENCE; the receiver's own cell\r\ncarries the full text and the two operator fixes. In short: LAYER 1 is the receiver's per-executable\r\nhost firewall (allow INBOUND UDP for the rig's port range from this host, PORT+REMOTE scoped, never\r\nprogram-scoped), LAYER 2 is the asymmetric tailnet ACL (this sender must appear among the receiver's\r\nPacketFilter permitted inbound sources — check `tailscale debug netmap` on the RECEIVER). Fixing only\r\none is the mistake: it is NECESSARY, NOT SUFFICIENT, and the run reds identically afterwards.\r\n\r\nDo not spend the ceremony's budget on this: the dial below would fail the same way for the same\r\nreason, and would report it as a pairing failure.\"\r\n    );\r\n}\r\n\r\n// ── Role B: the owner ─────────────────────────────────────────────────────\r\n\r\n/// Serves for the rig deadline: two registered files and one endpoint-scoped\r\n/// `WEB` deny for A's node on the locked one. The dispatcher is the\r\n/// production loop; the gate it runs is the production gate.\r\n// [int->REQ-WEB-CROSS-NODE-PROXY]\r\n// [int->REQ-WEB-ACCESS-SURFACE]\r\n#[test]\r\nfn two_host_web_role_b() {\r\n    let Some(rig) = Rig::from_env(\"b\") else {\r\n        return;\r\n    };\r\n    let home = TempDir::new().expect(\"temp home\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n    let scratch = TempDir::new().expect(\"scratch\");\r\n    println!(\"TWOHOST-WEB role B: node {} serving on udp {}\", rig.b_hex(), rig.port_b);\r\n\r\n    let mut subnets = SubnetStore::load();\r\n    subnets\r\n        .add_joined(&rig.subnet, *rig.totp.as_bytes(), 1, None, None)\r\n        .expect(\"seed subnet\");\r\n    subnets.save().expect(\"save subnets\");\r\n\r\n    // The served files, registered as two different endpoints would.\r\n    let open = home.path().join(\"open.md\");\r\n    let locked = home.path().join(\"locked.md\");\r\n    std::fs::write(&open, OPEN_BYTES).expect(\"write open.md\");\r\n    std::fs::write(&locked, LOCKED_BYTES).expect(\"write locked.md\");\r\n    let registry_path = perch::serving_registry_file_in(home.path());\r\n    let mut registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\r\n    registry.add_reference(&open, Some(\"open.md\"), Some(OPEN_ENDPOINT), 1).expect(\"register open.md\");\r\n    registry.add_reference(&locked, Some(\"locked.md\"), Some(LOCKED_ENDPOINT), 1).expect(\"register locked.md\");\r\n    registry.save_at(&registry_path).expect(\"save registry\");\r\n\r\n    // The deny: A's node may not read what `w1-locked` serves. Endpoint\r\n    // scope, so `w1-open`'s file stays open — the sibling the deny cell's\r\n    // mutation arm must leave intact.\r\n    let access_path = home.path().join(\"identity\").join(\"trust\").join(\"access.json\");\r\n    let mut store = AccessStore::load_from(&access_path);\r\n    store.endpoint_mut(LOCKED_ENDPOINT).rules.push(AccessRule {\r\n        // The DENY cell's node, named by its offset. With one shared key this\r\n        // rule happened to cover every A cell; now it covers exactly the cell\r\n        // whose refusal the arm is about, which is what it always meant.\r\n        subject: Subject::Node { node: rig.a_hex_for(DENY_OFFSET) },\r\n        surfaces: vec![\"WEB\".to_owned()],\r\n        origin: OriginQualifier::Any,\r\n        provenance: Provenance::Manual,\r\n        decision: RuleDecision::Deny,\r\n    });\r\n    store.save_to(&access_path).expect(\"write the deny\");\r\n\r\n    // [int->REQ-WEB-URL-BOUND-PORT]\r\n    // [int->REQ-SERVING-FIXTURE-CONTROL-PLANE]\r\n    let (serving, docs_port) = ServingFixture::start(\r\n        home.path(),\r\n        B_LABEL,\r\n        scratch.path().join(\"effects.log\"),\r\n        Some(rig_net_host(rig.id_b.clone(), rig.port_b)),\r\n    );\r\n    let broker_name = serving.broker_name.clone();\r\n    println!(\r\n        \"TWOHOST-WEB role B: listener {docs_port}, broker udp {} ({broker_name}), pid {}\",\r\n        rig.port_b,\r\n        std::process::id()\r\n    );\r\n    let registry_host = Arc::new(RegistryHost::new_at(\r\n        &rig.b_hex(),\r\n        EpochSource::load_from(&perch::epoch_file()),\r\n        spt_daemon::presence::registry_snapshot_dir(),\r\n    ));\r\n\r\n    // ── F19a: A must be a ROSTER MEMBER before its row can be merged at all ──\r\n    //\r\n    // `RegistryGatePolicy::admits` is `member subnet ∧ roster.is_member(subnet,\r\n    // origin)`, fail-closed on both, so without this the merge below answers\r\n    // `UntrustedOrigin` and writes nothing — MEASURED on the first run of this\r\n    // fix (`verdicts [UntrustedOrigin]`, and the precondition assert caught it at\r\n    // the seed rather than 120 s later at the helper deadline, which is the whole\r\n    // reason the assert reads through the product's own verb).\r\n    //\r\n    // This is the SAME direction gap as the row itself, one layer down: A's\r\n    // `requester` seeds a roster entry for B (and B's peer address), and nothing\r\n    // ever seeded the reverse. One-way knowledge in a rig that only ever ran one\r\n    // way — it could not surface until a cell needed B to judge something A said.\r\n    // Address stays `None`: B dials A by `rig.a_addr(HELPER_OFFSET)` on the WAN\r\n    // send below, never out of the roster, so seeding one would be an unused\r\n    // second spelling of A's address.\r\n    let mut roster = RosterStore::load();\r\n    roster.merge_entry(RosterEntry {\r\n        pubkey_hex: rig.a_hex_for(HELPER_OFFSET),\r\n        subnet: rig.subnet.clone(),\r\n        label: \"rig-web-a\".to_owned(),\r\n        machine_id: \"rig-web-a-machine\".to_owned(),\r\n        address: None,\r\n        last_seen: \"1\".to_owned(),\r\n        lease_epoch: 1,\r\n    });\r\n    roster.save().expect(\"seed A as a roster member\");\r\n\r\n    // ── F19: A's audience row, which `serve_for`'s SECOND conjunct requires ──\r\n    //\r\n    // `webproxy::serve_for` refuses unless `node_hosting_endpoint(home, audience)`\r","truncated":true,"truncatedBy":"lines","totalLines":845,"totalBytes":11372,"outputLines":215,"outputBytes":11372,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1413,"displayContent":{"text":"fn two_host_inbound_probe_role_b() {\r\n    let Some(rig) = Rig::from_env(\"b\") else {\r\n        return;\r\n    };\r\n    let listen = rig.port_b + PROBE_OFFSET;\r\n    let target = SocketAddr::new(rig.peer_ip, rig.port_a + PROBE_OFFSET);\r\n    // BOUND BEFORE ANYTHING IS SENT: A beacons to this port cold, and a beacon\r\n    // that arrives before the bind is a beacon that never happened.\r\n    let socket = UdpSocket::bind(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), listen))\r\n        .unwrap_or_else(|e| panic!(\"INBOUND_PROBE: role B could not bind udp {listen}: {e}\"));\r\n    socket\r\n        .set_read_timeout(Some(Duration::from_millis(250)))\r\n        .expect(\"probe socket takes a read timeout\");\r\n    // ⚠ THE PROBES LEAVE A SEPARATE, EPHEMERAL SOCKET — this fixed one only ever\r\n    // RECEIVES beacons.\r\n    //\r\n    // With a fixed source port here, the tuple (B:port_b+9 → A:probe) REPEATS\r\n    // from run to run, so any earlier outbound from A to that port leaves return\r\n    // state that carries the next run's probe across a link that is actually\r\n    // blocked — at either layer, since WFP and Tailscale's filter both track\r\n    // outbound UDP flows by 4-tuple. An ephemeral source port makes each run's\r\n    // tuple new, so no run can poison the next BY CONSTRUCTION rather than by a\r\n    // cold-start convention nobody will remember. This socket only ever SENDS:\r\n    // the ack comes back to the fixed socket above, because A addresses it to\r\n    // this host's rig port (the only address either host is reachable on cold).\r\n    let probe_socket = UdpSocket::bind(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0))\r\n        .expect(\"probe sender binds an ephemeral port\");\r\n    println!(\r\n        \"TWOHOST-WEB probe role B: waiting for rendezvous up to {:?} on udp {listen} for A's beacon, then sending to {target} for {PROBE_WINDOW:?}\",\r\n        rig.rendezvous\r\n    );\r\n\r\n    let mut buf = [0u8; 64];\r\n    let beacon_deadline = Instant::now() + rig.rendezvous;\r\n    let mut heard_beacon = false;\r\n    while Instant::now() < beacon_deadline {\r\n        match socket.recv_from(&mut buf) {\r\n            Ok((n, from)) if buf[..n] == *PROBE_BEACON => {\r\n                println!(\"TWOHOST-WEB probe role B: beacon from {from} — A is up, starting the clock\");\r\n                heard_beacon = true;\r\n                break;\r\n            }\r\n            Ok((n, from)) => println!(\"TWOHOST-WEB probe role B: {n} foreign bytes from {from}\"),\r\n            Err(_) => {}\r\n        }\r\n    }\r\n\r\n    // THE FOURTH OUTCOME, and it gets its own name. No beacon means this half\r\n    // never established that the other half exists — which is a RENDEZVOUS\r\n    // failure, not an inbound block, and calling it INBOUND_BLOCKED would be the\r\n    // same conflation this whole probe was written to end, one layer up.\r\n    assert!(\r\n        heard_beacon,\r\n        \"PROBE_NO_BEACON: role B heard nothing from A on udp {listen} within {:?}.\r\nThis is NOT an inbound-block finding and must not be reported as one: it says only that A's beacons\r\ndid not reach this host, so this half never learned that the other half is running.\r\n\r\nLook, in this order: (1) did A's step run at all on the other host, and did it reach its 'listening on\r\nudp' line; (2) is A->B blocked, which is the direction this rig has always found OPEN (measured 3/3\r\nfrom A on 2026-09-08 while the reverse read 0/3) — if it has closed, that is a NEW box finding and\r\nthe more interesting one; (3) is this host's own udp {listen} bound by something else.\r\nA's own no-probe result is PROBE_RENDEZVOUS_UNVERIFIED: inspect both matching job timelines before\r\nclassifying its silence as a blocked link.\",\r\n        rig.rendezvous\r\n    );\r\n\r\n    // The window that actually measures the link, started from evidence.\r\n    let deadline = Instant::now() + PROBE_WINDOW;\r\n    let mut sent = 0usize;\r\n    let mut acked = false;\r\n    while Instant::now() < deadline {\r\n        match probe_socket.send_to(PROBE_MAGIC, target) {\r\n            Ok(_) => sent += 1,\r\n            Err(e) => println!(\"TWOHOST-WEB probe role B: send error (continuing): {e}\"),\r\n        }\r\n        // The ack arrives on the FIXED socket, not the ephemeral sender: A sends\r\n        // it to this host's rig port so it is addressable cold (see A's cell).\r\n        match socket.recv_from(&mut buf) {\r\n            Ok((n, from)) if buf[..n] == *PROBE_ACK => {\r\n                println!(\"TWOHOST-WEB probe role B: ack from {from} after {sent} datagram(s)\");\r\n                acked = true;\r\n                break;\r\n            }\r\n            // A beacon is not an ack. Reading one as the other would stop the\r\n            // clock on the evidence that started it.\r\n            Ok((n, _)) if buf[..n] == *PROBE_BEACON => {}\r\n            Ok((n, from)) => println!(\"TWOHOST-WEB probe role B: {n} foreign bytes from {from}\"),\r\n            Err(_) => {}\r\n        }\r\n    }\r\n    println!(\"TWOHOST-WEB probe role B: {sent} datagrams sent to {target}, acked={acked}\");\r\n    assert!(sent > 0, \"role B could not send a single probe datagram to {target}\");\r\n\r\n    // B's fast RED, and it is the one IR-89 promises: ten seconds, its own name,\r\n    // with \"A is not up yet\" already excluded by the beacon above.\r\n    assert!(\r\n        acked,\r\n        \"INBOUND_BLOCKED: role B sent {sent} datagram(s) to {target} in {PROBE_WINDOW:?} and A never\r\nacked — while A's beacons were arriving here the whole time, so A IS UP and the reverse direction is\r\nopen. B->A is blocked.\r\n\r\nTHERE ARE TWO LAYERS AND EITHER ONE ALONE PRODUCES THIS EXACT SILENCE; the receiver's own cell\r\ncarries the full text and the two operator fixes. In short: LAYER 1 is the receiver's per-executable\r\nhost firewall (allow INBOUND UDP for the rig's port range from this host, PORT+REMOTE scoped, never\r\nprogram-scoped), LAYER 2 is the asymmetric tailnet ACL (this sender must appear among the receiver's\r\nPacketFilter permitted inbound sources — check `tailscale debug netmap` on the RECEIVER). Fixing only\r\none is the mistake: it is NECESSARY, NOT SUFFICIENT, and the run reds identically afterwards.\r\n\r\nDo not spend the ceremony's budget on this: the dial below would fail the same way for the same\r\nreason, and would report it as a pairing failure.\"\r\n    );\r\n}\r\n\r\n// ── Role B: the owner ─────────────────────────────────────────────────────\r\n\r\n/// Serves for the rig deadline: two registered files and one endpoint-scoped\r\n/// `WEB` deny for A's node on the locked one. The dispatcher is the\r\n/// production loop; the gate it runs is the production gate.\r\n// [int->REQ-WEB-CROSS-NODE-PROXY]\r\n// [int->REQ-WEB-ACCESS-SURFACE]\r\n#[test]\r\nfn two_host_web_role_b() {\r\n    let Some(rig) = Rig::from_env(\"b\") else {\r\n        return;\r\n    };\r\n    let home = TempDir::new().expect(\"temp home\");\r\n    std::env::set_var(\"SPT_HOME\", home.path());\r\n    let scratch = TempDir::new().expect(\"scratch\");\r\n    println!(\"TWOHOST-WEB role B: node {} serving on udp {}\", rig.b_hex(), rig.port_b);\r\n\r\n    let mut subnets = SubnetStore::load();\r\n    subnets\r\n        .add_joined(&rig.subnet, *rig.totp.as_bytes(), 1, None, None)\r\n        .expect(\"seed subnet\");\r\n    subnets.save().expect(\"save subnets\");\r\n\r\n    // The served files, registered as two different endpoints would.\r\n    let open = home.path().join(\"open.md\");\r\n    let locked = home.path().join(\"locked.md\");\r\n    std::fs::write(&open, OPEN_BYTES).expect(\"write open.md\");\r\n    std::fs::write(&locked, LOCKED_BYTES).expect(\"write locked.md\");\r\n    let registry_path = perch::serving_registry_file_in(home.path());\r\n    let mut registry = ServingRegistry::load_at(&registry_path).expect(\"registry\");\r\n    registry.add_reference(&open, Some(\"open.md\"), Some(OPEN_ENDPOINT), 1).expect(\"register open.md\");\r\n    registry.add_reference(&locked, Some(\"locked.md\"), Some(LOCKED_ENDPOINT), 1).expect(\"register locked.md\");\r\n    registry.save_at(&registry_path).expect(\"save registry\");\r\n\r\n    // The deny: A's node may not read what `w1-locked` serves. Endpoint\r\n    // scope, so `w1-open`'s file stays open — the sibling the deny cell's\r\n    // mutation arm must leave intact.\r\n    let access_path = home.path().join(\"identity\").join(\"trust\").join(\"access.json\");\r\n    let mut store = AccessStore::load_from(&access_path);\r\n    store.endpoint_mut(LOCKED_ENDPOINT).rules.push(AccessRule {\r\n        // The DENY cell's node, named by its offset. With one shared key this\r\n        // rule happened to cover every A cell; now it covers exactly the cell\r\n        // whose refusal the arm is about, which is what it always meant.\r\n        subject: Subject::Node { node: rig.a_hex_for(DENY_OFFSET) },\r\n        surfaces: vec![\"WEB\".to_owned()],\r\n        origin: OriginQualifier::Any,\r\n        provenance: Provenance::Manual,\r\n        decision: RuleDecision::Deny,\r\n    });\r\n    store.save_to(&access_path).expect(\"write the deny\");\r\n\r\n    // [int->REQ-WEB-URL-BOUND-PORT]\r\n    // [int->REQ-SERVING-FIXTURE-CONTROL-PLANE]\r\n    let (serving, docs_port) = ServingFixture::start(\r\n        home.path(),\r\n        B_LABEL,\r\n        scratch.path().join(\"effects.log\"),\r\n        Some(rig_net_host(rig.id_b.clone(), rig.port_b)),\r\n    );\r\n    let broker_name = serving.broker_name.clone();\r\n    println!(\r\n        \"TWOHOST-WEB role B: listener {docs_port}, broker udp {} ({broker_name}), pid {}\",\r\n        rig.port_b,\r\n        std::process::id()\r\n    );\r\n    let registry_host = Arc::new(RegistryHost::new_at(\r\n        &rig.b_hex(),\r\n        EpochSource::load_from(&perch::epoch_file()),\r\n        spt_daemon::presence::registry_snapshot_dir(),\r\n    ));\r\n\r\n    // ── F19a: A must be a ROSTER MEMBER before its row can be merged at all ──\r\n    //\r\n    // `RegistryGatePolicy::admits` is `member subnet ∧ roster.is_member(subnet,\r\n    // origin)`, fail-closed on both, so without this the merge below answers\r\n    // `UntrustedOrigin` and writes nothing — MEASURED on the first run of this\r\n    // fix (`verdicts [UntrustedOrigin]`, and the precondition assert caught it at\r\n    // the seed rather than 120 s later at the helper deadline, which is the whole\r\n    // reason the assert reads through the product's own verb).\r\n    //\r\n    // This is the SAME direction gap as the row itself, one layer down: A's\r\n    // `requester` seeds a roster entry for B (and B's peer address), and nothing\r\n    // ever seeded the reverse. One-way knowledge in a rig that only ever ran one\r\n    // way — it could not surface until a cell needed B to judge something A said.\r\n    // Address stays `None`: B dials A by `rig.a_addr(HELPER_OFFSET)` on the WAN\r\n    // send below, never out of the roster, so seeding one would be an unused\r\n    // second spelling of A's address.\r\n    let mut roster = RosterStore::load();\r\n    roster.merge_entry(RosterEntry {\r\n        pubkey_hex: rig.a_hex_for(HELPER_OFFSET),\r\n        subnet: rig.subnet.clone(),\r\n        label: \"rig-web-a\".to_owned(),\r\n        machine_id: \"rig-web-a-machine\".to_owned(),\r\n        address: None,\r\n        last_seen: \"1\".to_owned(),\r\n        lease_epoch: 1,\r\n    });\r\n    roster.save().expect(\"seed A as a roster member\");\r\n\r\n    // ── F19: A's audience row, which `serve_for`'s SECOND conjunct requires ──\r\n    //\r\n    // `webproxy::serve_for` refuses unless `node_hosting_endpoint(home, audience)`\r\n…\n}","startLine":569,"lineNumbers":[569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,null,1069]},"fileSize":73122,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\tests\\twohost_web.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1413,"totalBytes":11372,"outputLines":215,"outputBytes":11372,"shownRange":{"start":569,"end":783},"nextOffset":784}}},"isError":false,"timestamp":1789522929260}}
{"type":"message","id":"6e2ee04c","parentId":"d85e8232","timestamp":"2026-09-16T01:42:09.265Z","message":{"role":"toolResult","toolCallId":"call_HuidBG8Wf2CvnQAn72FSEa5b|fc_0cdf94810c888815016aa9f3f0592487d1828116d68a324b06","toolName":"read","content":[{"type":"text","text":"fn an_attachment_is_snapshot_served_fetched_back_and_named_by_its_message() {\n…\n    assert_eq!(std::fs::read(&beta_back).unwrap(), b\"beta bytes\");\n\n    // ══ ARM 12 — the TTL REAPER: bytes and entry retired TOGETHER ═══════════\n    // F1. ARM 10 proves an expired link stops SERVING; masking at serve time\n    // does that on its own. This arm is about the disk: without a reaper on the\n    // daemon's pulse, `$SPT_HOME/serve/snapshots/` grows forever and the entry\n    // never leaves the registry. The REQ promises both, deleted together, with\n    // the count logged.\n    //\n    // The guard is asserted in the same arm, because a reaper that deletes files\n    // is exactly the code that must never delete the WRONG file: a REFERENCE\n    // entry (`ServedKind::File`) whose ttl has expired must lose its ENTRY and\n    // keep its BYTES — the path there is the user's own file, not a snapshot\n    // this node minted.\n    // [int->REQ-WEB-ATTACHMENT-PULL]\n    let registry_path = spt_store::perch::serving_registry_file_in(home);\n    let snapshot_of = |name: &str| -> Option<PathBuf> {\n        let registry = spt_store::serving::ServingRegistry::load_at(&registry_path).ok()?;\n        let found = registry\n            .entries()\n            .find(|entry| entry.served_name == name)\n            .map(|entry| entry.path.clone());\n        found\n    };\n    // This arm mints its OWN short-lived attachment rather than reusing ARM\n    // 10's. Measured, not guessed: the rig's whole run is ~8.7s and the reaper\n    // ticks every 5s, so at least one sweep lands mid-test, and ARM 11 spends\n    // six child processes between ARM 10's send and here. Depending on ARM 10's\n    // entry to still be present-but-unreaped is a race the reaper WINS\n    // sometimes — and it would have failed on a precondition, reading as a\n    // product defect on a golden run rather than as the rig's own timing.\n    let doomed_source = work.join(\"reap-me.md\");\n    std::fs::write(&doomed_source, b\"bytes with a short lifetime\").unwrap();\n    let doomed_send = run(\n        home,\n        &[\n            \"send\", TARGET, \"--from\", SENDER,\n            \"--attachment\", doomed_source.to_str().unwrap(),\n            // A LIFETIME THAT CANNOT EXPIRE DURING THIS TEST. The arm used to\n            // mint a short-lived entry and race it: 1s, then widened to 5s with\n            // the note that \"five seconds cannot expire before the capture\n            // below\". On a loaded golden runner five seconds DID expire before\n            // the capture — the entry was already reaped when this arm read the\n            // registry, and it panicked as \"not in the registry\", which reads as\n            // a product defect rather than as the rig's own clock.\n            //\n            // Widening the window again would be the SAME FIX THAT ALREADY\n            // FAILED ONCE, just with a bigger number: any wall-clock margin is a\n            // bet against the slowest machine that will ever run this. So the\n            // lifetime is now long enough to be irrelevant, and the EXPIRY is\n            // driven deliberately below, through the registry writer this arm\n            // already uses to plant its guard entry. No wall clock decides\n            // anything here any more.\n            \"--ttl\", \"3600s\",\n        ],\n        Some(\"this one is for the reaper\"),\n    );\n    // Elapsed since the send RETURNED, so any future red carries a number instead\n    // of a guess. It bounds the CAPTURE only — the registration happens inside\n    // the daemon before the send's delivery leg finishes, so this is not the\n    // register-to-read gap and must not be quoted as one. Bound at the point of\n    // use rather than pre-declared: the instant must still be taken AFTER `run`\n    // returns, and a late-initialised binding buys nothing for that.\n    let since_send = std::time::Instant::now();\n    assert!(doomed_send.status.success(), \"ttl send: {}\", stderr(&doomed_send));\n    let doomed_snapshot = snapshot_of(\"reap-me.md\").unwrap_or_else(|| {\n        let listed = run(home, &[\"serve\", \"list\", \"--json\"], None);\n        panic!(\n            \"the attachment this arm just registered is not in the registry {} ms after the send RETURNED (this bounds the capture, not the register-to-read gap: the daemon's registration precedes the send's own delivery leg).\nsend stderr: {}\nregistry: {}\",\n            since_send.elapsed().as_millis(),\n            stderr(&doomed_send),\n            stdout(&listed)\n        )\n    });\n    assert!(doomed_snapshot.exists(), \"its snapshot bytes are on disk before the reap\");\n\n    // NOW retire it, deliberately, instead of waiting for a clock. `scope_entry`\n    // addresses an entry by its served NAME and rewrites its ttl; expiry is\n    // `registered_at_ms + ttl <= now`, so 1 ms against a registration already in\n    // the past is expired by construction, on the reaper's next tick.\n    //\n    // `scope_entry` also sets `audience` unconditionally from its argument, so\n    // passing None CLEARS any audience. reap-me.md has none, which makes that a\n    // no-op HERE — but do not lift this mutation onto an audienced entry without\n    // passing its audience back in, or the scope goes with the ttl.\n    //\n    // This fixture is another PROCESS writing the live daemon's registry.\n    // Hold its shared guard across the fresh read and publication, not a retry\n    // after a stale write. An already-retired entry needs no further mutation.\n    // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n    {\n        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n        if registry\n            .scope_entry(\"reap-me.md\", Some(1), None, None)\n            .is_some()\n        {\n            registry.save_at(&registry_path).unwrap();\n        }\n    }\n\n    // Plant the already-expired reference under the same transaction guard.\n    // Release it before waiting for the brain to reap either entry.\n    let kept_source = work.join(\"kept-by-the-guard.md\");\n    std::fs::write(&kept_source, b\"the user's own file\").unwrap();\n    {\n        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\n        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\n        let planted = registry\n            .add_reference(&kept_source, Some(\"kept-by-the-guard.md\"), None, 1)\n            .expect(\"plant a reference entry\");\n        registry\n            .scope_entry(&planted.id, Some(1), None, None)\n            .expect(\"give it an already-expired ttl\");\n        registry.save_at(&registry_path).unwrap();\n    }\n\n    // One pulse is LIVE_RECONCILE_INTERVAL_MS (5 s); wait past two.\n    let reap_deadline = std::time::Instant::now() + Duration::from_secs(30);\n    while std::time::Instant::now() < reap_deadline {\n        if !doomed_snapshot.exists() && snapshot_of(\"reap-me.md\").is_none() {\n            break;\n        }\n        std::thread::sleep(Duration::from_millis(250));\n    }\n    assert!(\n        !doomed_snapshot.exists(),\n        \"the reaper deleted the expired attachment's SNAPSHOT BYTES: {}\",\n        doomed_snapshot.display()\n    );\n    assert!(\n        snapshot_of(\"reap-me.md\").is_none(),\n        \"and its registry ENTRY, in the same sweep\"\n    );\n    assert!(\n        kept_source.exists(),\n        \"THE GUARD: an expired REFERENCE loses its entry and keeps the user's file\"\n    );\n    assert!(\n        snapshot_of(\"kept-by-the-guard.md\").is_none(),\n        \"the reference entry itself is still retired — only its bytes are spared\"\n    );\n    let daemon_log =\n        std::fs::read_to_string(spt_daemon::stderrlog::sink_path(home)).unwrap_or_default();\n    assert!(\n        daemon_log.lines().any(|line| line.starts_with(\"SERVE_REAP: reaped \")),\n        \"the reap says how many it took, so a silent no-op is not mistaken for a sweep:\n{daemon_log}\"\n    );\n\n    // The daemon this test started is reaped through its own breadcrumb.\n    let _ = run(home, &[\"daemon\", \"stop\"], None);\n    let pid = common::reap::breadcrumb_daemon_pid(home);\n    let observed = pid.and_then(|pid| common::reap::observe(\"w2-attach\", pid));\n    common::reap::reap_breadcrumb_daemon(\"w2-attach\", home, &spt(), observed);\n}"}],"details":{"totalLines":736,"displayContent":{"text":"fn an_attachment_is_snapshot_served_fetched_back_and_named_by_its_message() {\n…\n    assert_eq!(std::fs::read(&beta_back).unwrap(), b\"beta bytes\");\r\n\r\n    // ══ ARM 12 — the TTL REAPER: bytes and entry retired TOGETHER ═══════════\r\n    // F1. ARM 10 proves an expired link stops SERVING; masking at serve time\r\n    // does that on its own. This arm is about the disk: without a reaper on the\r\n    // daemon's pulse, `$SPT_HOME/serve/snapshots/` grows forever and the entry\r\n    // never leaves the registry. The REQ promises both, deleted together, with\r\n    // the count logged.\r\n    //\r\n    // The guard is asserted in the same arm, because a reaper that deletes files\r\n    // is exactly the code that must never delete the WRONG file: a REFERENCE\r\n    // entry (`ServedKind::File`) whose ttl has expired must lose its ENTRY and\r\n    // keep its BYTES — the path there is the user's own file, not a snapshot\r\n    // this node minted.\r\n    // [int->REQ-WEB-ATTACHMENT-PULL]\r\n    let registry_path = spt_store::perch::serving_registry_file_in(home);\r\n    let snapshot_of = |name: &str| -> Option<PathBuf> {\r\n        let registry = spt_store::serving::ServingRegistry::load_at(&registry_path).ok()?;\r\n        let found = registry\r\n            .entries()\r\n            .find(|entry| entry.served_name == name)\r\n            .map(|entry| entry.path.clone());\r\n        found\r\n    };\r\n    // This arm mints its OWN short-lived attachment rather than reusing ARM\r\n    // 10's. Measured, not guessed: the rig's whole run is ~8.7s and the reaper\r\n    // ticks every 5s, so at least one sweep lands mid-test, and ARM 11 spends\r\n    // six child processes between ARM 10's send and here. Depending on ARM 10's\r\n    // entry to still be present-but-unreaped is a race the reaper WINS\r\n    // sometimes — and it would have failed on a precondition, reading as a\r\n    // product defect on a golden run rather than as the rig's own timing.\r\n    let doomed_source = work.join(\"reap-me.md\");\r\n    std::fs::write(&doomed_source, b\"bytes with a short lifetime\").unwrap();\r\n    let doomed_send = run(\r\n        home,\r\n        &[\r\n            \"send\", TARGET, \"--from\", SENDER,\r\n            \"--attachment\", doomed_source.to_str().unwrap(),\r\n            // A LIFETIME THAT CANNOT EXPIRE DURING THIS TEST. The arm used to\r\n            // mint a short-lived entry and race it: 1s, then widened to 5s with\r\n            // the note that \"five seconds cannot expire before the capture\r\n            // below\". On a loaded golden runner five seconds DID expire before\r\n            // the capture — the entry was already reaped when this arm read the\r\n            // registry, and it panicked as \"not in the registry\", which reads as\r\n            // a product defect rather than as the rig's own clock.\r\n            //\r\n            // Widening the window again would be the SAME FIX THAT ALREADY\r\n            // FAILED ONCE, just with a bigger number: any wall-clock margin is a\r\n            // bet against the slowest machine that will ever run this. So the\r\n            // lifetime is now long enough to be irrelevant, and the EXPIRY is\r\n            // driven deliberately below, through the registry writer this arm\r\n            // already uses to plant its guard entry. No wall clock decides\r\n            // anything here any more.\r\n            \"--ttl\", \"3600s\",\r\n        ],\r\n        Some(\"this one is for the reaper\"),\r\n    );\r\n    // Elapsed since the send RETURNED, so any future red carries a number instead\r\n    // of a guess. It bounds the CAPTURE only — the registration happens inside\r\n    // the daemon before the send's delivery leg finishes, so this is not the\r\n    // register-to-read gap and must not be quoted as one. Bound at the point of\r\n    // use rather than pre-declared: the instant must still be taken AFTER `run`\r\n    // returns, and a late-initialised binding buys nothing for that.\r\n    let since_send = std::time::Instant::now();\r\n    assert!(doomed_send.status.success(), \"ttl send: {}\", stderr(&doomed_send));\r\n    let doomed_snapshot = snapshot_of(\"reap-me.md\").unwrap_or_else(|| {\r\n        let listed = run(home, &[\"serve\", \"list\", \"--json\"], None);\r\n        panic!(\r\n            \"the attachment this arm just registered is not in the registry {} ms after the send RETURNED (this bounds the capture, not the register-to-read gap: the daemon's registration precedes the send's own delivery leg).\r\nsend stderr: {}\r\nregistry: {}\",\r\n            since_send.elapsed().as_millis(),\r\n            stderr(&doomed_send),\r\n            stdout(&listed)\r\n        )\r\n    });\r\n    assert!(doomed_snapshot.exists(), \"its snapshot bytes are on disk before the reap\");\r\n\r\n    // NOW retire it, deliberately, instead of waiting for a clock. `scope_entry`\r\n    // addresses an entry by its served NAME and rewrites its ttl; expiry is\r\n    // `registered_at_ms + ttl <= now`, so 1 ms against a registration already in\r\n    // the past is expired by construction, on the reaper's next tick.\r\n    //\r\n    // `scope_entry` also sets `audience` unconditionally from its argument, so\r\n    // passing None CLEARS any audience. reap-me.md has none, which makes that a\r\n    // no-op HERE — but do not lift this mutation onto an audienced entry without\r\n    // passing its audience back in, or the scope goes with the ttl.\r\n    //\r\n    // This fixture is another PROCESS writing the live daemon's registry.\r\n    // Hold its shared guard across the fresh read and publication, not a retry\r\n    // after a stale write. An already-retired entry needs no further mutation.\r\n    // [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\r\n    {\r\n        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\r\n        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\r\n        if registry\r\n            .scope_entry(\"reap-me.md\", Some(1), None, None)\r\n            .is_some()\r\n        {\r\n            registry.save_at(&registry_path).unwrap();\r\n        }\r\n    }\r\n\r\n    // Plant the already-expired reference under the same transaction guard.\r\n    // Release it before waiting for the brain to reap either entry.\r\n    let kept_source = work.join(\"kept-by-the-guard.md\");\r\n    std::fs::write(&kept_source, b\"the user's own file\").unwrap();\r\n    {\r\n        let _writer = spt_store::serving::lock_registry_at(&registry_path).unwrap();\r\n        let mut registry = spt_store::serving::ServingRegistry::load_at(&registry_path).unwrap();\r\n        let planted = registry\r\n            .add_reference(&kept_source, Some(\"kept-by-the-guard.md\"), None, 1)\r\n            .expect(\"plant a reference entry\");\r\n        registry\r\n            .scope_entry(&planted.id, Some(1), None, None)\r\n            .expect(\"give it an already-expired ttl\");\r\n        registry.save_at(&registry_path).unwrap();\r\n    }\r\n\r\n    // One pulse is LIVE_RECONCILE_INTERVAL_MS (5 s); wait past two.\r\n    let reap_deadline = std::time::Instant::now() + Duration::from_secs(30);\r\n    while std::time::Instant::now() < reap_deadline {\r\n        if !doomed_snapshot.exists() && snapshot_of(\"reap-me.md\").is_none() {\r\n            break;\r\n        }\r\n        std::thread::sleep(Duration::from_millis(250));\r\n    }\r\n    assert!(\r\n        !doomed_snapshot.exists(),\r\n        \"the reaper deleted the expired attachment's SNAPSHOT BYTES: {}\",\r\n        doomed_snapshot.display()\r\n    );\r\n    assert!(\r\n        snapshot_of(\"reap-me.md\").is_none(),\r\n        \"and its registry ENTRY, in the same sweep\"\r\n    );\r\n    assert!(\r\n        kept_source.exists(),\r\n        \"THE GUARD: an expired REFERENCE loses its entry and keeps the user's file\"\r\n    );\r\n    assert!(\r\n        snapshot_of(\"kept-by-the-guard.md\").is_none(),\r\n        \"the reference entry itself is still retired — only its bytes are spared\"\r\n    );\r\n    let daemon_log =\r\n        std::fs::read_to_string(spt_daemon::stderrlog::sink_path(home)).unwrap_or_default();\r\n    assert!(\r\n        daemon_log.lines().any(|line| line.starts_with(\"SERVE_REAP: reaped \")),\r\n        \"the reap says how many it took, so a silent no-op is not mistaken for a sweep:\r\n{daemon_log}\"\r\n    );\r\n\r\n    // The daemon this test started is reaped through its own breadcrumb.\r\n    let _ = run(home, &[\"daemon\", \"stop\"], None);\r\n    let pid = common::reap::breadcrumb_daemon_pid(home);\r\n    let observed = pid.and_then(|pid| common::reap::observe(\"w2-attach\", pid));\r\n    common::reap::reap_breadcrumb_daemon(\"w2-attach\", home, &spt(), observed);\r\n}\r","startLine":272,"lineNumbers":[272,null,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736]},"fileSize":38380,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt\\tests\\webserve_attachment_e2e.rs"}}},"isError":false,"timestamp":1789522929265}}
{"type":"message","id":"db28df5a","parentId":"6e2ee04c","timestamp":"2026-09-16T01:42:09.271Z","message":{"role":"toolResult","toolCallId":"call_WEO2rRRpMoFwNA5aqMg5rvY3|fc_0cdf94810c888815016aa9f3f058fc87d1840b524603074676","toolName":"read","content":[{"type":"text","text":"use std::fs::File;\nuse std::io::{Error, Result};\nuse std::mem;\nuse std::os::windows::ffi::OsStrExt;\nuse std::os::windows::io::{AsRawHandle, FromRawHandle};\nuse std::path::Path;\nuse std::ptr;\n\nuse winapi::shared::minwindef::{BOOL, DWORD};\nuse winapi::shared::winerror::ERROR_LOCK_VIOLATION;\nuse winapi::um::fileapi::{FILE_ALLOCATION_INFO, FILE_STANDARD_INFO, GetDiskFreeSpaceW};\nuse winapi::um::fileapi::{GetVolumePathNameW, LockFileEx, UnlockFile, SetFileInformationByHandle};\nuse winapi::um::handleapi::DuplicateHandle;\nuse winapi::um::minwinbase::{FileAllocationInfo, FileStandardInfo};\nuse winapi::um::minwinbase::{LOCKFILE_FAIL_IMMEDIATELY, LOCKFILE_EXCLUSIVE_LOCK};\nuse winapi::um::processthreadsapi::GetCurrentProcess;\nuse winapi::um::winbase::GetFileInformationByHandleEx;\nuse winapi::um::winnt::DUPLICATE_SAME_ACCESS;\n\nuse FsStats;\n\npub fn duplicate(file: &File) -> Result<File> {\n    unsafe {\n        let mut handle = ptr::null_mut();\n        let current_process = GetCurrentProcess();\n        let ret = DuplicateHandle(current_process,\n                                  file.as_raw_handle(),\n                                  current_process,\n                                  &mut handle,\n                                  0,\n                                  true as BOOL,\n                                  DUPLICATE_SAME_ACCESS);\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            Ok(File::from_raw_handle(handle))\n        }\n    }\n}\n\npub fn allocated_size(file: &File) -> Result<u64> {\n    unsafe {\n        let mut info: FILE_STANDARD_INFO = mem::zeroed();\n\n        let ret = GetFileInformationByHandleEx(\n            file.as_raw_handle(),\n            FileStandardInfo,\n            &mut info as *mut _ as *mut _,\n            mem::size_of::<FILE_STANDARD_INFO>() as DWORD);\n\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            Ok(*info.AllocationSize.QuadPart() as u64)\n        }\n    }\n}\n\npub fn allocate(file: &File, len: u64) -> Result<()> {\n    if try!(allocated_size(file)) < len {\n        unsafe {\n            let mut info: FILE_ALLOCATION_INFO = mem::zeroed();\n            *info.AllocationSize.QuadPart_mut() = len as i64;\n            let ret = SetFileInformationByHandle(\n                file.as_raw_handle(),\n                FileAllocationInfo,\n                &mut info as *mut _ as *mut _,\n                mem::size_of::<FILE_ALLOCATION_INFO>() as DWORD);\n            if ret == 0 {\n                return Err(Error::last_os_error());\n            }\n        }\n    }\n    if try!(file.metadata()).len() < len {\n        file.set_len(len)\n    } else {\n        Ok(())\n    }\n}\n\npub fn lock_shared(file: &File) -> Result<()> {\n    lock_file(file, 0)\n}\n\npub fn lock_exclusive(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)\n}\n\npub fn try_lock_shared(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)\n}\n\npub fn try_lock_exclusive(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)\n}\n\npub fn unlock(file: &File) -> Result<()> {\n    unsafe {\n        let ret = UnlockFile(file.as_raw_handle(), 0, 0, !0, !0);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(()) }\n    }\n}\n\npub fn lock_error() -> Error {\n    Error::from_raw_os_error(ERROR_LOCK_VIOLATION as i32)\n}\n\nfn lock_file(file: &File, flags: DWORD) -> Result<()> {\n    unsafe {\n        let mut overlapped = mem::zeroed();\n        let ret = LockFileEx(file.as_raw_handle(), flags, 0, !0, !0, &mut overlapped);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(()) }\n    }\n}\n\nfn volume_path(path: &Path, volume_path: &mut [u16]) -> Result<()> {\n    let path_utf8: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();\n    unsafe {\n        let ret = GetVolumePathNameW(path_utf8.as_ptr(),\n                                     volume_path.as_mut_ptr(),\n                                     volume_path.len() as DWORD);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(())\n        }\n    }\n}\n\npub fn statvfs(path: &Path) -> Result<FsStats> {\n    let root_path: &mut [u16] = &mut [0; 261];\n    try!(volume_path(path, root_path));\n    unsafe {\n\n        let mut sectors_per_cluster = 0;\n        let mut bytes_per_sector = 0;\n        let mut number_of_free_clusters = 0;\n        let mut total_number_of_clusters = 0;\n        let ret = GetDiskFreeSpaceW(root_path.as_ptr(),\n                                    &mut sectors_per_cluster,\n                                    &mut bytes_per_sector,\n                                    &mut number_of_free_clusters,\n                                    &mut total_number_of_clusters);\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64;\n            let free_space = bytes_per_cluster * number_of_free_clusters as u64;\n            let total_space = bytes_per_cluster * total_number_of_clusters as u64;\n            Ok(FsStats {\n                free_space: free_space,\n                available_space: free_space,\n                total_space: total_space,\n                allocation_granularity: bytes_per_cluster,\n            })\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n\n    extern crate tempdir;\n\n    use std::fs;\n    use std::os::windows::io::AsRawHandle;\n\n    use {FileExt, lock_contended_error};\n\n    /// The duplicate method returns a file with a new file handle.\n    #[test]\n    fn duplicate_new_handle() {\n        let tempdir = tempdir::TempDir::new(\"fs2\").unwrap();\n        let path = tempdir.path().join(\"fs2\");\n        let file1 = fs::OpenOptions::new().write(true).create(true).open(&path).unwrap();\n        let file2 = file1.duplicate().unwrap();\n        assert!(file1.as_raw_handle() != file2.as_raw_handle());\n    }\n\n    /// A duplicated file handle does not have access to the original handle's locks.\n    #[test]\n    fn lock_duplicate_handle_independence() {\n        let tempdir = tempdir::TempDir::new(\"fs2\").unwrap();\n        let path = tempdir.path().join(\"fs2\");\n        let file1 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();\n        let file2 = file1.duplicate().unwrap();\n…\n    }\n…\n}\n\n[Showing lines 1-183 of 279. Use :184 to continue]"}],"details":{"truncation":{"content":"use std::fs::File;\nuse std::io::{Error, Result};\nuse std::mem;\nuse std::os::windows::ffi::OsStrExt;\nuse std::os::windows::io::{AsRawHandle, FromRawHandle};\nuse std::path::Path;\nuse std::ptr;\n\nuse winapi::shared::minwindef::{BOOL, DWORD};\nuse winapi::shared::winerror::ERROR_LOCK_VIOLATION;\nuse winapi::um::fileapi::{FILE_ALLOCATION_INFO, FILE_STANDARD_INFO, GetDiskFreeSpaceW};\nuse winapi::um::fileapi::{GetVolumePathNameW, LockFileEx, UnlockFile, SetFileInformationByHandle};\nuse winapi::um::handleapi::DuplicateHandle;\nuse winapi::um::minwinbase::{FileAllocationInfo, FileStandardInfo};\nuse winapi::um::minwinbase::{LOCKFILE_FAIL_IMMEDIATELY, LOCKFILE_EXCLUSIVE_LOCK};\nuse winapi::um::processthreadsapi::GetCurrentProcess;\nuse winapi::um::winbase::GetFileInformationByHandleEx;\nuse winapi::um::winnt::DUPLICATE_SAME_ACCESS;\n\nuse FsStats;\n\npub fn duplicate(file: &File) -> Result<File> {\n    unsafe {\n        let mut handle = ptr::null_mut();\n        let current_process = GetCurrentProcess();\n        let ret = DuplicateHandle(current_process,\n                                  file.as_raw_handle(),\n                                  current_process,\n                                  &mut handle,\n                                  0,\n                                  true as BOOL,\n                                  DUPLICATE_SAME_ACCESS);\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            Ok(File::from_raw_handle(handle))\n        }\n    }\n}\n\npub fn allocated_size(file: &File) -> Result<u64> {\n    unsafe {\n        let mut info: FILE_STANDARD_INFO = mem::zeroed();\n\n        let ret = GetFileInformationByHandleEx(\n            file.as_raw_handle(),\n            FileStandardInfo,\n            &mut info as *mut _ as *mut _,\n            mem::size_of::<FILE_STANDARD_INFO>() as DWORD);\n\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            Ok(*info.AllocationSize.QuadPart() as u64)\n        }\n    }\n}\n\npub fn allocate(file: &File, len: u64) -> Result<()> {\n    if try!(allocated_size(file)) < len {\n        unsafe {\n            let mut info: FILE_ALLOCATION_INFO = mem::zeroed();\n            *info.AllocationSize.QuadPart_mut() = len as i64;\n            let ret = SetFileInformationByHandle(\n                file.as_raw_handle(),\n                FileAllocationInfo,\n                &mut info as *mut _ as *mut _,\n                mem::size_of::<FILE_ALLOCATION_INFO>() as DWORD);\n            if ret == 0 {\n                return Err(Error::last_os_error());\n            }\n        }\n    }\n    if try!(file.metadata()).len() < len {\n        file.set_len(len)\n    } else {\n        Ok(())\n    }\n}\n\npub fn lock_shared(file: &File) -> Result<()> {\n    lock_file(file, 0)\n}\n\npub fn lock_exclusive(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)\n}\n\npub fn try_lock_shared(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)\n}\n\npub fn try_lock_exclusive(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)\n}\n\npub fn unlock(file: &File) -> Result<()> {\n    unsafe {\n        let ret = UnlockFile(file.as_raw_handle(), 0, 0, !0, !0);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(()) }\n    }\n}\n\npub fn lock_error() -> Error {\n    Error::from_raw_os_error(ERROR_LOCK_VIOLATION as i32)\n}\n\nfn lock_file(file: &File, flags: DWORD) -> Result<()> {\n    unsafe {\n        let mut overlapped = mem::zeroed();\n        let ret = LockFileEx(file.as_raw_handle(), flags, 0, !0, !0, &mut overlapped);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(()) }\n    }\n}\n\nfn volume_path(path: &Path, volume_path: &mut [u16]) -> Result<()> {\n    let path_utf8: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();\n    unsafe {\n        let ret = GetVolumePathNameW(path_utf8.as_ptr(),\n                                     volume_path.as_mut_ptr(),\n                                     volume_path.len() as DWORD);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(())\n        }\n    }\n}\n\npub fn statvfs(path: &Path) -> Result<FsStats> {\n    let root_path: &mut [u16] = &mut [0; 261];\n    try!(volume_path(path, root_path));\n    unsafe {\n\n        let mut sectors_per_cluster = 0;\n        let mut bytes_per_sector = 0;\n        let mut number_of_free_clusters = 0;\n        let mut total_number_of_clusters = 0;\n        let ret = GetDiskFreeSpaceW(root_path.as_ptr(),\n                                    &mut sectors_per_cluster,\n                                    &mut bytes_per_sector,\n                                    &mut number_of_free_clusters,\n                                    &mut total_number_of_clusters);\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64;\n            let free_space = bytes_per_cluster * number_of_free_clusters as u64;\n            let total_space = bytes_per_cluster * total_number_of_clusters as u64;\n            Ok(FsStats {\n                free_space: free_space,\n                available_space: free_space,\n                total_space: total_space,\n                allocation_granularity: bytes_per_cluster,\n            })\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n\n    extern crate tempdir;\n\n    use std::fs;\n    use std::os::windows::io::AsRawHandle;\n\n    use {FileExt, lock_contended_error};\n\n    /// The duplicate method returns a file with a new file handle.\n    #[test]\n    fn duplicate_new_handle() {\n        let tempdir = tempdir::TempDir::new(\"fs2\").unwrap();\n        let path = tempdir.path().join(\"fs2\");\n        let file1 = fs::OpenOptions::new().write(true).create(true).open(&path).unwrap();\n        let file2 = file1.duplicate().unwrap();\n        assert!(file1.as_raw_handle() != file2.as_raw_handle());\n    }\n\n    /// A duplicated file handle does not have access to the original handle's locks.\n    #[test]\n    fn lock_duplicate_handle_independence() {\n        let tempdir = tempdir::TempDir::new(\"fs2\").unwrap();\n        let path = tempdir.path().join(\"fs2\");\n        let file1 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();\n        let file2 = file1.duplicate().unwrap();","truncated":true,"truncatedBy":"lines","totalLines":279,"totalBytes":6279,"outputLines":183,"outputBytes":6279,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":279,"displayContent":{"text":"use std::fs::File;\nuse std::io::{Error, Result};\nuse std::mem;\nuse std::os::windows::ffi::OsStrExt;\nuse std::os::windows::io::{AsRawHandle, FromRawHandle};\nuse std::path::Path;\nuse std::ptr;\n\nuse winapi::shared::minwindef::{BOOL, DWORD};\nuse winapi::shared::winerror::ERROR_LOCK_VIOLATION;\nuse winapi::um::fileapi::{FILE_ALLOCATION_INFO, FILE_STANDARD_INFO, GetDiskFreeSpaceW};\nuse winapi::um::fileapi::{GetVolumePathNameW, LockFileEx, UnlockFile, SetFileInformationByHandle};\nuse winapi::um::handleapi::DuplicateHandle;\nuse winapi::um::minwinbase::{FileAllocationInfo, FileStandardInfo};\nuse winapi::um::minwinbase::{LOCKFILE_FAIL_IMMEDIATELY, LOCKFILE_EXCLUSIVE_LOCK};\nuse winapi::um::processthreadsapi::GetCurrentProcess;\nuse winapi::um::winbase::GetFileInformationByHandleEx;\nuse winapi::um::winnt::DUPLICATE_SAME_ACCESS;\n\nuse FsStats;\n\npub fn duplicate(file: &File) -> Result<File> {\n    unsafe {\n        let mut handle = ptr::null_mut();\n        let current_process = GetCurrentProcess();\n        let ret = DuplicateHandle(current_process,\n                                  file.as_raw_handle(),\n                                  current_process,\n                                  &mut handle,\n                                  0,\n                                  true as BOOL,\n                                  DUPLICATE_SAME_ACCESS);\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            Ok(File::from_raw_handle(handle))\n        }\n    }\n}\n\npub fn allocated_size(file: &File) -> Result<u64> {\n    unsafe {\n        let mut info: FILE_STANDARD_INFO = mem::zeroed();\n\n        let ret = GetFileInformationByHandleEx(\n            file.as_raw_handle(),\n            FileStandardInfo,\n            &mut info as *mut _ as *mut _,\n            mem::size_of::<FILE_STANDARD_INFO>() as DWORD);\n\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            Ok(*info.AllocationSize.QuadPart() as u64)\n        }\n    }\n}\n\npub fn allocate(file: &File, len: u64) -> Result<()> {\n    if try!(allocated_size(file)) < len {\n        unsafe {\n            let mut info: FILE_ALLOCATION_INFO = mem::zeroed();\n            *info.AllocationSize.QuadPart_mut() = len as i64;\n            let ret = SetFileInformationByHandle(\n                file.as_raw_handle(),\n                FileAllocationInfo,\n                &mut info as *mut _ as *mut _,\n                mem::size_of::<FILE_ALLOCATION_INFO>() as DWORD);\n            if ret == 0 {\n                return Err(Error::last_os_error());\n            }\n        }\n    }\n    if try!(file.metadata()).len() < len {\n        file.set_len(len)\n    } else {\n        Ok(())\n    }\n}\n\npub fn lock_shared(file: &File) -> Result<()> {\n    lock_file(file, 0)\n}\n\npub fn lock_exclusive(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)\n}\n\npub fn try_lock_shared(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)\n}\n\npub fn try_lock_exclusive(file: &File) -> Result<()> {\n    lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)\n}\n\npub fn unlock(file: &File) -> Result<()> {\n    unsafe {\n        let ret = UnlockFile(file.as_raw_handle(), 0, 0, !0, !0);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(()) }\n    }\n}\n\npub fn lock_error() -> Error {\n    Error::from_raw_os_error(ERROR_LOCK_VIOLATION as i32)\n}\n\nfn lock_file(file: &File, flags: DWORD) -> Result<()> {\n    unsafe {\n        let mut overlapped = mem::zeroed();\n        let ret = LockFileEx(file.as_raw_handle(), flags, 0, !0, !0, &mut overlapped);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(()) }\n    }\n}\n\nfn volume_path(path: &Path, volume_path: &mut [u16]) -> Result<()> {\n    let path_utf8: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();\n    unsafe {\n        let ret = GetVolumePathNameW(path_utf8.as_ptr(),\n                                     volume_path.as_mut_ptr(),\n                                     volume_path.len() as DWORD);\n        if ret == 0 { Err(Error::last_os_error()) } else { Ok(())\n        }\n    }\n}\n\npub fn statvfs(path: &Path) -> Result<FsStats> {\n    let root_path: &mut [u16] = &mut [0; 261];\n    try!(volume_path(path, root_path));\n    unsafe {\n\n        let mut sectors_per_cluster = 0;\n        let mut bytes_per_sector = 0;\n        let mut number_of_free_clusters = 0;\n        let mut total_number_of_clusters = 0;\n        let ret = GetDiskFreeSpaceW(root_path.as_ptr(),\n                                    &mut sectors_per_cluster,\n                                    &mut bytes_per_sector,\n                                    &mut number_of_free_clusters,\n                                    &mut total_number_of_clusters);\n        if ret == 0 {\n            Err(Error::last_os_error())\n        } else {\n            let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64;\n            let free_space = bytes_per_cluster * number_of_free_clusters as u64;\n            let total_space = bytes_per_cluster * total_number_of_clusters as u64;\n            Ok(FsStats {\n                free_space: free_space,\n                available_space: free_space,\n                total_space: total_space,\n                allocation_granularity: bytes_per_cluster,\n            })\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n\n    extern crate tempdir;\n\n    use std::fs;\n    use std::os::windows::io::AsRawHandle;\n\n    use {FileExt, lock_contended_error};\n\n    /// The duplicate method returns a file with a new file handle.\n    #[test]\n    fn duplicate_new_handle() {\n        let tempdir = tempdir::TempDir::new(\"fs2\").unwrap();\n        let path = tempdir.path().join(\"fs2\");\n        let file1 = fs::OpenOptions::new().write(true).create(true).open(&path).unwrap();\n        let file2 = file1.duplicate().unwrap();\n        assert!(file1.as_raw_handle() != file2.as_raw_handle());\n    }\n\n    /// A duplicated file handle does not have access to the original handle's locks.\n    #[test]\n    fn lock_duplicate_handle_independence() {\n        let tempdir = tempdir::TempDir::new(\"fs2\").unwrap();\n        let path = tempdir.path().join(\"fs2\");\n        let file1 = fs::OpenOptions::new().read(true).write(true).create(true).open(&path).unwrap();\n        let file2 = file1.duplicate().unwrap();\n…\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,null,193,null,279]},"fileSize":10484,"meta":{"source":{"type":"path","value":"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/windows.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":279,"totalBytes":6279,"outputLines":183,"outputBytes":6279,"shownRange":{"start":1,"end":183},"nextOffset":184}}},"isError":false,"timestamp":1789522929271}}
{"type":"message","id":"a5fea163","parentId":"db28df5a","timestamp":"2026-09-16T01:42:09.274Z","message":{"role":"toolResult","toolCallId":"call_vb9ucnRKRxC6owkyf3s4XBzg|fc_0cdf94810c888815016aa9f3f0590c87d1ba0603099280a66f","toolName":"read","content":[{"type":"text","text":"//! Extended utilities for working with files and filesystems in Rust.\n\n#![doc(html_root_url = \"https://docs.rs/fs2/0.4.3\")]\n\n#![cfg_attr(test, feature(test))]\n\n#[cfg(windows)]\nextern crate winapi;\n\n#[cfg(unix)]\nmod unix;\n#[cfg(unix)]\nuse unix as sys;\n\n#[cfg(windows)]\nmod windows;\n#[cfg(windows)]\nuse windows as sys;\n\nuse std::fs::File;\nuse std::io::{Error, Result};\nuse std::path::Path;\n\n/// Extension trait for `std::fs::File` which provides allocation, duplication and locking methods.\n///\n/// ## Notes on File Locks\n///\n/// This library provides whole-file locks in both shared (read) and exclusive\n/// (read-write) varieties.\n///\n/// File locks are a cross-platform hazard since the file lock APIs exposed by\n/// operating system kernels vary in subtle and not-so-subtle ways.\n///\n/// The API exposed by this library can be safely used across platforms as long\n/// as the following rules are followed:\n///\n///   * Multiple locks should not be created on an individual `File` instance\n///     concurrently.\n///   * Duplicated files should not be locked without great care.\n///   * Files to be locked should be opened with at least read or write\n///     permissions.\n///   * File locks may only be relied upon to be advisory.\n///\n/// See the tests in `lib.rs` for cross-platform lock behavior that may be\n/// relied upon; see the tests in `unix.rs` and `windows.rs` for examples of\n/// platform-specific behavior. File locks are implemented with\n/// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and\n/// [`LockFile`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365202(v=vs.85).aspx)\n/// on Windows.\npub trait FileExt {\n\n    /// Returns a duplicate instance of the file.\n    ///\n    /// The returned file will share the same file position as the original\n    /// file.\n    ///\n    /// If using rustc version 1.9 or later, prefer using `File::try_clone` to this.\n    ///\n    /// # Notes\n    ///\n    /// This is implemented with\n    /// [`dup(2)`](http://man7.org/linux/man-pages/man2/dup.2.html) on Unix and\n    /// [`DuplicateHandle`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx)\n    /// on Windows.\n    fn duplicate(&self) -> Result<File>;\n\n    /// Returns the amount of physical space allocated for a file.\n    fn allocated_size(&self) -> Result<u64>;\n\n    /// Ensures that at least `len` bytes of disk space are allocated for the\n    /// file, and the file size is at least `len` bytes. After a successful call\n    /// to `allocate`, subsequent writes to the file within the specified length\n    /// are guaranteed not to fail because of lack of disk space.\n    fn allocate(&self, len: u64) -> Result<()>;\n\n    /// Locks the file for shared usage, blocking if the file is currently\n    /// locked exclusively.\n    fn lock_shared(&self) -> Result<()>;\n\n    /// Locks the file for exclusive usage, blocking if the file is currently\n    /// locked.\n    fn lock_exclusive(&self) -> Result<()>;\n\n    /// Locks the file for shared usage, or returns a an error if the file is\n    /// currently locked (see `lock_contended_error`).\n    fn try_lock_shared(&self) -> Result<()>;\n\n    /// Locks the file for shared usage, or returns a an error if the file is\n    /// currently locked (see `lock_contended_error`).\n    fn try_lock_exclusive(&self) -> Result<()>;\n\n    /// Unlocks the file.\n    fn unlock(&self) -> Result<()>;\n}\n\nimpl FileExt for File {\n    fn duplicate(&self) -> Result<File> {\n        sys::duplicate(self)\n    }\n    fn allocated_size(&self) -> Result<u64> {\n        sys::allocated_size(self)\n    }\n    fn allocate(&self, len: u64) -> Result<()> {\n        sys::allocate(self, len)\n    }\n    fn lock_shared(&self) -> Result<()> {\n        sys::lock_shared(self)\n    }\n    fn lock_exclusive(&self) -> Result<()> {\n        sys::lock_exclusive(self)\n    }\n    fn try_lock_shared(&self) -> Result<()> {\n        sys::try_lock_shared(self)\n    }\n    fn try_lock_exclusive(&self) -> Result<()> {\n        sys::try_lock_exclusive(self)\n    }\n    fn unlock(&self) -> Result<()> {\n        sys::unlock(self)\n    }\n}\n\n/// Returns the error that a call to a try lock method on a contended file will\n/// return.\npub fn lock_contended_error() -> Error {\n    sys::lock_error()\n}\n\n/// `FsStats` contains some common stats about a file system.\n#[derive(Clone, Debug, PartialEq, Eq, Hash)]\npub struct FsStats {\n    free_space: u64,\n    available_space: u64,\n    total_space: u64,\n    allocation_granularity: u64,\n}\n\nimpl FsStats {\n    /// Returns the number of free bytes in the file system containing the provided\n    /// path.\n    pub fn free_space(&self) -> u64 {\n        self.free_space\n    }\n\n    /// Returns the available space in bytes to non-priveleged users in the file\n    /// system containing the provided path.\n    pub fn available_space(&self) -> u64 {\n        self.available_space\n    }\n\n    /// Returns the total space in bytes in the file system containing the provided\n    /// path.\n    pub fn total_space(&self) -> u64 {\n        self.total_space\n    }\n\n    /// Returns the filesystem's disk space allocation granularity in bytes.\n    /// The provided path may be for any file in the filesystem.\n    ///\n    /// On Posix, this is equivalent to the filesystem's block size.\n    /// On Windows, this is equivalent to the filesystem's cluster size.\n    pub fn allocation_granularity(&self) -> u64 {\n        self.allocation_granularity\n    }\n}\n\n/// Get the stats of the file system containing the provided path.\npub fn statvfs<P>(path: P) -> Result<FsStats> where P: AsRef<Path> {\n    sys::statvfs(path.as_ref())\n}\n\n/// Returns the number of free bytes in the file system containing the provided\n/// path.\npub fn free_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.free_space)\n}\n\n/// Returns the available space in bytes to non-priveleged users in the file\n/// system containing the provided path.\npub fn available_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.available_space)\n}\n\n/// Returns the total space in bytes in the file system containing the provided\n/// path.\npub fn total_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.total_space)\n}\n\n[Showing lines 1-188 of 458. Use :189 to continue]"}],"details":{"truncation":{"content":"//! Extended utilities for working with files and filesystems in Rust.\n\n#![doc(html_root_url = \"https://docs.rs/fs2/0.4.3\")]\n\n#![cfg_attr(test, feature(test))]\n\n#[cfg(windows)]\nextern crate winapi;\n\n#[cfg(unix)]\nmod unix;\n#[cfg(unix)]\nuse unix as sys;\n\n#[cfg(windows)]\nmod windows;\n#[cfg(windows)]\nuse windows as sys;\n\nuse std::fs::File;\nuse std::io::{Error, Result};\nuse std::path::Path;\n\n/// Extension trait for `std::fs::File` which provides allocation, duplication and locking methods.\n///\n/// ## Notes on File Locks\n///\n/// This library provides whole-file locks in both shared (read) and exclusive\n/// (read-write) varieties.\n///\n/// File locks are a cross-platform hazard since the file lock APIs exposed by\n/// operating system kernels vary in subtle and not-so-subtle ways.\n///\n/// The API exposed by this library can be safely used across platforms as long\n/// as the following rules are followed:\n///\n///   * Multiple locks should not be created on an individual `File` instance\n///     concurrently.\n///   * Duplicated files should not be locked without great care.\n///   * Files to be locked should be opened with at least read or write\n///     permissions.\n///   * File locks may only be relied upon to be advisory.\n///\n/// See the tests in `lib.rs` for cross-platform lock behavior that may be\n/// relied upon; see the tests in `unix.rs` and `windows.rs` for examples of\n/// platform-specific behavior. File locks are implemented with\n/// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and\n/// [`LockFile`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365202(v=vs.85).aspx)\n/// on Windows.\npub trait FileExt {\n\n    /// Returns a duplicate instance of the file.\n    ///\n    /// The returned file will share the same file position as the original\n    /// file.\n    ///\n    /// If using rustc version 1.9 or later, prefer using `File::try_clone` to this.\n    ///\n    /// # Notes\n    ///\n    /// This is implemented with\n    /// [`dup(2)`](http://man7.org/linux/man-pages/man2/dup.2.html) on Unix and\n    /// [`DuplicateHandle`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx)\n    /// on Windows.\n    fn duplicate(&self) -> Result<File>;\n\n    /// Returns the amount of physical space allocated for a file.\n    fn allocated_size(&self) -> Result<u64>;\n\n    /// Ensures that at least `len` bytes of disk space are allocated for the\n    /// file, and the file size is at least `len` bytes. After a successful call\n    /// to `allocate`, subsequent writes to the file within the specified length\n    /// are guaranteed not to fail because of lack of disk space.\n    fn allocate(&self, len: u64) -> Result<()>;\n\n    /// Locks the file for shared usage, blocking if the file is currently\n    /// locked exclusively.\n    fn lock_shared(&self) -> Result<()>;\n\n    /// Locks the file for exclusive usage, blocking if the file is currently\n    /// locked.\n    fn lock_exclusive(&self) -> Result<()>;\n\n    /// Locks the file for shared usage, or returns a an error if the file is\n    /// currently locked (see `lock_contended_error`).\n    fn try_lock_shared(&self) -> Result<()>;\n\n    /// Locks the file for shared usage, or returns a an error if the file is\n    /// currently locked (see `lock_contended_error`).\n    fn try_lock_exclusive(&self) -> Result<()>;\n\n    /// Unlocks the file.\n    fn unlock(&self) -> Result<()>;\n}\n\nimpl FileExt for File {\n    fn duplicate(&self) -> Result<File> {\n        sys::duplicate(self)\n    }\n    fn allocated_size(&self) -> Result<u64> {\n        sys::allocated_size(self)\n    }\n    fn allocate(&self, len: u64) -> Result<()> {\n        sys::allocate(self, len)\n    }\n    fn lock_shared(&self) -> Result<()> {\n        sys::lock_shared(self)\n    }\n    fn lock_exclusive(&self) -> Result<()> {\n        sys::lock_exclusive(self)\n    }\n    fn try_lock_shared(&self) -> Result<()> {\n        sys::try_lock_shared(self)\n    }\n    fn try_lock_exclusive(&self) -> Result<()> {\n        sys::try_lock_exclusive(self)\n    }\n    fn unlock(&self) -> Result<()> {\n        sys::unlock(self)\n    }\n}\n\n/// Returns the error that a call to a try lock method on a contended file will\n/// return.\npub fn lock_contended_error() -> Error {\n    sys::lock_error()\n}\n\n/// `FsStats` contains some common stats about a file system.\n#[derive(Clone, Debug, PartialEq, Eq, Hash)]\npub struct FsStats {\n    free_space: u64,\n    available_space: u64,\n    total_space: u64,\n    allocation_granularity: u64,\n}\n\nimpl FsStats {\n    /// Returns the number of free bytes in the file system containing the provided\n    /// path.\n    pub fn free_space(&self) -> u64 {\n        self.free_space\n    }\n\n    /// Returns the available space in bytes to non-priveleged users in the file\n    /// system containing the provided path.\n    pub fn available_space(&self) -> u64 {\n        self.available_space\n    }\n\n    /// Returns the total space in bytes in the file system containing the provided\n    /// path.\n    pub fn total_space(&self) -> u64 {\n        self.total_space\n    }\n\n    /// Returns the filesystem's disk space allocation granularity in bytes.\n    /// The provided path may be for any file in the filesystem.\n    ///\n    /// On Posix, this is equivalent to the filesystem's block size.\n    /// On Windows, this is equivalent to the filesystem's cluster size.\n    pub fn allocation_granularity(&self) -> u64 {\n        self.allocation_granularity\n    }\n}\n\n/// Get the stats of the file system containing the provided path.\npub fn statvfs<P>(path: P) -> Result<FsStats> where P: AsRef<Path> {\n    sys::statvfs(path.as_ref())\n}\n\n/// Returns the number of free bytes in the file system containing the provided\n/// path.\npub fn free_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.free_space)\n}\n\n/// Returns the available space in bytes to non-priveleged users in the file\n/// system containing the provided path.\npub fn available_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.available_space)\n}\n\n/// Returns the total space in bytes in the file system containing the provided\n/// path.\npub fn total_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.total_space)\n}","truncated":true,"truncatedBy":"lines","totalLines":458,"totalBytes":6255,"outputLines":188,"outputBytes":6255,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":458,"displayContent":{"text":"//! Extended utilities for working with files and filesystems in Rust.\n\n#![doc(html_root_url = \"https://docs.rs/fs2/0.4.3\")]\n\n#![cfg_attr(test, feature(test))]\n\n#[cfg(windows)]\nextern crate winapi;\n\n#[cfg(unix)]\nmod unix;\n#[cfg(unix)]\nuse unix as sys;\n\n#[cfg(windows)]\nmod windows;\n#[cfg(windows)]\nuse windows as sys;\n\nuse std::fs::File;\nuse std::io::{Error, Result};\nuse std::path::Path;\n\n/// Extension trait for `std::fs::File` which provides allocation, duplication and locking methods.\n///\n/// ## Notes on File Locks\n///\n/// This library provides whole-file locks in both shared (read) and exclusive\n/// (read-write) varieties.\n///\n/// File locks are a cross-platform hazard since the file lock APIs exposed by\n/// operating system kernels vary in subtle and not-so-subtle ways.\n///\n/// The API exposed by this library can be safely used across platforms as long\n/// as the following rules are followed:\n///\n///   * Multiple locks should not be created on an individual `File` instance\n///     concurrently.\n///   * Duplicated files should not be locked without great care.\n///   * Files to be locked should be opened with at least read or write\n///     permissions.\n///   * File locks may only be relied upon to be advisory.\n///\n/// See the tests in `lib.rs` for cross-platform lock behavior that may be\n/// relied upon; see the tests in `unix.rs` and `windows.rs` for examples of\n/// platform-specific behavior. File locks are implemented with\n/// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and\n/// [`LockFile`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365202(v=vs.85).aspx)\n/// on Windows.\npub trait FileExt {\n\n    /// Returns a duplicate instance of the file.\n    ///\n    /// The returned file will share the same file position as the original\n    /// file.\n    ///\n    /// If using rustc version 1.9 or later, prefer using `File::try_clone` to this.\n    ///\n    /// # Notes\n    ///\n    /// This is implemented with\n    /// [`dup(2)`](http://man7.org/linux/man-pages/man2/dup.2.html) on Unix and\n    /// [`DuplicateHandle`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx)\n    /// on Windows.\n    fn duplicate(&self) -> Result<File>;\n\n    /// Returns the amount of physical space allocated for a file.\n    fn allocated_size(&self) -> Result<u64>;\n\n    /// Ensures that at least `len` bytes of disk space are allocated for the\n    /// file, and the file size is at least `len` bytes. After a successful call\n    /// to `allocate`, subsequent writes to the file within the specified length\n    /// are guaranteed not to fail because of lack of disk space.\n    fn allocate(&self, len: u64) -> Result<()>;\n\n    /// Locks the file for shared usage, blocking if the file is currently\n    /// locked exclusively.\n    fn lock_shared(&self) -> Result<()>;\n\n    /// Locks the file for exclusive usage, blocking if the file is currently\n    /// locked.\n    fn lock_exclusive(&self) -> Result<()>;\n\n    /// Locks the file for shared usage, or returns a an error if the file is\n    /// currently locked (see `lock_contended_error`).\n    fn try_lock_shared(&self) -> Result<()>;\n\n    /// Locks the file for shared usage, or returns a an error if the file is\n    /// currently locked (see `lock_contended_error`).\n    fn try_lock_exclusive(&self) -> Result<()>;\n\n    /// Unlocks the file.\n    fn unlock(&self) -> Result<()>;\n}\n\nimpl FileExt for File {\n    fn duplicate(&self) -> Result<File> {\n        sys::duplicate(self)\n    }\n    fn allocated_size(&self) -> Result<u64> {\n        sys::allocated_size(self)\n    }\n    fn allocate(&self, len: u64) -> Result<()> {\n        sys::allocate(self, len)\n    }\n    fn lock_shared(&self) -> Result<()> {\n        sys::lock_shared(self)\n    }\n    fn lock_exclusive(&self) -> Result<()> {\n        sys::lock_exclusive(self)\n    }\n    fn try_lock_shared(&self) -> Result<()> {\n        sys::try_lock_shared(self)\n    }\n    fn try_lock_exclusive(&self) -> Result<()> {\n        sys::try_lock_exclusive(self)\n    }\n    fn unlock(&self) -> Result<()> {\n        sys::unlock(self)\n    }\n}\n\n/// Returns the error that a call to a try lock method on a contended file will\n/// return.\npub fn lock_contended_error() -> Error {\n    sys::lock_error()\n}\n\n/// `FsStats` contains some common stats about a file system.\n#[derive(Clone, Debug, PartialEq, Eq, Hash)]\npub struct FsStats {\n    free_space: u64,\n    available_space: u64,\n    total_space: u64,\n    allocation_granularity: u64,\n}\n\nimpl FsStats {\n    /// Returns the number of free bytes in the file system containing the provided\n    /// path.\n    pub fn free_space(&self) -> u64 {\n        self.free_space\n    }\n\n    /// Returns the available space in bytes to non-priveleged users in the file\n    /// system containing the provided path.\n    pub fn available_space(&self) -> u64 {\n        self.available_space\n    }\n\n    /// Returns the total space in bytes in the file system containing the provided\n    /// path.\n    pub fn total_space(&self) -> u64 {\n        self.total_space\n    }\n\n    /// Returns the filesystem's disk space allocation granularity in bytes.\n    /// The provided path may be for any file in the filesystem.\n    ///\n    /// On Posix, this is equivalent to the filesystem's block size.\n    /// On Windows, this is equivalent to the filesystem's cluster size.\n    pub fn allocation_granularity(&self) -> u64 {\n        self.allocation_granularity\n    }\n}\n\n/// Get the stats of the file system containing the provided path.\npub fn statvfs<P>(path: P) -> Result<FsStats> where P: AsRef<Path> {\n    sys::statvfs(path.as_ref())\n}\n\n/// Returns the number of free bytes in the file system containing the provided\n/// path.\npub fn free_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.free_space)\n}\n\n/// Returns the available space in bytes to non-priveleged users in the file\n/// system containing the provided path.\npub fn available_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.available_space)\n}\n\n/// Returns the total space in bytes in the file system containing the provided\n/// path.\npub fn total_space<P>(path: P) -> Result<u64> where P: AsRef<Path> {\n    statvfs(path).map(|stat| stat.total_space)\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188]},"fileSize":16403,"meta":{"source":{"type":"path","value":"C:/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fs2-0.4.3/src/lib.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":458,"totalBytes":6255,"outputLines":188,"outputBytes":6255,"shownRange":{"start":1,"end":188},"nextOffset":189}}},"isError":false,"timestamp":1789522929274}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YhPV3uyItroprRg9SyVxexXf|fc_0cdf94810c888815016aa9f406d85c87d195ec1b94a16a80d3","toolName":"grep","startedAt":"2026-09-16T01:42:30.923Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src;.worktrees/308-registry-process-lock/crates/spt/src"},"intent":"Tracing serving request routing"},"id":"2714179b","parentId":"a5fea163","timestamp":"2026-09-16T01:42:30.923Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_iRGXhgH7973VGqD3f9QqAv7w|fc_0cdf94810c888815016aa9f406d86c87d19cf735ce18b3a9c8","toolName":"grep","startedAt":"2026-09-16T01:42:30.923Z","args":{"path":".worktrees/308-registry-process-lock/crates"},"intent":"Checking alternative registry write paths"},"id":"5537f065","parentId":"2714179b","timestamp":"2026-09-16T01:42:30.923Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_g8zJeatvhBJG94eWzWibH0pE|fc_0cdf94810c888815016aa9f406d87487d1af19cd9103f98d9f","toolName":"read","startedAt":"2026-09-16T01:42:30.923Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs:118-163"},"intent":"Reading durable save lifecycle"},"id":"448caf28","parentId":"5537f065","timestamp":"2026-09-16T01:42:30.923Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Z09sdPoWdWZe5uuTsdsFZ45o|fc_0cdf94810c888815016aa9f406d87887d1baa6d4db203e55a4","toolName":"read","startedAt":"2026-09-16T01:42:30.923Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-store/Cargo.toml:1-120"},"intent":"Reading test binary declarations"},"id":"50a083b2","parentId":"448caf28","timestamp":"2026-09-16T01:42:30.923Z"}
{"type":"message","id":"47751a6c","parentId":"50a083b2","timestamp":"2026-09-16T01:42:30.924Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Verifying source evidence**\n\n**Checking AddInputRef errors**"},{"type":"toolCall","id":"call_YhPV3uyItroprRg9SyVxexXf|fc_0cdf94810c888815016aa9f406d85c87d195ec1b94a16a80d3","name":"grep","arguments":{"i":"Tracing serving request routing","pattern":"KIND_SERVE_REQUEST|servehost::handle|INPUT_PATH_REGISTRY|AddInputReference|SERVE_REAP_LOCK_FAIL","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src;.worktrees/308-registry-process-lock/crates/spt/src","case":true,"gitignore":true,"skip":null},"intent":"Tracing serving request routing"},{"type":"toolCall","id":"call_iRGXhgH7973VGqD3f9QqAv7w|fc_0cdf94810c888815016aa9f406d86c87d19cf735ce18b3a9c8","name":"grep","arguments":{"i":"Checking alternative registry write paths","pattern":"serve[/\\\\]registry|join\\(\"serve\"\\)|join\\(\"registry\"\\)|remove_file\\([^\\n]*registry|remove_dir_all\\([^\\n]*serve","path":".worktrees/308-registry-process-lock/crates","case":true,"gitignore":true,"skip":null},"intent":"Checking alternative registry write paths"},{"type":"toolCall","id":"call_g8zJeatvhBJG94eWzWibH0pE|fc_0cdf94810c888815016aa9f406d87487d1af19cd9103f98d9f","name":"read","arguments":{"i":"Reading durable save lifecycle","path":".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs:118-163"},"intent":"Reading durable save lifecycle"},{"type":"toolCall","id":"call_Z09sdPoWdWZe5uuTsdsFZ45o|fc_0cdf94810c888815016aa9f406d87887d1baa6d4db203e55a4","name":"read","arguments":{"i":"Reading test binary declarations","path":".worktrees/308-registry-process-lock/crates/spt-store/Cargo.toml:1-120"},"intent":"Reading test binary declarations"},{"type":"toolCall","id":"call_ZSiOYlbqfVhasoA5ZUal0FFa|fc_0cdf94810c888815016aa9f406d87c87d18a897ab96ba227ff","name":"read","arguments":{"i":"Checking listener-only fixture writers","path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs:1-145"},"intent":"Checking listener-only fixture writers"},{"type":"toolCall","id":"call_KHXyRdPpv0ki5RGCfwXuQm5G|fc_0cdf94810c888815016aa9f406d88487d1a6aaa7a577d0f4ec","name":"grep","arguments":{"i":"Checking active hazard tags","pattern":"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE|PREP-ONLY","path":".worktrees/308-registry-process-lock/traceable-reqs.toml;.worktrees/308-registry-process-lock/docs/STORAGE.md;.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md;.worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","case":true,"gitignore":true,"skip":null},"intent":"Checking active hazard tags"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":10909,"output":676,"cacheRead":56192,"cacheWrite":0,"totalTokens":67777,"reasoningTokens":187,"cost":{"input":0.10909,"output":0.033800000000000004,"cacheRead":0.056192,"cacheWrite":0,"total":0.199082}},"stopReason":"toolUse","timestamp":1789522929283,"responseId":"resp_0cdf94810c888815016aa9f3f17d0487d1ae15210e70f89d44","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cdf94810c888815016aa9f3f2926087d1a5346d0b769bb99e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqfQGW65UrlUK5bboj0OehRWOVR1Kfb75cvt-zZ5yr89nfQiP_ADrBM9QXuoqqsvaJ7bKigOqo5gyAY6ZQzi8GH7191jgtySAeevflcVygewGLhBTwZybro5xB-tdUgAXZTnFw9Ro93ka6KOD7tqtC_kie4m-lTLBn3frk-_b6tssbVz1xEl3iHWAYTg5SnXZwumuN-MUDdNITU_9cLCdyUUm7LSRSBYnNgAegH0h1dEB3jlvaNcSp9O8FVug0698au4GLsczkYVyZ8mmx5Fk-_dVodzXsLIz7NUqtC7LZ6l6kF6zgLDqk1b3Qba4NkZq1s4eoR0Pp3AiW15PBTRx13e-AuE1RU0gXhec5FpQ4Yl8Bfnffi-ZfRaGyQ4sixQdY8NauUWfgp5O51AeyfPagjJHUmXwBdGEnd2iRyWK4KoqBZQCvch5-OaxgRaJomWkJQbOGhnYgYQg8AYv9hKzP0CO4lgyihAuWwJx5eGOFT_k8foQcYq_vhr3WTZnkJjfFqyNQD2EdJhkrSYk_zA4A5zEz4nBaPMxB6LAnfxApmX_trVFwzoxLAXv-NHcHW8wN2haq37bSbcKSjnR1BxpTCpAEw6PBpfS2CFKYHUctrYHw7xpeFYzYWpK-VU2udDcLbhkNOS3KyKFrjIn9vqJpP-7v5aPv1P2mSudxjh_zv1i-VGtP7ZdrxIkGWUK0jTJgn7KSYlb2Jykip8DUtwNBVac5M_n9ThMgII_Csz5MM8QHhWSCoJDtoq7oZ9GFIFDTzmGjfYHRtIlClBK7-jg_m3fK90LGAdhjC7OnGsDH2dD4_HMO61EmPa0OIxbPS0wbhgx0GJmAapuaSXwW8oSZvXOou-1MpoXcW33sFKms1lMQ1AWcbmM9o3rBkpc0sOuOzpaqiywcH4XkN8GlvQczqW2ewpfdZpmb6RlrISrfVYvXvWG4CK1zNT7ydbYts-uJLjxdTeYNiu-0DydIQwuprLr5HiEoyLbKP-UQpThKmMjTOK0GjMU-bTTgJMUMSmFV2W5pLfUaBhT-xPmryuiFglGrufmyH-Sd5FfWApCZLTvqlgvuS9-_UI4H0XuMyvezZJn4HjvlEfkdQyyozVa1B9p7mzmRRfSFPaXRgwVAEOPlxuoeH9xRtawL_atpvWylXqtS-RthTwOdAX_TGKbzylieM3Kll1l7Mie0u2RJEzOac1qVtvUE-WAu-2D9BL9Puic2bD0hmz-uf4f9nQeYgZRLra3QlbVm6II753sL72Ndi7cCRtT9rehfchLt0S9y4L9CznetBe_8TnZMnDe8DojQ4HXDBrMlaUFhBuMSgmI9QWyCPhkXHM-r3AFj_MFJcjeFQaIvR8GEHuvLipmuK-IdzYPp5DWgrCLv2fLAydYiw5E6JdSJ-j28U6426589jwucnDrZWbwKVOIURTzO2rlqxkALVU9xKqabQ8tMAVYXzuyUD6Qbi-QZSv8lH3vs1HMhDHoxpw7cEzAlJGQxFISdb7ZWDH6_EieHCKuw4aO9e4ODBQvJp3LIXB3qiKACSI1c-F8cePuC25EIY3VEbCjA6gKjA4S722YKkm-VNXcBxR4yeMCu3VDDhwaV-Nk7m6kit7m6oK8ZM5BSjAX-Ucn0sU-sgHifjIR2HebojDF-qw_6HmdxWyPjmo-zYZ7nM858z0rhA2DgMfE6DuX6U1TN6s-j4sRVXAtyRLKEUNVdAAxFn2wWFdL5FC2dhMhTFFu4EVTjp5Zom_goat4NGnOryD_q8CZgmN6ejOrg3cBj93QPxYgiczGWUs7-RPUdpFmEvPiDXfXOH5yZap8SHs7a0q6is7Phax4lQATNxTeds8AVmBzM31favpsDgT3cTQQx3e3ah8-FA99qffAyaenm7wPk-2T9-ZXTBKShZZV9hmYwrnetCiFRi7juVcLIi1sLF7RpUa-F208KpWF1TmkaciN3AJFPeVDHQMEuTGxUhfEhxeSegIB38GgYBC66J7y6vWwKkbFrRlWUzGNskhXXQSZAiO4Ib7zSINkRaxGKSHKyBz-BXl5AECCEp2b7xVUQGC9Jv_BK05_a8HT_mYdJ0JQU6HbHlK3lqE5Ryh8kCBhBSoEIkICx7ON_GAu32XMIGwznuSf89TbqpTprVoTVqZJHnayCm1OmK9TzZW9Teqe9GNVYmJs-obBKcMOkIh5Y17XtibkWskei-Yw4H8VnwM1bpii2ERNlBmLVeUwuYt0K2MTXZfI16i9vBW3p_49DMlx_asGtuZpw5CsyQ2x8CauB8GNaqo-ppvIGNL_Bc9t16osHcavxq0pcEo9v2Ce_crv7w7NLq--RlhnNd1V8AoEnW9h2J-mAwGmlqspQlpxedAiq3-6fQu79kTpzyQt5votUImhjwUSAIXQ2ZX58BpWZHeCEp0D8BPaBatjOnz4INSk2ca2d9teoBJR-bHxFA_UFHocanIxt299JLQBeUeuWfXhSzlT34HSyRBPA6ZDHm8ZFSmuKLPArRcQ1BTggUFE1cIunX5h5_lUJzvp5UnOMuoBOBGDC73S3X6ySNp64K6--JB5TPY_HrWJi_2QlMKI6ev9DF3OA3guD77TNasE9oPesQk2ZtVCDFjOoMHzLJhSC3irMgcrZxnl7LbPLRK8DML42rrqT5SNNGvtyzG-5n1DfTX_mL5906SbAX5fVj5cwr464fwTMnW4Y_Sd","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"summary":[{"type":"summary_text","text":"**Verifying source evidence**"},{"type":"summary_text","text":"**Checking AddInputRef errors**"}],"metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f406d85c87d195ec1b94a16a80d3","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing serving request routing\",\"pattern\":\"KIND_SERVE_REQUEST|servehost::handle|INPUT_PATH_REGISTRY|AddInputReference|SERVE_REAP_LOCK_FAIL\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src;.worktrees/308-registry-process-lock/crates/spt/src\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_YhPV3uyItroprRg9SyVxexXf","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f406d86c87d19cf735ce18b3a9c8","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking alternative registry write paths\",\"pattern\":\"serve[/\\\\\\\\]registry|join\\\\(\\\"serve\\\"\\\\)|join\\\\(\\\"registry\\\"\\\\)|remove_file\\\\([^\\\\n]*registry|remove_dir_all\\\\([^\\\\n]*serve\",\"path\":\".worktrees/308-registry-process-lock/crates\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_iRGXhgH7973VGqD3f9QqAv7w","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f406d87487d1af19cd9103f98d9f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading durable save lifecycle\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-store/src/atomic.rs:118-163\"}","call_id":"call_g8zJeatvhBJG94eWzWibH0pE","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f406d87887d1baa6d4db203e55a4","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading test binary declarations\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-store/Cargo.toml:1-120\"}","call_id":"call_Z09sdPoWdWZe5uuTsdsFZ45o","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f406d87c87d18a897ab96ba227ff","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking listener-only fixture writers\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs:1-145\"}","call_id":"call_ZSiOYlbqfVhasoA5ZUal0FFa","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f406d88487d1a6aaa7a577d0f4ec","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking active hazard tags\",\"pattern\":\"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE|PREP-ONLY\",\"path\":\".worktrees/308-registry-process-lock/traceable-reqs.toml;.worktrees/308-registry-process-lock/docs/STORAGE.md;.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md;.worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_KHXyRdPpv0ki5RGCfwXuQm5G","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":21636.344700000016,"ttft":1309.6063999999897,"completedAt":1789522950920,"contextSnapshot":{"promptTokens":67101,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZSiOYlbqfVhasoA5ZUal0FFa|fc_0cdf94810c888815016aa9f406d87c87d18a897ab96ba227ff","toolName":"read","startedAt":"2026-09-16T01:42:30.924Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs:1-145"},"intent":"Checking listener-only fixture writers"},"id":"8f1428f6","parentId":"47751a6c","timestamp":"2026-09-16T01:42:30.924Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KHXyRdPpv0ki5RGCfwXuQm5G|fc_0cdf94810c888815016aa9f406d88487d1a6aaa7a577d0f4ec","toolName":"grep","startedAt":"2026-09-16T01:42:30.924Z","args":{"path":".worktrees/308-registry-process-lock/traceable-reqs.toml;.worktrees/308-registry-process-lock/docs/STORAGE.md;.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md;.worktrees/308-registry-process…"},"intent":"Checking active hazard tags"},"id":"4725826d","parentId":"8f1428f6","timestamp":"2026-09-16T01:42:30.924Z"}
{"type":"message","id":"c49031e9","parentId":"4725826d","timestamp":"2026-09-16T01:42:30.937Z","message":{"role":"toolResult","toolCallId":"call_Z09sdPoWdWZe5uuTsdsFZ45o|fc_0cdf94810c888815016aa9f406d87887d1baa6d4db203e55a4","toolName":"read","content":[{"type":"text","text":"[package]\nname = \"spt-store\"\nversion.workspace = true\nedition = \"2021\"\npublish = false\ndescription = \"spt-core persistence layer: atomic writes, spool, perch layout, registry, info.json. Built on spt-proto.\"\n\n# Test-only fixture binary for `tests/wtlock_two_process_int.rs`\n# (REQ-PSYCHE-INGEST-SERIALIZED): a SECOND PROCESS that writes into a shared\n# context-store worktree, since the writers the lock exists for are separate\n# processes and a thread-based rig would prove the wrong arm. Its source lives\n# under `tests/` so it never ships from `src/` — the same tests/-homed `[[bin]]`\n# pattern the `spt` crate's fixtures use; `cargo test -p spt-store` builds it and\n# exposes `CARGO_BIN_EXE_wtlock_worker` to the integration test.\n[[bin]]\nname = \"wtlock_worker\"\npath = \"tests/fixtures/wtlock_worker.rs\"\n\n# Test-only second-process fixture for the serving-registry transaction lock.\n[[bin]]\nname = \"serving_registry_worker\"\npath = \"tests/fixtures/serving_registry_worker.rs\"\n\n[dependencies]\nfs2 = \"0.4\"\n# The ONE string-trigger match rule (`matchrule`), shared by monic triggers and\n# the manifest's keyword hints — regex-opt-in patterns compile through this.\n# Already in the tree; spt-runtime consumes the rule from here rather than\n# keeping a second copy of it.\nregex = \"1\"\n# FIDO2 seal-signature VERIFICATION only (SIGNET W2, REQ-SEAL-FIDO2-SIG-VERIFY;\n# doyle dep ruling 2026-08-24): core and every member node touch PUBLIC key\n# material only — signature CREATION stays behind the platform-authenticator\n# seam (CNG/Hello on Windows), so RUSTSEC-2023-0071 (Marvin: private-key-op\n# timing side channel) cannot reach this consumer; that scoping is the\n# security rationale, stated here so the next audit does not re-derive it.\nrsa = { version = \"0.9\", default-features = false, features = [\"sha2\"] }\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = { version = \"1\", features = [\"preserve_order\"] }\nsha2 = \"0.10\"\nspt-proto = { path = \"../spt-proto\" }\nspt-procident = { path = \"../spt-procident\" }\nuuid = { version = \"1\", features = [\"v4\"] }\n\n[target.'cfg(unix)'.dependencies]\nlibc = \"0.2\"\n\n[dev-dependencies]\ntempfile = \"3\"\n# TEST-ONLY signing key material for the verify-only rsa arm's units (the\n# production rule stands: signature creation never happens in core — a test\n# signing a fixture payload is how a verifier gets a true positive to check).\nrand = \"0.8\"\n# Same version/features as the main dep (feature-unified, no second build) — the\n# carrier-claim race rig matches rusqlite's DatabaseBusy error code directly.\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\n\n# The build-cache pool guard (releases#103). A build script sees ONLY\n# build-dependencies, which is the whole reason the identity predicate moved into\n# its own leaf crate rather than being copied.\n[build-dependencies]\nspt-poolguard = { path = \"../spt-poolguard\" }"}],"details":{"totalLines":63,"displayContent":{"text":"[package]\r\nname = \"spt-store\"\r\nversion.workspace = true\r\nedition = \"2021\"\r\npublish = false\r\ndescription = \"spt-core persistence layer: atomic writes, spool, perch layout, registry, info.json. Built on spt-proto.\"\r\n\r\n# Test-only fixture binary for `tests/wtlock_two_process_int.rs`\r\n# (REQ-PSYCHE-INGEST-SERIALIZED): a SECOND PROCESS that writes into a shared\r\n# context-store worktree, since the writers the lock exists for are separate\r\n# processes and a thread-based rig would prove the wrong arm. Its source lives\r\n# under `tests/` so it never ships from `src/` — the same tests/-homed `[[bin]]`\r\n# pattern the `spt` crate's fixtures use; `cargo test -p spt-store` builds it and\r\n# exposes `CARGO_BIN_EXE_wtlock_worker` to the integration test.\r\n[[bin]]\r\nname = \"wtlock_worker\"\r\npath = \"tests/fixtures/wtlock_worker.rs\"\r\n\r\n# Test-only second-process fixture for the serving-registry transaction lock.\r\n[[bin]]\r\nname = \"serving_registry_worker\"\r\npath = \"tests/fixtures/serving_registry_worker.rs\"\r\n\r\n[dependencies]\r\nfs2 = \"0.4\"\r\n# The ONE string-trigger match rule (`matchrule`), shared by monic triggers and\r\n# the manifest's keyword hints — regex-opt-in patterns compile through this.\r\n# Already in the tree; spt-runtime consumes the rule from here rather than\r\n# keeping a second copy of it.\r\nregex = \"1\"\r\n# FIDO2 seal-signature VERIFICATION only (SIGNET W2, REQ-SEAL-FIDO2-SIG-VERIFY;\r\n# doyle dep ruling 2026-08-24): core and every member node touch PUBLIC key\r\n# material only — signature CREATION stays behind the platform-authenticator\r\n# seam (CNG/Hello on Windows), so RUSTSEC-2023-0071 (Marvin: private-key-op\r\n# timing side channel) cannot reach this consumer; that scoping is the\r\n# security rationale, stated here so the next audit does not re-derive it.\r\nrsa = { version = \"0.9\", default-features = false, features = [\"sha2\"] }\r\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\r\nserde = { version = \"1\", features = [\"derive\"] }\r\nserde_json = { version = \"1\", features = [\"preserve_order\"] }\r\nsha2 = \"0.10\"\r\nspt-proto = { path = \"../spt-proto\" }\r\nspt-procident = { path = \"../spt-procident\" }\r\nuuid = { version = \"1\", features = [\"v4\"] }\r\n\r\n[target.'cfg(unix)'.dependencies]\r\nlibc = \"0.2\"\r\n\r\n[dev-dependencies]\r\ntempfile = \"3\"\r\n# TEST-ONLY signing key material for the verify-only rsa arm's units (the\r\n# production rule stands: signature creation never happens in core — a test\r\n# signing a fixture payload is how a verifier gets a true positive to check).\r\nrand = \"0.8\"\r\n# Same version/features as the main dep (feature-unified, no second build) — the\r\n# carrier-claim race rig matches rusqlite's DatabaseBusy error code directly.\r\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\r\n\r\n# The build-cache pool guard (releases#103). A build script sees ONLY\r\n# build-dependencies, which is the whole reason the identity predicate moved into\r\n# its own leaf crate rather than being copied.\r\n[build-dependencies]\r\nspt-poolguard = { path = \"../spt-poolguard\" }\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]},"fileSize":2997,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-store\\Cargo.toml"}}},"isError":false,"timestamp":1789522950936}}
{"type":"message","id":"95da5980","parentId":"c49031e9","timestamp":"2026-09-16T01:42:30.941Z","message":{"role":"toolResult","toolCallId":"call_g8zJeatvhBJG94eWzWibH0pE|fc_0cdf94810c888815016aa9f406d87487d1af19cd9103f98d9f","toolName":"read","content":[{"type":"text","text":"\n/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\n/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\n/// reconstructible-state default, [`atomic_write_bytes`]).\n///\n/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\n/// 5.15). A fixed `{name}.tmp` shared by every writer is a latent collision: two\n/// processes writing the same target concurrently (e.g. the daemon's `mutate_info`\n/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\n/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\n/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\n/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\n/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\n// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\nfn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\n    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\n    let tmp = path.with_file_name(format!(\n        \"{}.tmp.{}-{}\",\n        path.file_name().and_then(|s| s.to_str()).unwrap_or(\"epoch\"),\n        std::process::id(),\n        seq\n    ));\n    if durable {\n        let mut f = fs::File::create(&tmp)?;\n        f.write_all(body)?;\n        f.sync_all()?; // durable data BEFORE the rename publishes the name\n    } else {\n        fs::write(&tmp, body)?;\n    }\n    match rename_with_retry(&tmp, path) {\n        Ok(()) => Ok(()),\n        Err(e) => {\n            // Best-effort cleanup of the orphaned tmp; never mask the real error.\n            let _ = fs::remove_file(&tmp);\n            Err(e)\n        }\n    }\n}\n\n/// Rename `from` → `to`, retrying transient sharing/access failures with\n/// linear backoff. Non-transient errors (e.g. NotFound) return immediately.\n// [impl->REQ-HAZARD-EBUSY-RENAME]\nfn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {\n    retry_transient(MAX_RENAME_ATTEMPTS, backoff, || fs::rename(from, to))\n}\n\n/// Linear backoff: attempt 1 → 2ms, attempt 2 → 4ms, … capped modestly. Kept\n/// small so a real transient (handle released within tens of ms) is absorbed\n/// without a perceptible stall.\nfn backoff(attempt: u32) -> Duration {\n…\n}\n\n[Showing lines 117-166 of 392. Use :167 to continue]"}],"details":{"truncation":{"content":"\r\n/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\r\n/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\r\n/// reconstructible-state default, [`atomic_write_bytes`]).\r\n///\r\n/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\r\n/// 5.15). A fixed `{name}.tmp` shared by every writer is a latent collision: two\r\n/// processes writing the same target concurrently (e.g. the daemon's `mutate_info`\r\n/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\r\n/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\r\n/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\r\n/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\r\n/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\r\n// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\r\nfn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\r\n    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\r\n    let tmp = path.with_file_name(format!(\r\n        \"{}.tmp.{}-{}\",\r\n        path.file_name().and_then(|s| s.to_str()).unwrap_or(\"epoch\"),\r\n        std::process::id(),\r\n        seq\r\n    ));\r\n    if durable {\r\n        let mut f = fs::File::create(&tmp)?;\r\n        f.write_all(body)?;\r\n        f.sync_all()?; // durable data BEFORE the rename publishes the name\r\n    } else {\r\n        fs::write(&tmp, body)?;\r\n    }\r\n    match rename_with_retry(&tmp, path) {\r\n        Ok(()) => Ok(()),\r\n        Err(e) => {\r\n            // Best-effort cleanup of the orphaned tmp; never mask the real error.\r\n            let _ = fs::remove_file(&tmp);\r\n            Err(e)\r\n        }\r\n    }\r\n}\r\n\r\n/// Rename `from` → `to`, retrying transient sharing/access failures with\r\n/// linear backoff. Non-transient errors (e.g. NotFound) return immediately.\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\nfn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {\r\n    retry_transient(MAX_RENAME_ATTEMPTS, backoff, || fs::rename(from, to))\r\n}\r\n\r\n/// Linear backoff: attempt 1 → 2ms, attempt 2 → 4ms, … capped modestly. Kept\r\n/// small so a real transient (handle released within tens of ms) is absorbed\r\n/// without a perceptible stall.\r\nfn backoff(attempt: u32) -> Duration {\r","truncated":true,"truncatedBy":"lines","totalLines":276,"totalBytes":2343,"outputLines":50,"outputBytes":2343,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":392,"displayContent":{"text":"\r\n/// Shared tmp-write + atomic-rename core. `durable` selects the fsync-before-rename\r\n/// path ([`atomic_write_bytes_durable`]); otherwise a plain `fs::write` (the\r\n/// reconstructible-state default, [`atomic_write_bytes`]).\r\n///\r\n/// The tmp sibling is UNIQUE per write — `{name}.tmp.{pid}-{seq}` (KNOWN-HAZARDS\r\n/// 5.15). A fixed `{name}.tmp` shared by every writer is a latent collision: two\r\n/// processes writing the same target concurrently (e.g. the daemon's `mutate_info`\r\n/// RMW and `spt api bind`'s establish, traced ~700µs apart at bind) both stage the\r\n/// SAME tmp; whichever renames first CONSUMES it, and the loser's rename hits\r\n/// `NotFound` (os error 2) — non-transient, so it surfaces as a hard write error\r\n/// (the bind-fatal path that wedged the endpoint offline). A microsecond window\r\n/// forever; `sync_all` widened create→rename ~50× and made it near-certain at bind.\r\n// [impl->REQ-HAZARD-ATOMIC-TMP-COLLISION]\r\nfn write_then_rename(path: &Path, body: &[u8], durable: bool) -> io::Result<()> {\r\n    let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);\r\n    let tmp = path.with_file_name(format!(\r\n        \"{}.tmp.{}-{}\",\r\n        path.file_name().and_then(|s| s.to_str()).unwrap_or(\"epoch\"),\r\n        std::process::id(),\r\n        seq\r\n    ));\r\n    if durable {\r\n        let mut f = fs::File::create(&tmp)?;\r\n        f.write_all(body)?;\r\n        f.sync_all()?; // durable data BEFORE the rename publishes the name\r\n    } else {\r\n        fs::write(&tmp, body)?;\r\n    }\r\n    match rename_with_retry(&tmp, path) {\r\n        Ok(()) => Ok(()),\r\n        Err(e) => {\r\n            // Best-effort cleanup of the orphaned tmp; never mask the real error.\r\n            let _ = fs::remove_file(&tmp);\r\n            Err(e)\r\n        }\r\n    }\r\n}\r\n\r\n/// Rename `from` → `to`, retrying transient sharing/access failures with\r\n/// linear backoff. Non-transient errors (e.g. NotFound) return immediately.\r\n// [impl->REQ-HAZARD-EBUSY-RENAME]\r\nfn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {\r\n    retry_transient(MAX_RENAME_ATTEMPTS, backoff, || fs::rename(from, to))\r\n}\r\n\r\n/// Linear backoff: attempt 1 → 2ms, attempt 2 → 4ms, … capped modestly. Kept\r\n/// small so a real transient (handle released within tens of ms) is absorbed\r\n/// without a perceptible stall.\r\nfn backoff(attempt: u32) -> Duration {\r\n…\n}","startLine":117,"lineNumbers":[117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,null,168]},"fileSize":18102,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-store\\src\\atomic.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":392,"totalBytes":2343,"outputLines":50,"outputBytes":2343,"shownRange":{"start":117,"end":166},"nextOffset":167}}},"isError":false,"timestamp":1789522950940}}
{"type":"message","id":"845970b1","parentId":"95da5980","timestamp":"2026-09-16T01:42:30.954Z","message":{"role":"toolResult","toolCallId":"call_ZSiOYlbqfVhasoA5ZUal0FFa|fc_0cdf94810c888815016aa9f406d87c87d18a897ab96ba227ff","toolName":"read","content":[{"type":"text","text":"//! W0 serving over the production loopback listener, not a test HTTP server.\n//! Each fixture supplies its own home, node label and ephemeral port; registry\n//! writes model the daemon's persisted mutations without changing process env.\n\nuse std::io::{Read, Write};\nuse std::net::TcpStream;\nuse std::path::Path;\nuse std::time::Duration;\n\nuse spt_store::serving::{ServedEntry, ServingRegistry};\n\nfn http(port: u16, method: &str, path: &str) -> (u16, String, Vec<u8>) {\n    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\n    stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();\n    stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();\n    write!(stream, \"{method} {path} HTTP/1.1\\r\\nHost: localhost:{port}\\r\\nConnection: close\\r\\n\\r\\n\").unwrap();\n    let mut raw = Vec::new();\n    stream.read_to_end(&mut raw).expect(\"read serving response\");\n    let split = raw.windows(4).position(|window| window == b\"\\r\\n\\r\\n\").expect(\"HTTP header boundary\");\n    let headers = String::from_utf8(raw[..split].to_vec()).unwrap();\n    let status = headers.split_whitespace().nth(1).unwrap().parse().unwrap();\n    (status, headers, raw[split + 4..].to_vec())\n}\n\nfn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\n    let path = spt_store::perch::serving_registry_file_in(home);\n    let mut registry = ServingRegistry::load_at(&path).unwrap();\n    let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\n    registry.save_at(&path).unwrap();\n    entry\n}\n\n// [int->REQ-WEB-URL-NODE-PREFIX]\n// [int->REQ-WEB-SERVING-REGISTRY]\n#[test]\nfn production_listener_routes_nodes_docs_and_live_registry_references() {\n    let home = tempfile::tempdir().unwrap();\n    let docs = home.path().join(\"docs\");\n    std::fs::create_dir_all(docs.join(\"cli\")).unwrap();\n    std::fs::create_dir_all(docs.join(\"new-section\")).unwrap();\n    std::fs::write(docs.join(\"index.html\"), b\"<html>book</html>\").unwrap();\n    std::fs::write(docs.join(\"cli/reference.md\"), b\"# reference bytes\").unwrap();\n    std::fs::write(docs.join(\"new-section/page.html\"), b\"new section bytes\").unwrap();\n    std::fs::write(docs.join(\"llms-full.txt\"), b\"full docs bytes\").unwrap();\n    std::fs::write(docs.join(\"manifest.schema.json\"), b\"{\\\"type\\\":\\\"object\\\"}\").unwrap();\n    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), docs, \"LOCAL\".to_owned(), 0)\n        .expect(\"bind production serving listener\");\n\n    let (status, headers, _) = http(port, \"GET\", \"/\");\n    assert_eq!(status, 302);\n    assert!(headers.to_ascii_lowercase().contains(\"location: /local/\\r\\n\"));\n    for path in [\"/LoCaL/\", \"/local/?json\"] {\n        let (status, headers, _) = http(port, \"GET\", path);\n        assert_eq!(status, 302, \"{path}\");\n        let location = headers.lines().find_map(|line| {\n            let (name, value) = line.split_once(':')?;\n            name.eq_ignore_ascii_case(\"location\").then(|| value.trim())\n        }).expect(\"node root redirects to docs\");\n        assert_eq!(location.split('?').next().unwrap(), \"/local/docs/\");\n        assert_eq!(http(port, \"GET\", location).2, b\"<html>book</html>\");\n    }\n\n    // Registration after bind proves that the HTTP reader is not a startup snapshot.\n    let source = home.path().join(\"source & quote.txt\");\n    std::fs::write(&source, b\"first bytes\").unwrap();\n    let entry = add(home.path(), &source, \"report #1.txt\");\n    let uri = \"/local/f/report%20%231.txt\";\n    let (status, headers, _) = http(port, \"GET\", \"/local/\");\n    assert_eq!(status, 302);\n    assert!(headers.to_ascii_lowercase().contains(\"location: /local/docs/\"));\n    assert_eq!(http(port, \"GET\", uri).2, b\"first bytes\");\n    std::fs::write(&source, b\"edited bytes\").unwrap();\n    assert_eq!(http(port, \"GET\", uri).2, b\"edited bytes\");\n    let (status, headers, bytes) = http(port, \"HEAD\", uri);\n    assert_eq!(status, 200);\n    assert!(headers.to_ascii_lowercase().contains(\"content-type: text/plain\"));\n    assert!(bytes.is_empty());\n    let (status, headers, _) = http(port, \"POST\", uri);\n    assert_eq!(status, 405);\n    assert!(headers.to_ascii_lowercase().contains(\"allow: get, head\"));\n\n    for (path, expected) in [\n        (\"index.html\", b\"<html>book</html>\".as_slice()),\n        (\"cli/reference.md\", b\"# reference bytes\".as_slice()),\n        (\"new-section/page.html\", b\"new section bytes\".as_slice()),\n        (\"llms-full.txt\", b\"full docs bytes\".as_slice()),\n        (\"manifest.schema.json\", b\"{\\\"type\\\":\\\"object\\\"}\".as_slice()),\n    ] {\n        let (bare_status, _, bare_bytes) = http(port, \"GET\", &format!(\"/{path}\"));\n        let (prefixed_status, _, prefixed_bytes) = http(port, \"GET\", &format!(\"/local/docs/{path}\"));\n        assert_eq!(bare_status, 200, \"bare {path}\");\n        assert_eq!(prefixed_status, 200, \"prefixed {path}\");\n        assert_eq!(bare_bytes, expected);\n        assert_eq!(prefixed_bytes, expected);\n    }\n    assert_eq!(http(port, \"GET\", \"/local/docs/\").2, b\"<html>book</html>\");\n    assert_eq!(http(port, \"GET\", \"/cli/missing.md\").0, 404);\n    for facet in [\"m\", \"bin\", \"install\"] {\n        let (status, _, bytes) = http(port, \"GET\", &format!(\"/local/{facet}/anything\"));\n        assert_eq!(status, 404);\n        assert!(String::from_utf8(bytes).unwrap().contains(facet));\n    }\n    assert_eq!(http(port, \"GET\", \"/local/unknown/resource\").0, 404);\n    assert_eq!(http(port, \"GET\", \"/OTHER/f/report.md\").0, 404);\n    assert_eq!(http(port, \"GET\", \"/no-such-page.html\").0, 404);\n    assert_eq!(http(port, \"GET\", \"/unknown/no-such-page.html\").0, 404);\n    for uri in [\"/local/docs/%2e%2e/source.txt\", \"/local/f/report%2fchild\", \"/cli/../../secret\"] {\n        assert_eq!(http(port, \"GET\", uri).0, 400, \"{uri}\");\n    }\n\n    std::fs::remove_file(&source).unwrap();\n    assert_eq!(http(port, \"GET\", uri).0, 404);\n    std::fs::write(&source, b\"recreated\").unwrap();\n    assert_eq!(http(port, \"GET\", uri).2, b\"recreated\");\n    let path = spt_store::perch::serving_registry_file_in(home.path());\n    let mut registry = ServingRegistry::load_at(&path).unwrap();\n    registry.remove(&entry.id).unwrap();\n    registry.save_at(&path).unwrap();\n    assert_eq!(http(port, \"GET\", uri).0, 404);\n    std::fs::write(path, b\"invalid registry json\").unwrap();\n    // The retired index must not make the docs redirect depend on registry health.\n    assert_eq!(http(port, \"GET\", \"/local/?json\").0, 302);\n    let (status, _, bytes) = http(port, \"GET\", uri);\n    assert_eq!(status, 500);\n    assert!(String::from_utf8(bytes).unwrap().contains(\"SERVING_REGISTRY_LOAD_FAIL\"));\n}\n\n// [int->REQ-WEB-URL-NODE-PREFIX]\n#[test]\nfn subnet_node_labels_shadow_only_the_docs_compat_alias_while_members() {\n    use spt_net::net::registry::SubnetRegistry;\n    use spt_store::roster::{RosterEntry, RosterStore, Tombstone};\n    use spt_store::subnet::SubnetStore;\n\n    let home = tempfile::tempdir().unwrap();\n    let docs = home.path().join(\"docs\");\n    std::fs::create_dir_all(docs.join(\"cli\")).unwrap();\n    std::fs::write(docs.join(\"cli/reference.md\"), b\"# stable docs bytes\").unwrap();\n    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), docs.clone(), \"LOCAL\".to_owned(), 0)\n        .expect(\"bind production serving listener\");\n    let alias = \"/cli/reference.md\";\n    let canonical = \"/local/docs/cli/reference.md\";\n    let (_, alias_headers, alias_bytes) = http(port, \"GET\", alias);\n    assert_eq!(alias_bytes, b\"# stable docs bytes\");\n    assert_eq!(http(port, \"GET\", canonical).2, alias_bytes);\n\n    let identity = home.path().join(\"identity\");\n    let roster_path = identity.join(\"roster.json\");\n…\n}\n\n[Showing lines 1-148 of 455. Use :149 to continue]"}],"details":{"truncation":{"content":"//! W0 serving over the production loopback listener, not a test HTTP server.\r\n//! Each fixture supplies its own home, node label and ephemeral port; registry\r\n//! writes model the daemon's persisted mutations without changing process env.\r\n\r\nuse std::io::{Read, Write};\r\nuse std::net::TcpStream;\r\nuse std::path::Path;\r\nuse std::time::Duration;\r\n\r\nuse spt_store::serving::{ServedEntry, ServingRegistry};\r\n\r\nfn http(port: u16, method: &str, path: &str) -> (u16, String, Vec<u8>) {\r\n    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\r\n    stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();\r\n    stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();\r\n    write!(stream, \"{method} {path} HTTP/1.1\\r\\nHost: localhost:{port}\\r\\nConnection: close\\r\\n\\r\\n\").unwrap();\r\n    let mut raw = Vec::new();\r\n    stream.read_to_end(&mut raw).expect(\"read serving response\");\r\n    let split = raw.windows(4).position(|window| window == b\"\\r\\n\\r\\n\").expect(\"HTTP header boundary\");\r\n    let headers = String::from_utf8(raw[..split].to_vec()).unwrap();\r\n    let status = headers.split_whitespace().nth(1).unwrap().parse().unwrap();\r\n    (status, headers, raw[split + 4..].to_vec())\r\n}\r\n\r\nfn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\r\n    let path = spt_store::perch::serving_registry_file_in(home);\r\n    let mut registry = ServingRegistry::load_at(&path).unwrap();\r\n    let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\r\n    registry.save_at(&path).unwrap();\r\n    entry\r\n}\r\n\r\n// [int->REQ-WEB-URL-NODE-PREFIX]\r\n// [int->REQ-WEB-SERVING-REGISTRY]\r\n#[test]\r\nfn production_listener_routes_nodes_docs_and_live_registry_references() {\r\n    let home = tempfile::tempdir().unwrap();\r\n    let docs = home.path().join(\"docs\");\r\n    std::fs::create_dir_all(docs.join(\"cli\")).unwrap();\r\n    std::fs::create_dir_all(docs.join(\"new-section\")).unwrap();\r\n    std::fs::write(docs.join(\"index.html\"), b\"<html>book</html>\").unwrap();\r\n    std::fs::write(docs.join(\"cli/reference.md\"), b\"# reference bytes\").unwrap();\r\n    std::fs::write(docs.join(\"new-section/page.html\"), b\"new section bytes\").unwrap();\r\n    std::fs::write(docs.join(\"llms-full.txt\"), b\"full docs bytes\").unwrap();\r\n    std::fs::write(docs.join(\"manifest.schema.json\"), b\"{\\\"type\\\":\\\"object\\\"}\").unwrap();\r\n    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), docs, \"LOCAL\".to_owned(), 0)\r\n        .expect(\"bind production serving listener\");\r\n\r\n    let (status, headers, _) = http(port, \"GET\", \"/\");\r\n    assert_eq!(status, 302);\r\n    assert!(headers.to_ascii_lowercase().contains(\"location: /local/\\r\\n\"));\r\n    for path in [\"/LoCaL/\", \"/local/?json\"] {\r\n        let (status, headers, _) = http(port, \"GET\", path);\r\n        assert_eq!(status, 302, \"{path}\");\r\n        let location = headers.lines().find_map(|line| {\r\n            let (name, value) = line.split_once(':')?;\r\n            name.eq_ignore_ascii_case(\"location\").then(|| value.trim())\r\n        }).expect(\"node root redirects to docs\");\r\n        assert_eq!(location.split('?').next().unwrap(), \"/local/docs/\");\r\n        assert_eq!(http(port, \"GET\", location).2, b\"<html>book</html>\");\r\n    }\r\n\r\n    // Registration after bind proves that the HTTP reader is not a startup snapshot.\r\n    let source = home.path().join(\"source & quote.txt\");\r\n    std::fs::write(&source, b\"first bytes\").unwrap();\r\n    let entry = add(home.path(), &source, \"report #1.txt\");\r\n    let uri = \"/local/f/report%20%231.txt\";\r\n    let (status, headers, _) = http(port, \"GET\", \"/local/\");\r\n    assert_eq!(status, 302);\r\n    assert!(headers.to_ascii_lowercase().contains(\"location: /local/docs/\"));\r\n    assert_eq!(http(port, \"GET\", uri).2, b\"first bytes\");\r\n    std::fs::write(&source, b\"edited bytes\").unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).2, b\"edited bytes\");\r\n    let (status, headers, bytes) = http(port, \"HEAD\", uri);\r\n    assert_eq!(status, 200);\r\n    assert!(headers.to_ascii_lowercase().contains(\"content-type: text/plain\"));\r\n    assert!(bytes.is_empty());\r\n    let (status, headers, _) = http(port, \"POST\", uri);\r\n    assert_eq!(status, 405);\r\n    assert!(headers.to_ascii_lowercase().contains(\"allow: get, head\"));\r\n\r\n    for (path, expected) in [\r\n        (\"index.html\", b\"<html>book</html>\".as_slice()),\r\n        (\"cli/reference.md\", b\"# reference bytes\".as_slice()),\r\n        (\"new-section/page.html\", b\"new section bytes\".as_slice()),\r\n        (\"llms-full.txt\", b\"full docs bytes\".as_slice()),\r\n        (\"manifest.schema.json\", b\"{\\\"type\\\":\\\"object\\\"}\".as_slice()),\r\n    ] {\r\n        let (bare_status, _, bare_bytes) = http(port, \"GET\", &format!(\"/{path}\"));\r\n        let (prefixed_status, _, prefixed_bytes) = http(port, \"GET\", &format!(\"/local/docs/{path}\"));\r\n        assert_eq!(bare_status, 200, \"bare {path}\");\r\n        assert_eq!(prefixed_status, 200, \"prefixed {path}\");\r\n        assert_eq!(bare_bytes, expected);\r\n        assert_eq!(prefixed_bytes, expected);\r\n    }\r\n    assert_eq!(http(port, \"GET\", \"/local/docs/\").2, b\"<html>book</html>\");\r\n    assert_eq!(http(port, \"GET\", \"/cli/missing.md\").0, 404);\r\n    for facet in [\"m\", \"bin\", \"install\"] {\r\n        let (status, _, bytes) = http(port, \"GET\", &format!(\"/local/{facet}/anything\"));\r\n        assert_eq!(status, 404);\r\n        assert!(String::from_utf8(bytes).unwrap().contains(facet));\r\n    }\r\n    assert_eq!(http(port, \"GET\", \"/local/unknown/resource\").0, 404);\r\n    assert_eq!(http(port, \"GET\", \"/OTHER/f/report.md\").0, 404);\r\n    assert_eq!(http(port, \"GET\", \"/no-such-page.html\").0, 404);\r\n    assert_eq!(http(port, \"GET\", \"/unknown/no-such-page.html\").0, 404);\r\n    for uri in [\"/local/docs/%2e%2e/source.txt\", \"/local/f/report%2fchild\", \"/cli/../../secret\"] {\r\n        assert_eq!(http(port, \"GET\", uri).0, 400, \"{uri}\");\r\n    }\r\n\r\n    std::fs::remove_file(&source).unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).0, 404);\r\n    std::fs::write(&source, b\"recreated\").unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).2, b\"recreated\");\r\n    let path = spt_store::perch::serving_registry_file_in(home.path());\r\n    let mut registry = ServingRegistry::load_at(&path).unwrap();\r\n    registry.remove(&entry.id).unwrap();\r\n    registry.save_at(&path).unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).0, 404);\r\n    std::fs::write(path, b\"invalid registry json\").unwrap();\r\n    // The retired index must not make the docs redirect depend on registry health.\r\n    assert_eq!(http(port, \"GET\", \"/local/?json\").0, 302);\r\n    let (status, _, bytes) = http(port, \"GET\", uri);\r\n    assert_eq!(status, 500);\r\n    assert!(String::from_utf8(bytes).unwrap().contains(\"SERVING_REGISTRY_LOAD_FAIL\"));\r\n}\r\n\r\n// [int->REQ-WEB-URL-NODE-PREFIX]\r\n#[test]\r\nfn subnet_node_labels_shadow_only_the_docs_compat_alias_while_members() {\r\n    use spt_net::net::registry::SubnetRegistry;\r\n    use spt_store::roster::{RosterEntry, RosterStore, Tombstone};\r\n    use spt_store::subnet::SubnetStore;\r\n\r\n    let home = tempfile::tempdir().unwrap();\r\n    let docs = home.path().join(\"docs\");\r\n    std::fs::create_dir_all(docs.join(\"cli\")).unwrap();\r\n    std::fs::write(docs.join(\"cli/reference.md\"), b\"# stable docs bytes\").unwrap();\r\n    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), docs.clone(), \"LOCAL\".to_owned(), 0)\r\n        .expect(\"bind production serving listener\");\r\n    let alias = \"/cli/reference.md\";\r\n    let canonical = \"/local/docs/cli/reference.md\";\r\n    let (_, alias_headers, alias_bytes) = http(port, \"GET\", alias);\r\n    assert_eq!(alias_bytes, b\"# stable docs bytes\");\r\n    assert_eq!(http(port, \"GET\", canonical).2, alias_bytes);\r\n\r\n    let identity = home.path().join(\"identity\");\r\n    let roster_path = identity.join(\"roster.json\");\r","truncated":true,"truncatedBy":"lines","totalLines":455,"totalBytes":7743,"outputLines":148,"outputBytes":7743,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":455,"displayContent":{"text":"//! W0 serving over the production loopback listener, not a test HTTP server.\r\n//! Each fixture supplies its own home, node label and ephemeral port; registry\r\n//! writes model the daemon's persisted mutations without changing process env.\r\n\r\nuse std::io::{Read, Write};\r\nuse std::net::TcpStream;\r\nuse std::path::Path;\r\nuse std::time::Duration;\r\n\r\nuse spt_store::serving::{ServedEntry, ServingRegistry};\r\n\r\nfn http(port: u16, method: &str, path: &str) -> (u16, String, Vec<u8>) {\r\n    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\r\n    stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();\r\n    stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();\r\n    write!(stream, \"{method} {path} HTTP/1.1\\r\\nHost: localhost:{port}\\r\\nConnection: close\\r\\n\\r\\n\").unwrap();\r\n    let mut raw = Vec::new();\r\n    stream.read_to_end(&mut raw).expect(\"read serving response\");\r\n    let split = raw.windows(4).position(|window| window == b\"\\r\\n\\r\\n\").expect(\"HTTP header boundary\");\r\n    let headers = String::from_utf8(raw[..split].to_vec()).unwrap();\r\n    let status = headers.split_whitespace().nth(1).unwrap().parse().unwrap();\r\n    (status, headers, raw[split + 4..].to_vec())\r\n}\r\n\r\nfn add(home: &Path, source: &Path, name: &str) -> ServedEntry {\r\n    let path = spt_store::perch::serving_registry_file_in(home);\r\n    let mut registry = ServingRegistry::load_at(&path).unwrap();\r\n    let entry = registry.add_reference(source, Some(name), None, 1).unwrap();\r\n    registry.save_at(&path).unwrap();\r\n    entry\r\n}\r\n\r\n// [int->REQ-WEB-URL-NODE-PREFIX]\r\n// [int->REQ-WEB-SERVING-REGISTRY]\r\n#[test]\r\nfn production_listener_routes_nodes_docs_and_live_registry_references() {\r\n    let home = tempfile::tempdir().unwrap();\r\n    let docs = home.path().join(\"docs\");\r\n    std::fs::create_dir_all(docs.join(\"cli\")).unwrap();\r\n    std::fs::create_dir_all(docs.join(\"new-section\")).unwrap();\r\n    std::fs::write(docs.join(\"index.html\"), b\"<html>book</html>\").unwrap();\r\n    std::fs::write(docs.join(\"cli/reference.md\"), b\"# reference bytes\").unwrap();\r\n    std::fs::write(docs.join(\"new-section/page.html\"), b\"new section bytes\").unwrap();\r\n    std::fs::write(docs.join(\"llms-full.txt\"), b\"full docs bytes\").unwrap();\r\n    std::fs::write(docs.join(\"manifest.schema.json\"), b\"{\\\"type\\\":\\\"object\\\"}\").unwrap();\r\n    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), docs, \"LOCAL\".to_owned(), 0)\r\n        .expect(\"bind production serving listener\");\r\n\r\n    let (status, headers, _) = http(port, \"GET\", \"/\");\r\n    assert_eq!(status, 302);\r\n    assert!(headers.to_ascii_lowercase().contains(\"location: /local/\\r\\n\"));\r\n    for path in [\"/LoCaL/\", \"/local/?json\"] {\r\n        let (status, headers, _) = http(port, \"GET\", path);\r\n        assert_eq!(status, 302, \"{path}\");\r\n        let location = headers.lines().find_map(|line| {\r\n            let (name, value) = line.split_once(':')?;\r\n            name.eq_ignore_ascii_case(\"location\").then(|| value.trim())\r\n        }).expect(\"node root redirects to docs\");\r\n        assert_eq!(location.split('?').next().unwrap(), \"/local/docs/\");\r\n        assert_eq!(http(port, \"GET\", location).2, b\"<html>book</html>\");\r\n    }\r\n\r\n    // Registration after bind proves that the HTTP reader is not a startup snapshot.\r\n    let source = home.path().join(\"source & quote.txt\");\r\n    std::fs::write(&source, b\"first bytes\").unwrap();\r\n    let entry = add(home.path(), &source, \"report #1.txt\");\r\n    let uri = \"/local/f/report%20%231.txt\";\r\n    let (status, headers, _) = http(port, \"GET\", \"/local/\");\r\n    assert_eq!(status, 302);\r\n    assert!(headers.to_ascii_lowercase().contains(\"location: /local/docs/\"));\r\n    assert_eq!(http(port, \"GET\", uri).2, b\"first bytes\");\r\n    std::fs::write(&source, b\"edited bytes\").unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).2, b\"edited bytes\");\r\n    let (status, headers, bytes) = http(port, \"HEAD\", uri);\r\n    assert_eq!(status, 200);\r\n    assert!(headers.to_ascii_lowercase().contains(\"content-type: text/plain\"));\r\n    assert!(bytes.is_empty());\r\n    let (status, headers, _) = http(port, \"POST\", uri);\r\n    assert_eq!(status, 405);\r\n    assert!(headers.to_ascii_lowercase().contains(\"allow: get, head\"));\r\n\r\n    for (path, expected) in [\r\n        (\"index.html\", b\"<html>book</html>\".as_slice()),\r\n        (\"cli/reference.md\", b\"# reference bytes\".as_slice()),\r\n        (\"new-section/page.html\", b\"new section bytes\".as_slice()),\r\n        (\"llms-full.txt\", b\"full docs bytes\".as_slice()),\r\n        (\"manifest.schema.json\", b\"{\\\"type\\\":\\\"object\\\"}\".as_slice()),\r\n    ] {\r\n        let (bare_status, _, bare_bytes) = http(port, \"GET\", &format!(\"/{path}\"));\r\n        let (prefixed_status, _, prefixed_bytes) = http(port, \"GET\", &format!(\"/local/docs/{path}\"));\r\n        assert_eq!(bare_status, 200, \"bare {path}\");\r\n        assert_eq!(prefixed_status, 200, \"prefixed {path}\");\r\n        assert_eq!(bare_bytes, expected);\r\n        assert_eq!(prefixed_bytes, expected);\r\n    }\r\n    assert_eq!(http(port, \"GET\", \"/local/docs/\").2, b\"<html>book</html>\");\r\n    assert_eq!(http(port, \"GET\", \"/cli/missing.md\").0, 404);\r\n    for facet in [\"m\", \"bin\", \"install\"] {\r\n        let (status, _, bytes) = http(port, \"GET\", &format!(\"/local/{facet}/anything\"));\r\n        assert_eq!(status, 404);\r\n        assert!(String::from_utf8(bytes).unwrap().contains(facet));\r\n    }\r\n    assert_eq!(http(port, \"GET\", \"/local/unknown/resource\").0, 404);\r\n    assert_eq!(http(port, \"GET\", \"/OTHER/f/report.md\").0, 404);\r\n    assert_eq!(http(port, \"GET\", \"/no-such-page.html\").0, 404);\r\n    assert_eq!(http(port, \"GET\", \"/unknown/no-such-page.html\").0, 404);\r\n    for uri in [\"/local/docs/%2e%2e/source.txt\", \"/local/f/report%2fchild\", \"/cli/../../secret\"] {\r\n        assert_eq!(http(port, \"GET\", uri).0, 400, \"{uri}\");\r\n    }\r\n\r\n    std::fs::remove_file(&source).unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).0, 404);\r\n    std::fs::write(&source, b\"recreated\").unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).2, b\"recreated\");\r\n    let path = spt_store::perch::serving_registry_file_in(home.path());\r\n    let mut registry = ServingRegistry::load_at(&path).unwrap();\r\n    registry.remove(&entry.id).unwrap();\r\n    registry.save_at(&path).unwrap();\r\n    assert_eq!(http(port, \"GET\", uri).0, 404);\r\n    std::fs::write(path, b\"invalid registry json\").unwrap();\r\n    // The retired index must not make the docs redirect depend on registry health.\r\n    assert_eq!(http(port, \"GET\", \"/local/?json\").0, 302);\r\n    let (status, _, bytes) = http(port, \"GET\", uri);\r\n    assert_eq!(status, 500);\r\n    assert!(String::from_utf8(bytes).unwrap().contains(\"SERVING_REGISTRY_LOAD_FAIL\"));\r\n}\r\n\r\n// [int->REQ-WEB-URL-NODE-PREFIX]\r\n#[test]\r\nfn subnet_node_labels_shadow_only_the_docs_compat_alias_while_members() {\r\n    use spt_net::net::registry::SubnetRegistry;\r\n    use spt_store::roster::{RosterEntry, RosterStore, Tombstone};\r\n    use spt_store::subnet::SubnetStore;\r\n\r\n    let home = tempfile::tempdir().unwrap();\r\n    let docs = home.path().join(\"docs\");\r\n    std::fs::create_dir_all(docs.join(\"cli\")).unwrap();\r\n    std::fs::write(docs.join(\"cli/reference.md\"), b\"# stable docs bytes\").unwrap();\r\n    let port = spt_daemon::docshost::start_serving(home.path().to_path_buf(), docs.clone(), \"LOCAL\".to_owned(), 0)\r\n        .expect(\"bind production serving listener\");\r\n    let alias = \"/cli/reference.md\";\r\n    let canonical = \"/local/docs/cli/reference.md\";\r\n    let (_, alias_headers, alias_bytes) = http(port, \"GET\", alias);\r\n    assert_eq!(alias_bytes, b\"# stable docs bytes\");\r\n    assert_eq!(http(port, \"GET\", canonical).2, alias_bytes);\r\n\r\n    let identity = home.path().join(\"identity\");\r\n    let roster_path = identity.join(\"roster.json\");\r\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,null,305]},"fileSize":25010,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\tests\\webserve_e2e.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":455,"totalBytes":7743,"outputLines":148,"outputBytes":7743,"shownRange":{"start":1,"end":148},"nextOffset":149}}},"isError":false,"timestamp":1789522950954}}
{"type":"message","id":"17340e76","parentId":"845970b1","timestamp":"2026-09-16T01:42:30.972Z","message":{"role":"toolResult","toolCallId":"call_YhPV3uyItroprRg9SyVxexXf|fc_0cdf94810c888815016aa9f406d85c87d195ec1b94a16a80d3","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-daemon/src/\n### livehost.rs\n 1291|        Err(err) => {\n*1292|            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n 1293|            return; // never reap outside the lock\n 1294|        }\n 1295|    };\n### seedmap.rs\n 181|            // [impl->REQ-WEB-SERVING-REGISTRY]\n*182|            crate::servehost::KIND_SERVE_REQUEST => {\n*183|                write_frame(&mut conn, &crate::servehost::handle(env.payload))?;\n 184|            }\n 185|            KIND_STOP => {\n 186|                // Ack FIRST so the client's bounded read completes, then latch + wake\n### servehost.rs\n 13|\n*14|pub const KIND_SERVE_REQUEST: &str = \"serve_request\";\n 15|pub const KIND_SERVE_RESULT: &str = \"serve_result\";\n 16|\n 17|/// An explicit local registry operation; no HTTP request can mutate exposure.\n...\n 56|    /// The audience is its established target, never a field supplied by the peer.\n*57|    AddInputReference {\n 58|        path: PathBuf,\n 59|        receipt_id: String,\n 60|        audience: String,\n...\n 149|    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n*150|        if matches!(&request, ServeRequest::AddInputReference { .. }) {\n*151|            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n 152|        } else {\n 153|            error\n 154|        }\n...\n 219|        }\n*220|        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\n 221|            // Resolve the submitter's home HERE, never on the receiving node.\n 222|            let source = if let Some(relative) = source.to_str().and_then(|p| p.strip_prefix(\"~/\")) {\n 223|                std::env::home_dir()\n...\n 235|                        registry.save_at(&path)\n*236|                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\n 237|                    }\n 238|                    Ok(ServeResult::Entry { entry })\n 239|                }\n...\n 431|                &Envelope::new(\n*432|                    KIND_SERVE_REQUEST,\n 433|                    serde_json::to_value(request).expect(\"serving request serializes\"),\n 434|                ),\n 435|            )?;\n...\n 468|    fn input_reference(path: &Path, receipt: &str, audience: &str, received_at_ms: u64) -> ServeRequest {\n*469|        ServeRequest::AddInputReference {\n 470|            path: path.to_owned(),\n 471|            receipt_id: receipt.into(),\n 472|            audience: audience.into(),\n\n## spt/src/\n### rc.rs\n 3121|        } else {\n*3122|            match call_with_timeout(&socket, ServeRequest::AddInputReference {\n 3123|                path: std::path::PathBuf::from(&path),\n 3124|                receipt_id: request.receipt_id.clone(),\n 3125|                audience: audience.to_owned(),"}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates/spt-daemon/src, .worktrees/308-registry-process-lock/crates/spt/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":12,"fileCount":4,"files":[".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/seedmap.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs",".worktrees/308-registry-process-lock/crates/spt/src/rc.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/seedmap.rs","count":2},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs","count":8},{"path":".worktrees/308-registry-process-lock/crates/spt/src/rc.rs","count":1}],"truncated":false,"displayContent":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-daemon/src/\n### livehost.rs\n 1291│        Err(err) => {\n*1292│            eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n 1293│            return; // never reap outside the lock\n 1294│        }\n 1295│    };\n### seedmap.rs\n 181│            // [impl->REQ-WEB-SERVING-REGISTRY]\n*182│            crate::servehost::KIND_SERVE_REQUEST => {\n*183│                write_frame(&mut conn, &crate::servehost::handle(env.payload))?;\n 184│            }\n 185│            KIND_STOP => {\n 186│                // Ack FIRST so the client's bounded read completes, then latch + wake\n### servehost.rs\n  13│\n *14│pub const KIND_SERVE_REQUEST: &str = \"serve_request\";\n  15│pub const KIND_SERVE_RESULT: &str = \"serve_result\";\n  16│\n  17│/// An explicit local registry operation; no HTTP request can mutate exposure.\n    │...\n  56│    /// The audience is its established target, never a field supplied by the peer.\n *57│    AddInputReference {\n  58│        path: PathBuf,\n  59│        receipt_id: String,\n  60│        audience: String,\n    │...\n 149│    let mut registry = ServingRegistry::load_at(&path).map_err(|error| {\n*150│        if matches!(&request, ServeRequest::AddInputReference { .. }) {\n*151│            io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\"))\n 152│        } else {\n 153│            error\n 154│        }\n    │...\n 219│        }\n*220│        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\n 221│            // Resolve the submitter's home HERE, never on the receiving node.\n 222│            let source = if let Some(relative) = source.to_str().and_then(|p| p.strip_prefix(\"~/\")) {\n 223│                std::env::home_dir()\n    │...\n 235│                        registry.save_at(&path)\n*236│                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\n 237│                    }\n 238│                    Ok(ServeResult::Entry { entry })\n 239│                }\n    │...\n 431│                &Envelope::new(\n*432│                    KIND_SERVE_REQUEST,\n 433│                    serde_json::to_value(request).expect(\"serving request serializes\"),\n 434│                ),\n 435│            )?;\n    │...\n 468│    fn input_reference(path: &Path, receipt: &str, audience: &str, received_at_ms: u64) -> ServeRequest {\n*469│        ServeRequest::AddInputReference {\n 470│            path: path.to_owned(),\n 471│            receipt_id: receipt.into(),\n 472│            audience: audience.into(),\n\n## spt/src/\n### rc.rs\n 3121│        } else {\n*3122│            match call_with_timeout(&socket, ServeRequest::AddInputReference {\n 3123│                path: std::path::PathBuf::from(&path),\n 3124│                receipt_id: request.receipt_id.clone(),\n 3125│                audience: audience.to_owned(),"},"isError":false,"timestamp":1789522950972}}
{"type":"message","id":"c8a1d44d","parentId":"17340e76","timestamp":"2026-09-16T01:42:30.976Z","message":{"role":"toolResult","toolCallId":"call_iRGXhgH7973VGqD3f9QqAv7w|fc_0cdf94810c888815016aa9f406d86c87d19cf735ce18b3a9c8","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-daemon/\n\n### src/\n#### forkop.rs\n 97|    // name. Checked before the mind copy so a colliding request costs nothing.\n*98|    let regs = crate::presence::load_registry_snapshots(&perch::identity_dir().join(\"registry\"));\n 99|    if let Some(reg) = regs.get(subnet) {\n 100|        let holders = reg.instances(new_id);\n 101|        if !holders.is_empty() {\n#### pairhost.rs\n 110|                    intro,\n*111|                    &spt_store::perch::identity_dir().join(\"registry\"),\n 112|                );\n 113|            }\n 114|            // Grow the seed-holder's roster by the new member (REQ-MESH-2): the\n#### presence.rs\n 83|pub fn registry_snapshot_dir() -> std::path::PathBuf {\n*84|    spt_store::perch::identity_dir().join(\"registry\")\n 85|}\n 86|\n 87|/// This node's own pubkey hex — non-minting (the [`crate::access`] posture):\n...\n 400|        assert!(load_registry_snapshots(&dir.path().join(\"missing\")).is_empty());\n*401|        let d = dir.path().join(\"registry\");\n 402|        std::fs::create_dir_all(&d).unwrap();\n 403|        std::fs::write(d.join(\"junk.json\"), \"not json\").unwrap();\n 404|        std::fs::write(d.join(\"note.txt\"), \"{}\").unwrap();\n#### registryhost.rs\n 173|            EpochSource::load(),\n*174|            perch::identity_dir().join(\"registry\"),\n 175|        )\n 176|        .with_node_label(spt_store::hostlabel::os_hostname())\n 177|        .with_machine_id(crate::machineid::machine_id_hash())\n...\n 1572|            EpochSource::load_from(&dir.join(\"epoch.json\")),\n*1573|            dir.join(\"registry\"),\n 1574|        )\n 1575|    }\n 1576|    // [unit->REQ-REGISTRY-SNAPSHOT-HYDRATE] #281 face 2, the titled defect: a\n...\n 1657|        let dir = tempfile::tempdir().unwrap();\n*1658|        let snap_dir = dir.path().join(\"registry\");\n 1659|        let h1 = host(dir.path());\n 1660|        h1.apply_feed(\n 1661|            \"bb22\",\n...\n 1697|        let torn = dir.path().join(\"torn\");\n*1698|        std::fs::create_dir_all(torn.join(\"registry\")).unwrap();\n*1699|        std::fs::write(torn.join(\"registry\").join(\"home.json\"), b\"{not json\").unwrap();\n 1700|        let h = host(&torn);\n 1701|        assert!(\n 1702|            h.rows(\"home\", \"ling\").is_empty(),\n...\n 1773|            EpochSource::load_from(&epoch_path),\n*1774|            dir.path().join(\"registry\"),\n 1775|        );\n 1776|        // Registry leg advances the canonical counter (stand-in for\n 1777|        // advertise_local stamping rows).\n...\n 1847|        let dir = tempfile::tempdir().unwrap();\n*1848|        let snap_dir = dir.path().join(\"registry\");\n 1849|        let h = RegistryHost::new_at(\n 1850|            \"self\",\n 1851|            EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n...\n 2304|        // The label landed and is the only carrier — no endpoint row created.\n*2305|        let snap = RegistryHost::snapshot_path(&dir.path().join(\"registry\"), \"home\");\n 2306|        let reg: SubnetRegistry =\n 2307|            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2308|        assert_eq!(reg.node_labels().collect::<Vec<_>>(), vec![(\"bb22\", \"RENAMED\")]);\n...\n 2381|            serde_json::from_slice(\n*2382|                &std::fs::read(RegistryHost::snapshot_path(&d.join(\"registry\"), \"home\")).unwrap(),\n 2383|            )\n 2384|            .unwrap()\n 2385|        };\n...\n 2692|            // The snapshot mirror exists and parses back.\n*2693|            let snap = RegistryHost::snapshot_path(&home.join(\"registry\"), \"adv\");\n 2694|            let bytes = std::fs::read(&snap).expect(\"snapshot written\");\n 2695|            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect(\"parses\");\n 2696|            assert!(!parsed.instances(\"vis-ag\").is_empty());\n...\n 2711|        h.apply_feed(\"cc33\", &[upd(\"home\", \"ling\", \"cc33\", 5)], &p);\n*2712|        assert!(!RegistryHost::heard_path(&dir.path().join(\"registry\")).exists());\n 2713|\n 2714|        // Admitted: stamped + mirrored.\n 2715|        h.apply_feed(\"bb22\", &[upd(\"home\", \"ling\", \"bb22\", 5)], &p);\n*2716|        let heard_file = RegistryHost::heard_path(&dir.path().join(\"registry\"));\n 2717|        let heard: BTreeMap<String, u64> =\n 2718|            serde_json::from_slice(&std::fs::read(&heard_file).expect(\"mirror written\")).unwrap();\n 2719|        let first = *heard.get(\"bb22\").expect(\"origin stamped\");\n...\n 2752|            let heard: BTreeMap<String, u64> = serde_json::from_slice(\n*2753|                &std::fs::read(RegistryHost::heard_path(&dir.path().join(\"registry\"))).unwrap(),\n 2754|            )\n 2755|            .unwrap();\n 2756|            *heard.get(\"bb22\").unwrap()\n...\n 2780|        assert!(h.rows(\"home\", \"sergey\").is_empty(), \"ghost row evicted\");\n*2781|        let snap = RegistryHost::snapshot_path(&dir.path().join(\"registry\"), \"home\");\n 2782|        let parsed: SubnetRegistry =\n 2783|            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2784|        assert!(\n...\n 2829|        assert!(h.rows(\"home\", \"ghost\").is_empty(), \"aged Offline ghost evicted\");\n*2830|        let snap = RegistryHost::snapshot_path(&dir.path().join(\"registry\"), \"home\");\n 2831|        let parsed: SubnetRegistry =\n 2832|            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2833|        assert!(\n#### webserve.rs\n 304|    subnets.subnets.iter().find_map(|subnet| {\n*305|        let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join(\"registry\"), &subnet.name);\n 306|        let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path)\n 307|            .ok()\n 308|            .and_then(|bytes| serde_json::from_slice(&bytes).ok())\n...\n 686|    subnets.subnets.iter().find_map(|subnet| {\n*687|        let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join(\"registry\"), &subnet.name);\n 688|        let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path)\n 689|            .ok()\n 690|            .and_then(|bytes| serde_json::from_slice(&bytes).ok())\n\n#### pump/\n##### registry.rs\n 497|            EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n*498|            dir.path().join(\"registry\"),\n 499|        );\n 500|        fire_due_rotations(\n 501|            &subnets_path,\n...\n 554|                EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n*555|                dir.path().join(\"registry\"),\n 556|            )),\n 557|            owlery: dir.path().join(\"owlery\"),\n 558|            subnets: dir.path().join(\"subnet.json\"),\n...\n 615|                    EpochSource::load_from(&home.join(\"epoch.json\")),\n*616|                    home.join(\"registry\"),\n 617|                )),\n 618|                owlery: owlery.clone(),\n 619|                subnets: subnets_path.clone(),\n\n### tests/\n#### mesh.rs\n 219|            EpochSource::load_from(&root.join(\"epoch.json\")),\n*220|            root.join(\"registry\"),\n 221|        ));\n 222|        let dispatch = DispatchPaths {\n 223|            owlery,\n#### mesh_recovery.rs\n 211|        EpochSource::load_from(&root.join(\"epoch.json\")),\n*212|        root.join(\"registry\"),\n 213|    ));\n 214|    let name = broker.to_string();\n 215|    let flag = Arc::clone(&stop);\n#### pump.rs\n 345|        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*346|        a_root.join(\"registry\"),\n 347|    ));\n 348|    // A learned of B's endpoint via an earlier inbound feed (seeded directly\n 349|    // here — B runs no pump in this test): the bootstrap-ref source.\n...\n 581|        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*582|        a_root.join(\"registry\"),\n 583|    ));\n 584|\n 585|    let resolver: PeerResolver = {\n...\n 775|        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*776|        a_root.join(\"registry\"),\n 777|    ));\n 778|\n 779|    let resolver: PeerResolver = {\n#### registry_lifecycle.rs\n 334|        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*335|        a_root.join(\"registry\"),\n 336|    ));\n 337|    {\n 338|        // A learned of B's mind via an earlier inbound feed (seeded directly —\n#### sync.rs\n 209|            policy,\n*210|            &self.scratch.join(\"serve\"),\n 211|        )\n 212|        .expect(\"serve\");\n 213|        (puller.join().expect(\"pull thread\"), outcome)\n#### webserve_e2e.rs\n 189|    registry.merge_node_label(\"bb22\", \"CLI\".to_owned(), 2);\n*190|    let registry_dir = identity.join(\"registry\");\n 191|    std::fs::create_dir_all(&registry_dir).unwrap();\n 192|    let registry_path = spt_daemon::registryhost::RegistryHost::snapshot_path(&registry_dir, \"home\");\n 193|    std::fs::write(&registry_path, serde_json::to_vec(&registry).unwrap()).unwrap();\n\n## spt-store/\n\n### src/\n#### perch.rs\n 118|pub fn serving_registry_file_in(home: &Path) -> PathBuf {\n*119|    home.join(\"serve\").join(\"registry\")\n 120|}\n 121|\n*122|/// `$SPT_HOME/serve/registry`.\n 123|// [impl->REQ-WEB-SERVING-REGISTRY]\n 124|pub fn serving_registry_file() -> PathBuf {\n 125|    serving_registry_file_in(&spt_home())\n...\n 132|pub fn serve_snapshots_dir_in(home: &Path) -> PathBuf {\n*133|    home.join(\"serve\").join(\"snapshots\")\n 134|}\n 135|\n 136|/// `$SPT_HOME/serve/snapshots`.\n\n### tests/\n#### serving_registry_two_process_int.rs\n 105|    // sentinel must append .lock, not replace an existing extension.\n*106|    let registry_path = home.join(\"serve\").join(if holder_mode == \"reap\" { \"registry\" } else { \"registry.json\" });\n 107|    let expired_path = home.join(\"expired.snapshot\");\n 108|    let retired_path = home.join(\"retired.txt\");\n 109|    let registered_path = home.join(\"new.txt\");\n\n## spt/src/\n### cli.rs\n 6467|        let read_wait = RosterWait::start(\"roster.read_model\", \"none\");\n*6468|        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 6469|        let visibility_wait = RosterWait::start(\"roster.visibility\", \"none\");\n 6470|        let vis = VisibilityStore::load();\n 6471|        visibility_wait.finish(format_args!(\"outcome=returned\"));\n...\n 6741|\n*6742|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 6743|    let visibility_wait = RosterWait::start(\"roster.visibility\", \"none\");\n 6744|    let vis = VisibilityStore::load();\n 6745|    visibility_wait.finish(format_args!(\"outcome=returned\"));\n...\n 7691|        || {\n*7692|            crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"))\n 7693|                .values()\n 7694|                .flat_map(|reg| reg.instances(id).iter().map(|i| (i.node.clone(), i.status)))\n 7695|                .collect()\n...\n 7727|            // satisfying ones.\n*7728|            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 7729|            let cand_nodes: Vec<String> = regs\n 7730|                .values()\n 7731|                .flat_map(|reg| reg.instances(id).iter().map(|i| i.node.clone()))\n...\n 7742|        spt_daemon::RestRoute::Ambiguous { nodes } => {\n*7743|            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 7744|            eprintln!(\n 7745|                \"{}\",\n 7746|                crate::wansend::render_refusal(\n...\n 8822|        if !store.subnets.is_empty() {\n*8823|            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 8824|            let roster = spt_store::roster::RosterStore::load();\n 8825|            let own_hex = spt_store::nodeid::load_or_create()\n 8826|                .map(|i| i.public_key().to_hex())\n...\n 8984|    } else {\n*8985|        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 8986|        let roster = spt_store::roster::RosterStore::load();\n 8987|        let own_hex = spt_store::nodeid::load_or_create()\n 8988|            .map(|i| i.public_key().to_hex())\n...\n 14655|    };\n*14656|    let snap_dir = perch::identity_dir().join(\"registry\");\n 14657|    let regs = crate::wansend::load_snapshots(&snap_dir);\n 14658|    let roster = spt_store::roster::RosterStore::load();\n 14659|    let own_hex = spt_store::nodeid::load_or_create()\n...\n 14962|    let snap = perch::identity_dir()\n*14963|        .join(\"registry\")\n 14964|        .join(format!(\"{name}.json\"));\n 14965|    let _ = std::fs::remove_file(snap);\n 14966|    eprintln!(\n...\n 15055|    // so clear them now (every snapshot — the identity is dead everywhere).\n*15056|    let snap_dir = perch::identity_dir().join(\"registry\");\n 15057|    let mut evicted = 0usize;\n 15058|    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {\n 15059|        // Prune is an EXPLICIT forget: drop the dead identity's routable rows\n...\n 15241|    // explicit forget `prune` performs), so bare-id resolution never strands.\n*15242|    let snap_dir = perch::identity_dir().join(\"registry\");\n 15243|    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {\n 15244|        let mut n = 0usize;\n 15245|        for hex in &revokees {\n...\n 17000|    let subnets = spt_store::subnet::SubnetStore::load();\n*17001|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 17002|    for s in &subnets.subnets {\n 17003|        let Some(reg) = regs.get(&s.name) else {\n 17004|            continue;\n...\n 20521|    let subnets = spt_store::subnet::SubnetStore::load();\n*20522|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 20523|    for s in &subnets.subnets {\n 20524|        let Some(reg) = regs.get(&s.name) else {\n 20525|            continue;\n...\n 34080|        );\n*34081|        let dir = perch::identity_dir().join(\"registry\");\n 34082|        std::fs::create_dir_all(&dir).unwrap();\n 34083|        std::fs::write(\n 34084|            dir.join(format!(\"{subnet}.json\")),\n...\n 34222|        // Target-registry collision: ANY holder (even a resting one) owns it.\n*34223|        let snap_dir = perch::identity_dir().join(\"registry\");\n 34224|        std::fs::create_dir_all(&snap_dir).unwrap();\n 34225|        let mut reg = spt_net::net::registry::SubnetRegistry::new();\n 34226|        reg.merge_instance(\n### rc.rs\n 5222|        reg.merge_instance(\"l1\", instance(&own_hex));\n*5223|        let reg_dir = spt_store::perch::identity_dir().join(\"registry\");\n 5224|        std::fs::create_dir_all(&reg_dir).unwrap();\n 5225|        std::fs::write(reg_dir.join(\"s1.json\"), serde_json::to_string(&reg).unwrap()).unwrap();\n 5226|        {\n### sealverb.rs\n 225|    };\n*226|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 227|    let minter_subnets = endpoint_subnets(&regs, &minter);\n 228|    let binding = match resolve_binding_subnet(\n 229|        subnet.as_deref(),\n...\n 331|    };\n*332|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 333|    let member_subnets = endpoint_subnets(&regs, &endpoint);\n 334|    let binding = match resolve_binding_subnet(\n 335|        subnet.as_deref(),\n...\n 441|        .unwrap_or_else(|_| target.to_string());\n*442|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 443|    let minter_subnets = endpoint_subnets(&regs, minter);\n 444|    let dest_subnets = endpoint_subnets(&regs, &dest_id);\n 445|    let binding = resolve_binding_subnet(\n### wansend.rs\n 293|    };\n*294|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 295|    let excl = Exclusions::load();\n 296|    let excluded = excl.by_subnet();\n 297|\n...\n 342|    let address = Address::parse(endpoint).ok()?;\n*343|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 344|    let excl = Exclusions::load();\n 345|    let excluded = excl.by_subnet();\n 346|    match resolve_across_visible(&regs, &address, &own_hex, excluded) {\n...\n 370|    };\n*371|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 372|    if spt_daemon::daemon::ensure_running().is_err() {\n 373|        return WanSendOutcome::Failed(\"daemon did not come up\".to_string());\n 374|    }\n...\n 586|    };\n*587|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 588|\n 589|    let excl = Exclusions::load();\n 590|    let excluded = excl.by_subnet();\n...\n 815|    };\n*816|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 817|\n 818|    let excl = Exclusions::load();\n 819|    let excluded = excl.by_subnet();\n...\n 992|    };\n*993|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 994|    if spt_daemon::daemon::ensure_running().is_err() {\n 995|        return WanRestOutcome::Failed(\"daemon did not come up\".to_string());\n 996|    }\n...\n 1129|    };\n*1130|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1131|    if spt_daemon::daemon::ensure_running().is_err() {\n 1132|        return WanShellLinkOutcome::Failed(\"daemon did not come up\".to_string());\n 1133|    }\n...\n 1265|    };\n*1266|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1267|    if spt_daemon::daemon::ensure_running().is_err() {\n 1268|        return WanDigestOutcome::Failed(\"daemon did not come up\".to_string());\n 1269|    }\n...\n 1749|\n*1750|    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1751|    let pairs: Vec<(String, String)> = openings\n 1752|        .iter()\n 1753|        .map(|(subnet, sealed)| (subnet.to_string(), sealed.node_short_hex()))\n...\n 3160|        assert!(load_snapshots(&dir.path().join(\"nope\")).is_empty());\n*3161|        let d = dir.path().join(\"registry\");\n 3162|        std::fs::create_dir_all(&d).unwrap();\n 3163|        std::fs::write(\n 3164|            d.join(\"good.json\"),\n...\n 3178|    fn write_snapshot(subnet: &str, id: &str, node: &str) {\n*3179|        let dir = perch::identity_dir().join(\"registry\");\n 3180|        std::fs::create_dir_all(&dir).unwrap();\n 3181|        let map = reg_with(subnet, id, node);\n 3182|        std::fs::write(\n\n### api/\n#### reporting.rs\n 1302|pub(crate) fn registry_node_label_map() -> std::collections::BTreeMap<String, String> {\n*1303|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1304|    let mut out = std::collections::BTreeMap::new();\n 1305|    for reg in regs.values() {\n 1306|        for (k, l) in reg.node_labels() {\n\n### picker/\n#### data.rs\n 123|    let subnets = spt_store::subnet::SubnetStore::load();\n*124|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 125|    for s in &subnets.subnets {\n 126|        if let Some(reg) = regs.get(&s.name) {\n 127|            for (node, label) in reg.node_labels() {\n...\n 313|    let subnets = spt_store::subnet::SubnetStore::load();\n*314|    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 315|    // [impl->REQ-VIS-REMOTE-NOT-HIDE-NEW] \"visibility-gated exactly like `spt\n 316|    // endpoint list`\" is now literal: the same seam, node-wide scope, ownership\n 317|    // from the local roster — so a hide_new node's picker stops offering an empty\n...\n 798|\n*799|        let snap_dir = perch::identity_dir().join(\"registry\");\n 800|        std::fs::create_dir_all(&snap_dir).unwrap();\n 801|        std::fs::write(\n 802|            snap_dir.join(\"home.json\"),\n...\n 900|\n*901|        let snap_dir = perch::identity_dir().join(\"registry\");\n 902|        std::fs::create_dir_all(&snap_dir).unwrap();\n 903|        std::fs::write(snap_dir.join(\"home.json\"), serde_json::to_string(&reg).unwrap()).unwrap();\n 904|\n...\n 977|\n*978|        let snap_dir = perch::identity_dir().join(\"registry\");\n 979|        std::fs::create_dir_all(&snap_dir).unwrap();\n 980|        std::fs::write(snap_dir.join(\"bignet.json\"), serde_json::to_string(&reg_big).unwrap())\n 981|            .unwrap();\n\nShowing files 1-20 of 27. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":72,"fileCount":20,"files":[".worktrees/308-registry-process-lock/crates/spt-daemon/src/forkop.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/pairhost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/presence.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/pump/registry.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/registryhost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/webserve.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/mesh.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/mesh_recovery.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/pump.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/registry_lifecycle.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/sync.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs",".worktrees/308-registry-process-lock/crates/spt-store/src/perch.rs",".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs",".worktrees/308-registry-process-lock/crates/spt/src/api/reporting.rs",".worktrees/308-registry-process-lock/crates/spt/src/cli.rs",".worktrees/308-registry-process-lock/crates/spt/src/picker/data.rs",".worktrees/308-registry-process-lock/crates/spt/src/rc.rs",".worktrees/308-registry-process-lock/crates/spt/src/sealverb.rs",".worktrees/308-registry-process-lock/crates/spt/src/wansend.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/forkop.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/pairhost.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/presence.rs","count":2},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/pump/registry.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/registryhost.rs","count":15},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/webserve.rs","count":2},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/mesh.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/mesh_recovery.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/pump.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/registry_lifecycle.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/sync.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/perch.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/src/api/reporting.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/src/cli.rs","count":15},{"path":".worktrees/308-registry-process-lock/crates/spt/src/picker/data.rs","count":5},{"path":".worktrees/308-registry-process-lock/crates/spt/src/rc.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/src/sealverb.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt/src/wansend.rs","count":11}],"truncated":true,"fileLimitReached":20,"displayContent":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-daemon/\n\n### src/\n#### forkop.rs\n  97│    // name. Checked before the mind copy so a colliding request costs nothing.\n *98│    let regs = crate::presence::load_registry_snapshots(&perch::identity_dir().join(\"registry\"));\n  99│    if let Some(reg) = regs.get(subnet) {\n 100│        let holders = reg.instances(new_id);\n 101│        if !holders.is_empty() {\n#### pairhost.rs\n 110│                    intro,\n*111│                    &spt_store::perch::identity_dir().join(\"registry\"),\n 112│                );\n 113│            }\n 114│            // Grow the seed-holder's roster by the new member (REQ-MESH-2): the\n#### presence.rs\n  83│pub fn registry_snapshot_dir() -> std::path::PathBuf {\n *84│    spt_store::perch::identity_dir().join(\"registry\")\n  85│}\n  86│\n  87│/// This node's own pubkey hex — non-minting (the [`crate::access`] posture):\n    │...\n 400│        assert!(load_registry_snapshots(&dir.path().join(\"missing\")).is_empty());\n*401│        let d = dir.path().join(\"registry\");\n 402│        std::fs::create_dir_all(&d).unwrap();\n 403│        std::fs::write(d.join(\"junk.json\"), \"not json\").unwrap();\n 404│        std::fs::write(d.join(\"note.txt\"), \"{}\").unwrap();\n#### registryhost.rs\n  173│            EpochSource::load(),\n *174│            perch::identity_dir().join(\"registry\"),\n  175│        )\n  176│        .with_node_label(spt_store::hostlabel::os_hostname())\n  177│        .with_machine_id(crate::machineid::machine_id_hash())\n     │...\n 1572│            EpochSource::load_from(&dir.join(\"epoch.json\")),\n*1573│            dir.join(\"registry\"),\n 1574│        )\n 1575│    }\n 1576│    // [unit->REQ-REGISTRY-SNAPSHOT-HYDRATE] #281 face 2, the titled defect: a\n     │...\n 1657│        let dir = tempfile::tempdir().unwrap();\n*1658│        let snap_dir = dir.path().join(\"registry\");\n 1659│        let h1 = host(dir.path());\n 1660│        h1.apply_feed(\n 1661│            \"bb22\",\n     │...\n 1697│        let torn = dir.path().join(\"torn\");\n*1698│        std::fs::create_dir_all(torn.join(\"registry\")).unwrap();\n*1699│        std::fs::write(torn.join(\"registry\").join(\"home.json\"), b\"{not json\").unwrap();\n 1700│        let h = host(&torn);\n 1701│        assert!(\n 1702│            h.rows(\"home\", \"ling\").is_empty(),\n     │...\n 1773│            EpochSource::load_from(&epoch_path),\n*1774│            dir.path().join(\"registry\"),\n 1775│        );\n 1776│        // Registry leg advances the canonical counter (stand-in for\n 1777│        // advertise_local stamping rows).\n     │...\n 1847│        let dir = tempfile::tempdir().unwrap();\n*1848│        let snap_dir = dir.path().join(\"registry\");\n 1849│        let h = RegistryHost::new_at(\n 1850│            \"self\",\n 1851│            EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n     │...\n 2304│        // The label landed and is the only carrier — no endpoint row created.\n*2305│        let snap = RegistryHost::snapshot_path(&dir.path().join(\"registry\"), \"home\");\n 2306│        let reg: SubnetRegistry =\n 2307│            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2308│        assert_eq!(reg.node_labels().collect::<Vec<_>>(), vec![(\"bb22\", \"RENAMED\")]);\n     │...\n 2381│            serde_json::from_slice(\n*2382│                &std::fs::read(RegistryHost::snapshot_path(&d.join(\"registry\"), \"home\")).unwrap(),\n 2383│            )\n 2384│            .unwrap()\n 2385│        };\n     │...\n 2692│            // The snapshot mirror exists and parses back.\n*2693│            let snap = RegistryHost::snapshot_path(&home.join(\"registry\"), \"adv\");\n 2694│            let bytes = std::fs::read(&snap).expect(\"snapshot written\");\n 2695│            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect(\"parses\");\n 2696│            assert!(!parsed.instances(\"vis-ag\").is_empty());\n     │...\n 2711│        h.apply_feed(\"cc33\", &[upd(\"home\", \"ling\", \"cc33\", 5)], &p);\n*2712│        assert!(!RegistryHost::heard_path(&dir.path().join(\"registry\")).exists());\n 2713│\n 2714│        // Admitted: stamped + mirrored.\n 2715│        h.apply_feed(\"bb22\", &[upd(\"home\", \"ling\", \"bb22\", 5)], &p);\n*2716│        let heard_file = RegistryHost::heard_path(&dir.path().join(\"registry\"));\n 2717│        let heard: BTreeMap<String, u64> =\n 2718│            serde_json::from_slice(&std::fs::read(&heard_file).expect(\"mirror written\")).unwrap();\n 2719│        let first = *heard.get(\"bb22\").expect(\"origin stamped\");\n     │...\n 2752│            let heard: BTreeMap<String, u64> = serde_json::from_slice(\n*2753│                &std::fs::read(RegistryHost::heard_path(&dir.path().join(\"registry\"))).unwrap(),\n 2754│            )\n 2755│            .unwrap();\n 2756│            *heard.get(\"bb22\").unwrap()\n     │...\n 2780│        assert!(h.rows(\"home\", \"sergey\").is_empty(), \"ghost row evicted\");\n*2781│        let snap = RegistryHost::snapshot_path(&dir.path().join(\"registry\"), \"home\");\n 2782│        let parsed: SubnetRegistry =\n 2783│            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2784│        assert!(\n     │...\n 2829│        assert!(h.rows(\"home\", \"ghost\").is_empty(), \"aged Offline ghost evicted\");\n*2830│        let snap = RegistryHost::snapshot_path(&dir.path().join(\"registry\"), \"home\");\n 2831│        let parsed: SubnetRegistry =\n 2832│            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();\n 2833│        assert!(\n#### webserve.rs\n 304│    subnets.subnets.iter().find_map(|subnet| {\n*305│        let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join(\"registry\"), &subnet.name);\n 306│        let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path)\n 307│            .ok()\n 308│            .and_then(|bytes| serde_json::from_slice(&bytes).ok())\n    │...\n 686│    subnets.subnets.iter().find_map(|subnet| {\n*687│        let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join(\"registry\"), &subnet.name);\n 688│        let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path)\n 689│            .ok()\n 690│            .and_then(|bytes| serde_json::from_slice(&bytes).ok())\n\n#### pump/\n##### registry.rs\n 497│            EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n*498│            dir.path().join(\"registry\"),\n 499│        );\n 500│        fire_due_rotations(\n 501│            &subnets_path,\n    │...\n 554│                EpochSource::load_from(&dir.path().join(\"epoch.json\")),\n*555│                dir.path().join(\"registry\"),\n 556│            )),\n 557│            owlery: dir.path().join(\"owlery\"),\n 558│            subnets: dir.path().join(\"subnet.json\"),\n    │...\n 615│                    EpochSource::load_from(&home.join(\"epoch.json\")),\n*616│                    home.join(\"registry\"),\n 617│                )),\n 618│                owlery: owlery.clone(),\n 619│                subnets: subnets_path.clone(),\n\n### tests/\n#### mesh.rs\n 219│            EpochSource::load_from(&root.join(\"epoch.json\")),\n*220│            root.join(\"registry\"),\n 221│        ));\n 222│        let dispatch = DispatchPaths {\n 223│            owlery,\n#### mesh_recovery.rs\n 211│        EpochSource::load_from(&root.join(\"epoch.json\")),\n*212│        root.join(\"registry\"),\n 213│    ));\n 214│    let name = broker.to_string();\n 215│    let flag = Arc::clone(&stop);\n#### pump.rs\n 345│        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*346│        a_root.join(\"registry\"),\n 347│    ));\n 348│    // A learned of B's endpoint via an earlier inbound feed (seeded directly\n 349│    // here — B runs no pump in this test): the bootstrap-ref source.\n    │...\n 581│        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*582│        a_root.join(\"registry\"),\n 583│    ));\n 584│\n 585│    let resolver: PeerResolver = {\n    │...\n 775│        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*776│        a_root.join(\"registry\"),\n 777│    ));\n 778│\n 779│    let resolver: PeerResolver = {\n#### registry_lifecycle.rs\n 334│        EpochSource::load_from(&a_root.join(\"epoch.json\")),\n*335│        a_root.join(\"registry\"),\n 336│    ));\n 337│    {\n 338│        // A learned of B's mind via an earlier inbound feed (seeded directly —\n#### sync.rs\n 209│            policy,\n*210│            &self.scratch.join(\"serve\"),\n 211│        )\n 212│        .expect(\"serve\");\n 213│        (puller.join().expect(\"pull thread\"), outcome)\n#### webserve_e2e.rs\n 189│    registry.merge_node_label(\"bb22\", \"CLI\".to_owned(), 2);\n*190│    let registry_dir = identity.join(\"registry\");\n 191│    std::fs::create_dir_all(&registry_dir).unwrap();\n 192│    let registry_path = spt_daemon::registryhost::RegistryHost::snapshot_path(&registry_dir, \"home\");\n 193│    std::fs::write(&registry_path, serde_json::to_vec(&registry).unwrap()).unwrap();\n\n## spt-store/\n\n### src/\n#### perch.rs\n 118│pub fn serving_registry_file_in(home: &Path) -> PathBuf {\n*119│    home.join(\"serve\").join(\"registry\")\n 120│}\n 121│\n*122│/// `$SPT_HOME/serve/registry`.\n 123│// [impl->REQ-WEB-SERVING-REGISTRY]\n 124│pub fn serving_registry_file() -> PathBuf {\n 125│    serving_registry_file_in(&spt_home())\n    │...\n 132│pub fn serve_snapshots_dir_in(home: &Path) -> PathBuf {\n*133│    home.join(\"serve\").join(\"snapshots\")\n 134│}\n 135│\n 136│/// `$SPT_HOME/serve/snapshots`.\n\n### tests/\n#### serving_registry_two_process_int.rs\n 105│    // sentinel must append .lock, not replace an existing extension.\n*106│    let registry_path = home.join(\"serve\").join(if holder_mode == \"reap\" { \"registry\" } else { \"registry.json\" });\n 107│    let expired_path = home.join(\"expired.snapshot\");\n 108│    let retired_path = home.join(\"retired.txt\");\n 109│    let registered_path = home.join(\"new.txt\");\n\n## spt/src/\n### cli.rs\n  6467│        let read_wait = RosterWait::start(\"roster.read_model\", \"none\");\n *6468│        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n  6469│        let visibility_wait = RosterWait::start(\"roster.visibility\", \"none\");\n  6470│        let vis = VisibilityStore::load();\n  6471│        visibility_wait.finish(format_args!(\"outcome=returned\"));\n      │...\n  6741│\n *6742│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n  6743│    let visibility_wait = RosterWait::start(\"roster.visibility\", \"none\");\n  6744│    let vis = VisibilityStore::load();\n  6745│    visibility_wait.finish(format_args!(\"outcome=returned\"));\n      │...\n  7691│        || {\n *7692│            crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"))\n  7693│                .values()\n  7694│                .flat_map(|reg| reg.instances(id).iter().map(|i| (i.node.clone(), i.status)))\n  7695│                .collect()\n      │...\n  7727│            // satisfying ones.\n *7728│            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n  7729│            let cand_nodes: Vec<String> = regs\n  7730│                .values()\n  7731│                .flat_map(|reg| reg.instances(id).iter().map(|i| i.node.clone()))\n      │...\n  7742│        spt_daemon::RestRoute::Ambiguous { nodes } => {\n *7743│            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n  7744│            eprintln!(\n  7745│                \"{}\",\n  7746│                crate::wansend::render_refusal(\n      │...\n  8822│        if !store.subnets.is_empty() {\n *8823│            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n  8824│            let roster = spt_store::roster::RosterStore::load();\n  8825│            let own_hex = spt_store::nodeid::load_or_create()\n  8826│                .map(|i| i.public_key().to_hex())\n      │...\n  8984│    } else {\n *8985│        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n  8986│        let roster = spt_store::roster::RosterStore::load();\n  8987│        let own_hex = spt_store::nodeid::load_or_create()\n  8988│            .map(|i| i.public_key().to_hex())\n      │...\n 14655│    };\n*14656│    let snap_dir = perch::identity_dir().join(\"registry\");\n 14657│    let regs = crate::wansend::load_snapshots(&snap_dir);\n 14658│    let roster = spt_store::roster::RosterStore::load();\n 14659│    let own_hex = spt_store::nodeid::load_or_create()\n      │...\n 14962│    let snap = perch::identity_dir()\n*14963│        .join(\"registry\")\n 14964│        .join(format!(\"{name}.json\"));\n 14965│    let _ = std::fs::remove_file(snap);\n 14966│    eprintln!(\n      │...\n 15055│    // so clear them now (every snapshot — the identity is dead everywhere).\n*15056│    let snap_dir = perch::identity_dir().join(\"registry\");\n 15057│    let mut evicted = 0usize;\n 15058│    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {\n 15059│        // Prune is an EXPLICIT forget: drop the dead identity's routable rows\n      │...\n 15241│    // explicit forget `prune` performs), so bare-id resolution never strands.\n*15242│    let snap_dir = perch::identity_dir().join(\"registry\");\n 15243│    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {\n 15244│        let mut n = 0usize;\n 15245│        for hex in &revokees {\n      │...\n 17000│    let subnets = spt_store::subnet::SubnetStore::load();\n*17001│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 17002│    for s in &subnets.subnets {\n 17003│        let Some(reg) = regs.get(&s.name) else {\n 17004│            continue;\n      │...\n 20521│    let subnets = spt_store::subnet::SubnetStore::load();\n*20522│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 20523│    for s in &subnets.subnets {\n 20524│        let Some(reg) = regs.get(&s.name) else {\n 20525│            continue;\n      │...\n 34080│        );\n*34081│        let dir = perch::identity_dir().join(\"registry\");\n 34082│        std::fs::create_dir_all(&dir).unwrap();\n 34083│        std::fs::write(\n 34084│            dir.join(format!(\"{subnet}.json\")),\n      │...\n 34222│        // Target-registry collision: ANY holder (even a resting one) owns it.\n*34223│        let snap_dir = perch::identity_dir().join(\"registry\");\n 34224│        std::fs::create_dir_all(&snap_dir).unwrap();\n 34225│        let mut reg = spt_net::net::registry::SubnetRegistry::new();\n 34226│        reg.merge_instance(\n### rc.rs\n 5222│        reg.merge_instance(\"l1\", instance(&own_hex));\n*5223│        let reg_dir = spt_store::perch::identity_dir().join(\"registry\");\n 5224│        std::fs::create_dir_all(&reg_dir).unwrap();\n 5225│        std::fs::write(reg_dir.join(\"s1.json\"), serde_json::to_string(&reg).unwrap()).unwrap();\n 5226│        {\n### sealverb.rs\n 225│    };\n*226│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 227│    let minter_subnets = endpoint_subnets(&regs, &minter);\n 228│    let binding = match resolve_binding_subnet(\n 229│        subnet.as_deref(),\n    │...\n 331│    };\n*332│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 333│    let member_subnets = endpoint_subnets(&regs, &endpoint);\n 334│    let binding = match resolve_binding_subnet(\n 335│        subnet.as_deref(),\n    │...\n 441│        .unwrap_or_else(|_| target.to_string());\n*442│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 443│    let minter_subnets = endpoint_subnets(&regs, minter);\n 444│    let dest_subnets = endpoint_subnets(&regs, &dest_id);\n 445│    let binding = resolve_binding_subnet(\n### wansend.rs\n  293│    };\n *294│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n  295│    let excl = Exclusions::load();\n  296│    let excluded = excl.by_subnet();\n  297│\n     │...\n  342│    let address = Address::parse(endpoint).ok()?;\n *343│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n  344│    let excl = Exclusions::load();\n  345│    let excluded = excl.by_subnet();\n  346│    match resolve_across_visible(&regs, &address, &own_hex, excluded) {\n     │...\n  370│    };\n *371│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n  372│    if spt_daemon::daemon::ensure_running().is_err() {\n  373│        return WanSendOutcome::Failed(\"daemon did not come up\".to_string());\n  374│    }\n     │...\n  586│    };\n *587│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n  588│\n  589│    let excl = Exclusions::load();\n  590│    let excluded = excl.by_subnet();\n     │...\n  815│    };\n *816│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n  817│\n  818│    let excl = Exclusions::load();\n  819│    let excluded = excl.by_subnet();\n     │...\n  992│    };\n *993│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n  994│    if spt_daemon::daemon::ensure_running().is_err() {\n  995│        return WanRestOutcome::Failed(\"daemon did not come up\".to_string());\n  996│    }\n     │...\n 1129│    };\n*1130│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1131│    if spt_daemon::daemon::ensure_running().is_err() {\n 1132│        return WanShellLinkOutcome::Failed(\"daemon did not come up\".to_string());\n 1133│    }\n     │...\n 1265│    };\n*1266│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1267│    if spt_daemon::daemon::ensure_running().is_err() {\n 1268│        return WanDigestOutcome::Failed(\"daemon did not come up\".to_string());\n 1269│    }\n     │...\n 1749│\n*1750│    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1751│    let pairs: Vec<(String, String)> = openings\n 1752│        .iter()\n 1753│        .map(|(subnet, sealed)| (subnet.to_string(), sealed.node_short_hex()))\n     │...\n 3160│        assert!(load_snapshots(&dir.path().join(\"nope\")).is_empty());\n*3161│        let d = dir.path().join(\"registry\");\n 3162│        std::fs::create_dir_all(&d).unwrap();\n 3163│        std::fs::write(\n 3164│            d.join(\"good.json\"),\n     │...\n 3178│    fn write_snapshot(subnet: &str, id: &str, node: &str) {\n*3179│        let dir = perch::identity_dir().join(\"registry\");\n 3180│        std::fs::create_dir_all(&dir).unwrap();\n 3181│        let map = reg_with(subnet, id, node);\n 3182│        std::fs::write(\n\n### api/\n#### reporting.rs\n 1302│pub(crate) fn registry_node_label_map() -> std::collections::BTreeMap<String, String> {\n*1303│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 1304│    let mut out = std::collections::BTreeMap::new();\n 1305│    for reg in regs.values() {\n 1306│        for (k, l) in reg.node_labels() {\n\n### picker/\n#### data.rs\n 123│    let subnets = spt_store::subnet::SubnetStore::load();\n*124│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 125│    for s in &subnets.subnets {\n 126│        if let Some(reg) = regs.get(&s.name) {\n 127│            for (node, label) in reg.node_labels() {\n    │...\n 313│    let subnets = spt_store::subnet::SubnetStore::load();\n*314│    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join(\"registry\"));\n 315│    // [impl->REQ-VIS-REMOTE-NOT-HIDE-NEW] \"visibility-gated exactly like `spt\n 316│    // endpoint list`\" is now literal: the same seam, node-wide scope, ownership\n 317│    // from the local roster — so a hide_new node's picker stops offering an empty\n    │...\n 798│\n*799│        let snap_dir = perch::identity_dir().join(\"registry\");\n 800│        std::fs::create_dir_all(&snap_dir).unwrap();\n 801│        std::fs::write(\n 802│            snap_dir.join(\"home.json\"),\n    │...\n 900│\n*901│        let snap_dir = perch::identity_dir().join(\"registry\");\n 902│        std::fs::create_dir_all(&snap_dir).unwrap();\n 903│        std::fs::write(snap_dir.join(\"home.json\"), serde_json::to_string(&reg).unwrap()).unwrap();\n 904│\n    │...\n 977│\n*978│        let snap_dir = perch::identity_dir().join(\"registry\");\n 979│        std::fs::create_dir_all(&snap_dir).unwrap();\n 980│        std::fs::write(snap_dir.join(\"bignet.json\"), serde_json::to_string(&reg_big).unwrap())\n 981│            .unwrap();"},"isError":false,"timestamp":1789522950976}}
{"type":"message","id":"58c3e69f","parentId":"c8a1d44d","timestamp":"2026-09-16T01:42:31.081Z","message":{"role":"toolResult","toolCallId":"call_KHXyRdPpv0ki5RGCfwXuQm5G|fc_0cdf94810c888815016aa9f406d88487d1a6aaa7a577d0f4ec","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/\n## traceable-reqs.toml\n 4882|  \"REQ-HAZARD-REGISTRY-STALE-CLEAN\",\n*4883|  \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\",\n 4884|  \"REQ-HAZARD-SINGLE-PATH-SOURCE\",\n 4885|  \"REQ-HAZARD-STALE-INDEX-LOCK\",\n 4886|  \"REQ-HAZARD-STORE-INIT-RACE\",\n...\n 7749|[[requirements]]\n*7750|id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n 7751|title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side effects, and durable publication across processes using one stable per-registry sentinel. Atomic replacement alone must not erase acknowledged registrations or persisted name-allocation history. Lock acquisition failure never permits an unlocked write, and process exit releases the lock. Both writer binaries must participate; a brain-only update paire...\n 7752|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n\n## docs/\n### KNOWN-HAZARDS.md\n 462|### 6.14 Serving-registry writers must serialize across processes\n*463|<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n 464|- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n 465|- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n 466|- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n\n## crates/\n\n### spt-store/src/\n#### serving.rs\n 145|/// acquire it again while holding it; independently opened handles contend.\n*146|// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 147|// [impl->REQ-WEB-SERVING-REGISTRY]\n 148|// [impl->REQ-WEB-ATTACHMENT-PULL]\n 149|pub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n\n### spt-daemon/src/\n#### servehost.rs\n 132|// [impl->REQ-WEB-SERVING-REGISTRY]\n*133|// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 134|pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n 135|    // [impl->REQ-WEB-URL-BOUND-PORT]\n 136|    if matches!(&request, ServeRequest::DocsStatus) {\n...\n 634|\n*635|    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 636|    #[test]\n 637|    fn unavailable_registry_lock_cannot_publish_an_add() {\n 638|        let tmp = tempfile::tempdir().unwrap();\n...\n 653|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*654|    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 655|    #[test]\n 656|    fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\n 657|        let tmp = tempfile::tempdir().unwrap();\n#### livehost.rs\n 1280|// [impl->REQ-WEB-ATTACHMENT-PULL]\n*1281|// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 1282|fn reap_expired_attachments() {\n 1283|    let registry_path = perch::serving_registry_file();\n 1284|    if !registry_path.exists() {"}],"details":{"scopePath":".worktrees/308-registry-process-lock/traceable-reqs.toml, .worktrees/308-registry-process-lock/docs/STORAGE.md, .worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md, .worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs, .worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs, .worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":8,"fileCount":5,"files":[".worktrees/308-registry-process-lock/traceable-reqs.toml",".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md",".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/traceable-reqs.toml","count":2},{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs","count":3},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","count":1}],"truncated":false,"displayContent":"# .worktrees/308-registry-process-lock/\n## traceable-reqs.toml\n 4882│  \"REQ-HAZARD-REGISTRY-STALE-CLEAN\",\n*4883│  \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\",\n 4884│  \"REQ-HAZARD-SINGLE-PATH-SOURCE\",\n 4885│  \"REQ-HAZARD-STALE-INDEX-LOCK\",\n 4886│  \"REQ-HAZARD-STORE-INIT-RACE\",\n     │...\n 7749│[[requirements]]\n*7750│id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n 7751│title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side effects, and durable publication across processes using one stable per-registry sentinel. Atomic replacement alone must not erase acknowledged registrations or persisted name-allocation history. Lock acquisition failure never permits an unlocked write, and process exit releases the lock. Both writer binaries must participate; a brain-only update paire...\n 7752│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n\n## docs/\n### KNOWN-HAZARDS.md\n 462│### 6.14 Serving-registry writers must serialize across processes\n*463│<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n 464│- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n 465│- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n 466│- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n\n## crates/\n\n### spt-store/src/\n#### serving.rs\n 145│/// acquire it again while holding it; independently opened handles contend.\n*146│// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 147│// [impl->REQ-WEB-SERVING-REGISTRY]\n 148│// [impl->REQ-WEB-ATTACHMENT-PULL]\n 149│pub fn lock_registry_at(registry_path: &Path) -> io::Result<fs::File> {\n\n### spt-daemon/src/\n#### servehost.rs\n 132│// [impl->REQ-WEB-SERVING-REGISTRY]\n*133│// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 134│pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n 135│    // [impl->REQ-WEB-URL-BOUND-PORT]\n 136│    if matches!(&request, ServeRequest::DocsStatus) {\n    │...\n 634│\n*635│    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 636│    #[test]\n 637│    fn unavailable_registry_lock_cannot_publish_an_add() {\n 638│        let tmp = tempfile::tempdir().unwrap();\n    │...\n 653│    // [unit->REQ-WEB-SERVING-REGISTRY]\n*654│    // [unit->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 655│    #[test]\n 656│    fn concurrent_requests_publish_distinct_entries_without_lost_updates() {\n 657│        let tmp = tempfile::tempdir().unwrap();\n#### livehost.rs\n 1280│// [impl->REQ-WEB-ATTACHMENT-PULL]\n*1281│// [impl->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n 1282│fn reap_expired_attachments() {\n 1283│    let registry_path = perch::serving_registry_file();\n 1284│    if !registry_path.exists() {"},"isError":false,"timestamp":1789522951080}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_upibUYmBZcJhNEgTq7pz0vt7|fc_0cdf94810c888815016aa9f424b72087d18e94f755044f3d07","toolName":"read","startedAt":"2026-09-16T01:43:00.827Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/seedmap.rs:145-200"},"intent":"Checking final request dispatch route"},"id":"c2595847","parentId":"58c3e69f","timestamp":"2026-09-16T01:43:00.827Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kfJhLrLGfJpOtIv81bSvC4Pd|fc_0cdf94810c888815016aa9f424b73087d187118dfb0b065f9e","toolName":"read","startedAt":"2026-09-16T01:43:00.827Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt/src/rc.rs:3095-3165"},"intent":"Checking input reference failure handling"},"id":"1f90a16b","parentId":"c2595847","timestamp":"2026-09-16T01:43:00.827Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_K4ldp2LLM2Z6s7XtIST71w3X|fc_0cdf94810c888815016aa9f424b73c87d18ea1af0b775493f4","toolName":"read","startedAt":"2026-09-16T01:43:00.828Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs:410-455"},"intent":"Checking remaining fixture publication"},"id":"6a3892ba","parentId":"1f90a16b","timestamp":"2026-09-16T01:43:00.828Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_CahXSOkc5UO8Zzz3WKGwBIO8|fc_0cdf94810c888815016aa9f424b74487d1a9883c291c53e06a","toolName":"grep","startedAt":"2026-09-16T01:43:00.828Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs:191-212;.worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs:870-1414;.worktrees/308-reg…"},"intent":"Checking target test coverage names"},"id":"6b54f5dc","parentId":"6a3892ba","timestamp":"2026-09-16T01:43:00.828Z"}
{"type":"message","id":"f7955095","parentId":"6b54f5dc","timestamp":"2026-09-16T01:43:00.828Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Preparing read-only review**\n\n**Reviewing source locking**\n\n**Checking Windows lock tests**"},{"type":"toolCall","id":"call_upibUYmBZcJhNEgTq7pz0vt7|fc_0cdf94810c888815016aa9f424b72087d18e94f755044f3d07","name":"read","arguments":{"i":"Checking final request dispatch route","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/seedmap.rs:145-200"},"intent":"Checking final request dispatch route"},{"type":"toolCall","id":"call_kfJhLrLGfJpOtIv81bSvC4Pd|fc_0cdf94810c888815016aa9f424b73087d187118dfb0b065f9e","name":"read","arguments":{"i":"Checking input reference failure handling","path":".worktrees/308-registry-process-lock/crates/spt/src/rc.rs:3095-3165"},"intent":"Checking input reference failure handling"},{"type":"toolCall","id":"call_K4ldp2LLM2Z6s7XtIST71w3X|fc_0cdf94810c888815016aa9f424b73c87d18ea1af0b775493f4","name":"read","arguments":{"i":"Checking remaining fixture publication","path":".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs:410-455"},"intent":"Checking remaining fixture publication"},{"type":"toolCall","id":"call_CahXSOkc5UO8Zzz3WKGwBIO8|fc_0cdf94810c888815016aa9f424b74487d1a9883c291c53e06a","name":"grep","arguments":{"i":"Checking target test coverage names","pattern":"^\\s*fn .*\\(|#\\[test\\]","path":".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs:191-212;.worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs:870-1414;.worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs;.worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs","case":true,"gitignore":true,"skip":null},"intent":"Checking target test coverage names"},{"type":"toolCall","id":"call_A3qlTjORnd0cu6V6XDIJCD2X|fc_0cdf94810c888815016aa9f424b74887d1b0c1c56aca451361","name":"grep","arguments":{"i":"Checking write search remainder","pattern":"serve[/\\\\]registry|join\\(\"serve\"\\)|join\\(\"registry\"\\)|remove_file\\([^\\n]*registry|remove_dir_all\\([^\\n]*serve","path":".worktrees/308-registry-process-lock/crates","case":true,"gitignore":true,"skip":20},"intent":"Checking write search remainder"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":11796,"output":900,"cacheRead":66944,"cacheWrite":0,"totalTokens":79640,"reasoningTokens":516,"cost":{"input":0.11796000000000001,"output":0.045000000000000005,"cacheRead":0.066944,"cacheWrite":0,"total":0.22990400000000003}},"stopReason":"toolUse","timestamp":1789522951112,"responseId":"resp_0cdf94810c888815016aa9f4075d3c87d18ce6ad96d9901484","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cdf94810c888815016aa9f409bfe887d1bed24fcca246ef46","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqfQk3ltKzESGf5koGNW25as9V7L6SjqExLcQSpCsyTTaE-fldhXenrMAmw4wBJFdGou9FrXM7T5UI4CcbkWZH6xX5i8RVGXvmx6sj_99GQ2lfScBfdgHhDzO8H8qj3en7uGOpjHfNkbbIPOldYWG6gk80luFPqAIYYaUuqaKGKhMj3Uyv7ELc9PBanSUDKlRNiaRtAMXa2-BAtRSvE4_5KCbze7KmfirvlW_hHE3vQACe8wKTYoFG83VHwCjx28qHHbNAzsyAJkDFpks6ps3pWP2U0a_iiNwEeTNXkpHBr3xOhDmzf2RvOWj3QRv7cGKnUHo8aQOryOhTokk_HB1AhsjLziyhsVi02QYu4qx2Y0-SfekH20v98xE-a7nr7sqXAfsRPnCmtTrjK9ph99Mp3w_H3EHp_TlCh3AAuWsZA19GJ1PEXIBO49SFFEk6qMTPonMUrCv2eU336mXeDtpuJG8UQ_aNGG4e7InjRxTBGPVc7l5hACMKHU2kAuUl-VoOfrnNlvuidy6GT0YAUsvJbJYqoZ_U3n8sb655LRI_7Fp2WJOOx3ViRLv5FyelwoGxG5fFWVRgm0rLWs6PytYL59I2tbX1IJGEz8oR4pACM298yzjAAESg96n9Ge0w0-bXtsDpHMP3TJbzVWSSBA6OJ1_k-kseJnTziy8K-j0KI4tokR_krVEbzNROCKsYKyOck9QvuY9GOCMVX75hDCnSxt_DLAdsNe5SSTKmAGC4lNEQXCNhAZsmi2zATn2ryPMykJ1iFXA8JsrAJ2_zVlyUYIvvrBnSZMpaoA8yfUmjCvorASKVeNYPPc86zTafC6Gp_gZTZM6IHsZgHJ_dciJRwYCsq2mALA26uYOH_tiu_piENPbFVDteAvH3iVIrxq5KHSKIplOgjEqvw_FqBhupi8P7hN5_YafgVIfYuTT53--B9TsPwS9E_ra5IE8wIVbZtPETnV8sbgOICK74QU4udbGc1Nov_FptD50mzkajKuOeq5QPFaJQ-S8qjfDQdfqKQUmomygH7ypTZWxiEe2CEWxQqpA2mGHPthoEu0Jg4rjrhNox2PqjU-ZFgoxs4Kkfcz3M7KDty_I56HLP2r26qApqJIs3VtpqXbLJfOdmigMQ_wS9KqnR78mC2PUp_21x_q046vM6ss0gi3eWxRxpWcjqa-sfl375beWd5BPJ802ArLfiPI9SNaM-iLp82jpvPx2dOfhkuw96hhukrDteJgPGz0mKwRFgLOPurL8g4tNl9irwTsY5p-S0e2paKnbIyC5EBgi62JXb8f_qcWXHqm6eYfuJ0N0s4oumKCgdhDRdgdwsvjZe10R5I0YG4cPkcd-s-x1s4yCVn8PkCnNnFGE8RvoMST58ZW5WJmxNN6kr03IHH9k-B1MNvzyVknr6bRjp31yBnJGsx3p90jaO6t3cVAQ2J2xck3ZIvOKG2a2kvuRsPbLkzRjTuIpah9d2OS4RYmee8BmE6ycPTg-lUkZtdB1CCpkN0vOxekfrXKHyXjFZXeJZ2yzXf5qYRLZgfPG2kHT-oqSbFNuDKckzTrGC-t5Egwemihtzt2I5fd5eFUYH9nWhUPoUym3WzNSLnOKxOiE9jB_AQ8jnCZYabUqE6jsiKZI1gAl1gYEIHD5W7aH2Uft8TOegkd--ptUGRFLr91i5BOgTqw0mfbNDGCrsAXdnVlRLNvj1_DbIKtxHNcc7AIzpVmLffwYOnSGGjPygl5Qfrxsw5-oj9uqDQzOcUSXqPsWqfGk7ewk3uk8CSrJ3UTz6uHujrQ2rd2Ux_UAp40hEmRbHV_kuN1RMnK8FtHba20-Gf5uM2q4XSSDNIgMkpEphqXBsCiDWZtB0_Nn0JqccsSeodvKGs5-Xgek2UIGzr9mn4Tvh6zkpAZgep4Amj1WDuLkTn8UA0U-juIiHe1NHBqfj7VMGRS2vt_2J-ua05sCqtJLfOQkUk3K2qSO2iyis5TyKiJM8QJl5J7BqRrX4Q92vTSJHJWwxZ9Bo5qrQ3bGWx93dLAlBqaxj6rauiTNT-ljR2XvU_FY_-pPH5n8VmmJ-6iGcl1kyT7KLT8ntxLqWuNXYH3oJD3XYK5gygbRSAdl1FC-avLqqNhRjSK-U4kmSi3VO6pz8YmeH6ouJC68bmWHQ-Hba-kKEpsIh2LYUfjCyjFPfH7aJFw989RT1Q2ubUc6PvIXqrIvCaoRke3p3lKy_TUxuVicM8-E3Qg6v2f2MbM5kVqGYQP6alYU1m5kHHyspsP5aoOFn_aFMJ-K_j4O6nP9UfhsB7Yt2Ilk87cCOqdxVi_LSoU1przbGJaGRE359o_o6DSAKeSBO8pWkIYQpCxcS1zfGpIboJv3DlX7ogWKY4aBeo99Vp8y0ApulAdRiBIqEVvRFJDZP1yAG8YAXJrTDRiNXRSoLSqZl5ZoGYneJVpb6AEXAUZo2QBGHg6xJsiEccwiPPiWWiUcUD5q1ONi0GHmaxbOsSJ8ILnl-MMUf1hRnYyezCN-V3SzyMSwB-mSJGhdxQoByNm_FQw4xqggNOXxg0w7Xdlqd0cItOZSS3UwnAFKEU6MpfKCQgFxsTr7gMASCYwgrnhfXhcSc_jwTTn7bllD1hLzemjRHGbkgwd1z-baaHRC9M1YLPGNbje2rKUL_ghadAerzE2EXRbku2T7cScQIQfJhJDxmP3NHUX-RjEQ-3DHPCaHPBaxjRNoEqTgciEsIbKZStB2W6CYPevc7Y7Lw9f7V94cx3i0RxFwvqJlz_2nxsEzKFL8oxKPsxU8yumHOLXvR-_fxUh-KaALQSESFaytbkDoEczlKNSeBBZd_tPak3H4-8HSbG8QKj3dMZdwTLIwmOsn3PKAT3oC4sjl4bjlJmQHeRCfyRsLBxm6ZNt6jLn2vERlXYongj_aylLPLcUXVGR9cEzkRi3w3kwvQVex7ezD5zLV7KkQoSJODmk1u45xGlrCxHBnPegcs6RmzNx0W2hTZwuaZhqsq0OUcElQv_iRzSMRXiyWkQT_vnA9lg9w84YB7lzuauhtpH9mXMWPfrkudLit_yUur2B6bPonpCN3MNpyIL59uzR5LNCNCat_QZcl0ymKmvFLRFW_J5zAA-N_uDYOauzK_5FAsSr8HND4xRbrN47KY5yEZYe30Lh7dH9wi9DsNeF7RICzHTsaRFShlpvnipK5k9awcKH2JJkhzlPUyig6z-Ml4UuUMhE27Fg_a4vOfqWVOpZIZAy_FKxgFGxxYB7QRfRRXghmdFvhiYzYprW8yxPzWEwSQuE_c7WcBE8AXitY-Fx9fcBbnw2SiDegUnH32q-0sPXK0zYxQzKpUVDwNYL1uV-P_WhM3Wy_qlJWcN8lJTaK66gzW4x1oMVIFXTYe8Ag_pqS6al-70aoYovMRbMomjMZDwv1DXIlbh6JN6q8W_Z03mtZOidpjnBiKkJjPVCA6c9pH_Tkoqo6xR3CveKVzsM49dJ-TSTx7G_gnMvhmlQO3nJzRgT31n5aYoFozrJf-OjEtWn3xq4fk-U1MuDIEsiJQOAg7lvX0RxOgt0KCQ6200UjTwJ3GrZ8vS2JSkCasC3JQXna_7XODq2XaG_5jDnbW9WlJKbE8TkwHH-CuHrbXoRXxCMG2mZ4Vl1KwiKJwU5gDAOu_e2xL1PFtjrcKtKqTUEScXqB7hIKG25792dOb468WoDQYl9dp_6TL-P6GMR6xDv8DrEA6ytEorKi8qQItHu9g28LB1rBUZvvmkU_NtcOSKrxNsb-K6PkWs9RgZ96K-BKiRu3-28PrSS6T0-rH4vgBxWMOqbiQWNcdx4AnCR682emhIoDLNjsetc-gz6T9x5fJiaqEnJNVKnXQb_GjgObg6elPoFnBRqgHomvEbB_7GVQxClW-AVSoRFFEItJbkftTIzMvU6ZbpSeZCLevHPXuRJzCbcGezuqcaV0kas2NUzu7bQawKd9b7L-MphWMgsjPUe1lLiKJ5AcDnbpK8h7k5m3KFlPRbMTDO86hrjBmtH0czN_OHr4DvXA90LyvyH_9Vwe2h8_6s2Fd7g7jF825ARJwAoT6uKu-8B4aQ_2OQtat6b9JGkzJn3Tl0zjo0dQ5WothDvXQ_ubsQfqLr4JMrC9JhpQj1OU4Es_kgCEhJdPU2v8Rcewc05mmPEFc9Bf3HnULdcRuYHzGO-5Xub64TFmYEiYuo0FvN8x1ViM1LQKIIVsYwe7Xxq7XWYalt6MRQz8LuVvY94jvkPYebWDdiTYahOip_8_c7WpGIV3BgiQ0f5kUD5-O5Sug2O-8xBnxR4K_jddBd8WRHzZKZAekBPQn6S-kRP18ZQHBgOA_6fAeaNa_rys2gb532ctRSG6LvOsdCRmV_Zkcjw3anjp1L_W0vQQcaJ2vUvyFmFbuzH6pGBXkWjHMjUn-gfVlBq4TOT-mGXWQJ8EPdqp5eHkge6IKJlDU2ROdvUxloiZarKbBPgSumQhl9TVW1E2iTFXBMumfjFJGxOVlnuCVCJSsRJRSY2dwGCfOZBcUpnxoA0SSGvjKovc6MNwqQ3E16PEv8KA1dSYysCVZv6ivb3DkfTzghNSN4NBYXPW4fyBEEGHyi9Hn1FLUsNvYF-EnVpaRSZMwNE2Om7LRY2DDuBlCXYMDkkKNI6mm9mhHDeR_bpXLIUEcTxkSKxG0izQbnBasOpgvuOTjUq9voj6bCEirBo0g8oTfmEXjPibyNhZzfBHkhwuFxYVPwY-N7SSqNmGqsrLZheTGYq-27VWhSuvOwtu9TgdhkkiEVWgI0b1GXrME0XSlrGEUHa6Kh0jk-mCBfdwqE829phuFLuyzqsSfANBHHgFtIvnQ-BiR4fOoc5DY0KIYCGzQo0FcgB0ApVCPT3CJsCNg3iKsu-3cdpT35FTeg_bI2OVD6JnKmekjmAEFCLQRxIZx8zKErP2YT572-g7MqILpPsaIV6gaJz7GzkO4Vliu1n0IgrOzhN0SxNdT0rYsiP88XgFcNffNVltrh4Ob7Wc3YocCXAjjag6eeMbTzRRBkRvvxmJNlTeDSjSzp9Ri0o7ML1HewzMJ-qu33FmaPJUTAnm","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"summary":[{"type":"summary_text","text":"**Preparing read-only review**"},{"type":"summary_text","text":"**Reviewing source locking**"},{"type":"summary_text","text":"**Checking Windows lock tests**"}],"metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f424b72087d18e94f755044f3d07","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking final request dispatch route\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src/seedmap.rs:145-200\"}","call_id":"call_upibUYmBZcJhNEgTq7pz0vt7","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f424b73087d187118dfb0b065f9e","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking input reference failure handling\",\"path\":\".worktrees/308-registry-process-lock/crates/spt/src/rc.rs:3095-3165\"}","call_id":"call_kfJhLrLGfJpOtIv81bSvC4Pd","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f424b73c87d18ea1af0b775493f4","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking remaining fixture publication\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/tests/webserve_e2e.rs:410-455\"}","call_id":"call_K4ldp2LLM2Z6s7XtIST71w3X","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"read","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f424b74487d1a9883c291c53e06a","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking target test coverage names\",\"pattern\":\"^\\\\s*fn .*\\\\(|#\\\\[test\\\\]\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs:191-212;.worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs:870-1414;.worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs;.worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_CahXSOkc5UO8Zzz3WKGwBIO8","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f424b74887d1b0c1c56aca451361","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking write search remainder\",\"pattern\":\"serve[/\\\\\\\\]registry|join\\\\(\\\"serve\\\"\\\\)|join\\\\(\\\"registry\\\"\\\\)|remove_file\\\\([^\\\\n]*registry|remove_dir_all\\\\([^\\\\n]*serve\",\"path\":\".worktrees/308-registry-process-lock/crates\",\"case\":true,\"gitignore\":true,\"skip\":20}","call_id":"call_A3qlTjORnd0cu6V6XDIJCD2X","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"grep","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":29709.18729999999,"ttft":2662.1614999999874,"completedAt":1789522980823,"contextSnapshot":{"promptTokens":78740,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_A3qlTjORnd0cu6V6XDIJCD2X|fc_0cdf94810c888815016aa9f424b74887d1b0c1c56aca451361","toolName":"grep","startedAt":"2026-09-16T01:43:00.829Z","args":{"path":".worktrees/308-registry-process-lock/crates"},"intent":"Checking write search remainder"},"id":"d951d079","parentId":"f7955095","timestamp":"2026-09-16T01:43:00.829Z"}
{"type":"message","id":"9409e736","parentId":"d951d079","timestamp":"2026-09-16T01:43:00.843Z","message":{"role":"toolResult","toolCallId":"call_upibUYmBZcJhNEgTq7pz0vt7|fc_0cdf94810c888815016aa9f424b72087d18e94f755044f3d07","toolName":"read","content":[{"type":"text","text":"pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {\n…\n    loop {\n…\n        thread::spawn(move || {\n…\n        });\n    }\n}\n\n/// Drive one control connection: handshake, then service its op frames until it\n/// disconnects.\nfn handle_conn<C: Read + Write>(\n    mut conn: C,\n    reg: &SeedRegistry,\n    stop: &std::sync::atomic::AtomicBool,\n    name: &str,\n) -> io::Result<()> {\n    // A control client announces itself as a brain (a transient logic client of\n    // the daemon); the version handshake gives the same forward-compat as the\n    // broker IPC.\n    recv_hello(&mut conn, Role::Brain)?;\n    loop {\n        let env = match read_frame(&mut conn) {\n            Ok(e) => e,\n            Err(_) => return Ok(()), // peer hung up — connection done\n        };\n        match env.kind.as_str() {\n            KIND_SEED_PUT => {\n                let seed: Seed = serde_json::from_value(env.payload)\n                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                let echo = seed.clone();\n                reg.put(seed);\n                // Echo the stored seed so the client knows the put landed.\n                write_frame(&mut conn, &result_frame(Some(echo)))?;\n            }\n            KIND_SEED_TAKE => {\n                let req: TakeReq = serde_json::from_value(env.payload)\n                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;\n            }\n            // Serving exposure is mutated only here in the daemon, never by\n            // the CLI or the read-only HTTP listener.\n            // [impl->REQ-WEB-SERVING-REGISTRY]\n            crate::servehost::KIND_SERVE_REQUEST => {\n                write_frame(&mut conn, &crate::servehost::handle(env.payload))?;\n            }\n            KIND_STOP => {\n                // Ack FIRST so the client's bounded read completes, then latch + wake\n                // the accept loop with a throwaway dial.\n                write_frame(\n                    &mut conn,\n                    &Envelope::new(KIND_STOPPING, serde_json::Value::Null),\n                )?;\n                stop.store(true, std::sync::atomic::Ordering::SeqCst);\n                // (ii) REQ-HAZARD-DAEMON-STOP-BARRIER conn-close discipline: DROP this\n                // handler's OWN server-side conn (instance A) BEFORE the wake-dial +\n                // return, so it can never be the lingering server instance that pins\n                // the pipe name past the stop barrier (the self-sustaining leak that\n                // held the name busy → every barrier ping parked → TimedOut). The\n                // wake-dial still connects — the serve loop is parked in accept, ready,\n                // so it does not park.\n                drop(conn);\n                let _ = LocalSocketTransport::connect(name);\n                return Ok(());\n            }\n…\n        }\n    }\n}\n\n[Showing lines 144-203 of 574. Use :204 to continue]"}],"details":{"truncation":{"content":"        });\r\n    }\r\n}\r\n\r\n/// Drive one control connection: handshake, then service its op frames until it\r\n/// disconnects.\r\nfn handle_conn<C: Read + Write>(\r\n    mut conn: C,\r\n    reg: &SeedRegistry,\r\n    stop: &std::sync::atomic::AtomicBool,\r\n    name: &str,\r\n) -> io::Result<()> {\r\n    // A control client announces itself as a brain (a transient logic client of\r\n    // the daemon); the version handshake gives the same forward-compat as the\r\n    // broker IPC.\r\n    recv_hello(&mut conn, Role::Brain)?;\r\n    loop {\r\n        let env = match read_frame(&mut conn) {\r\n            Ok(e) => e,\r\n            Err(_) => return Ok(()), // peer hung up — connection done\r\n        };\r\n        match env.kind.as_str() {\r\n            KIND_SEED_PUT => {\r\n                let seed: Seed = serde_json::from_value(env.payload)\r\n                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                let echo = seed.clone();\r\n                reg.put(seed);\r\n                // Echo the stored seed so the client knows the put landed.\r\n                write_frame(&mut conn, &result_frame(Some(echo)))?;\r\n            }\r\n            KIND_SEED_TAKE => {\r\n                let req: TakeReq = serde_json::from_value(env.payload)\r\n                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;\r\n            }\r\n            // Serving exposure is mutated only here in the daemon, never by\r\n            // the CLI or the read-only HTTP listener.\r\n            // [impl->REQ-WEB-SERVING-REGISTRY]\r\n            crate::servehost::KIND_SERVE_REQUEST => {\r\n                write_frame(&mut conn, &crate::servehost::handle(env.payload))?;\r\n            }\r\n            KIND_STOP => {\r\n                // Ack FIRST so the client's bounded read completes, then latch + wake\r\n                // the accept loop with a throwaway dial.\r\n                write_frame(\r\n                    &mut conn,\r\n                    &Envelope::new(KIND_STOPPING, serde_json::Value::Null),\r\n                )?;\r\n                stop.store(true, std::sync::atomic::Ordering::SeqCst);\r\n                // (ii) REQ-HAZARD-DAEMON-STOP-BARRIER conn-close discipline: DROP this\r\n                // handler's OWN server-side conn (instance A) BEFORE the wake-dial +\r\n                // return, so it can never be the lingering server instance that pins\r\n                // the pipe name past the stop barrier (the self-sustaining leak that\r\n                // held the name busy → every barrier ping parked → TimedOut). The\r\n                // wake-dial still connects — the serve loop is parked in accept, ready,\r\n                // so it does not park.\r\n                drop(conn);\r\n                let _ = LocalSocketTransport::connect(name);\r\n                return Ok(());\r\n            }\r","truncated":true,"truncatedBy":"lines","totalLines":431,"totalBytes":2872,"outputLines":60,"outputBytes":2872,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":574,"displayContent":{"text":"pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {\n…\n    loop {\n…\n        thread::spawn(move || {\n…\n        });\r\n    }\r\n}\r\n\r\n/// Drive one control connection: handshake, then service its op frames until it\r\n/// disconnects.\r\nfn handle_conn<C: Read + Write>(\r\n    mut conn: C,\r\n    reg: &SeedRegistry,\r\n    stop: &std::sync::atomic::AtomicBool,\r\n    name: &str,\r\n) -> io::Result<()> {\r\n    // A control client announces itself as a brain (a transient logic client of\r\n    // the daemon); the version handshake gives the same forward-compat as the\r\n    // broker IPC.\r\n    recv_hello(&mut conn, Role::Brain)?;\r\n    loop {\r\n        let env = match read_frame(&mut conn) {\r\n            Ok(e) => e,\r\n            Err(_) => return Ok(()), // peer hung up — connection done\r\n        };\r\n        match env.kind.as_str() {\r\n            KIND_SEED_PUT => {\r\n                let seed: Seed = serde_json::from_value(env.payload)\r\n                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                let echo = seed.clone();\r\n                reg.put(seed);\r\n                // Echo the stored seed so the client knows the put landed.\r\n                write_frame(&mut conn, &result_frame(Some(echo)))?;\r\n            }\r\n            KIND_SEED_TAKE => {\r\n                let req: TakeReq = serde_json::from_value(env.payload)\r\n                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;\r\n            }\r\n            // Serving exposure is mutated only here in the daemon, never by\r\n            // the CLI or the read-only HTTP listener.\r\n            // [impl->REQ-WEB-SERVING-REGISTRY]\r\n            crate::servehost::KIND_SERVE_REQUEST => {\r\n                write_frame(&mut conn, &crate::servehost::handle(env.payload))?;\r\n            }\r\n            KIND_STOP => {\r\n                // Ack FIRST so the client's bounded read completes, then latch + wake\r\n                // the accept loop with a throwaway dial.\r\n                write_frame(\r\n                    &mut conn,\r\n                    &Envelope::new(KIND_STOPPING, serde_json::Value::Null),\r\n                )?;\r\n                stop.store(true, std::sync::atomic::Ordering::SeqCst);\r\n                // (ii) REQ-HAZARD-DAEMON-STOP-BARRIER conn-close discipline: DROP this\r\n                // handler's OWN server-side conn (instance A) BEFORE the wake-dial +\r\n                // return, so it can never be the lingering server instance that pins\r\n                // the pipe name past the stop barrier (the self-sustaining leak that\r\n                // held the name busy → every barrier ping parked → TimedOut). The\r\n                // wake-dial still connects — the serve loop is parked in accept, ready,\r\n                // so it does not park.\r\n                drop(conn);\r\n                let _ = LocalSocketTransport::connect(name);\r\n                return Ok(());\r\n            }\r\n…\n        }\n    }\n}","startLine":117,"lineNumbers":[117,null,126,null,142,null,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,null,212,213,214]},"fileSize":26382,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\src\\seedmap.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":574,"totalBytes":2872,"outputLines":60,"outputBytes":2872,"shownRange":{"start":144,"end":203},"nextOffset":204}}},"isError":false,"timestamp":1789522980842}}
{"type":"message","id":"0812e9e8","parentId":"9409e736","timestamp":"2026-09-16T01:43:00.845Z","message":{"role":"toolResult","toolCallId":"call_K4ldp2LLM2Z6s7XtIST71w3X|fc_0cdf94810c888815016aa9f424b73c87d18ea1af0b775493f4","toolName":"read","content":[{"type":"text","text":"fn the_adapter_docs_facet_serves_over_the_production_listener() {\n…\n    let docs_root = home.join(\"docs\");\n    std::fs::create_dir_all(&docs_root).unwrap();\n\n    // The adapter as the store lays it out, with its own docs beside its\n    // manifest — and a decoy `docs/` inside the core-owned web root.\n    let adapter_dir = home.join(\"adapters\").join(\"cc\");\n    std::fs::create_dir_all(adapter_dir.join(\"share\").join(\"docs\")).unwrap();\n    std::fs::write(\n        adapter_dir.join(\"share\").join(\"docs\").join(\"index.md\"),\n        b\"# adapter docs bytes\",\n    )\n    .unwrap();\n    let manifest = \"[adapter]\\nname = \\\"cc\\\"\\nversion = \\\"1\\\"\\nmin_spt_core_version = \\\"0\\\"\\ndocs_dir = \\\"share/docs\\\"\\n\";\n    std::fs::write(adapter_dir.join(\"manifest.toml\"), manifest).unwrap();\n    let web_root = spt_store::perch::adapter_web_dir_in(home, \"cc\").unwrap();\n    std::fs::create_dir_all(web_root.join(\"docs\")).unwrap();\n    std::fs::write(web_root.join(\"docs\").join(\"index.md\"), b\"core web root bytes\").unwrap();\n    std::fs::write(web_root.join(\"report.txt\"), b\"core web root report\").unwrap();\n    let registry_path = spt_store::perch::serving_registry_file_in(home);\n    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\n    registry.add_adapter(&web_root, \"cc\", None, 1).unwrap();\n    registry.save_at(&registry_path).unwrap();\n\n    let port = spt_daemon::docshost::start_serving(home.to_path_buf(), docs_root, \"LOCAL\".to_owned(), 0)\n        .expect(\"bind production serving listener\");\n\n    let (status, _, bytes) = http(port, \"GET\", \"/local/a/cc/docs/index.md\");\n    assert_eq!(status, 200);\n    assert_eq!(bytes, b\"# adapter docs bytes\");\n    // Beside, not instead: the facet root still serves the core-owned root.\n    assert_eq!(http(port, \"GET\", \"/local/a/cc/report.txt\").2, b\"core web root report\");\n\n    // The key goes away (an adapter update that drops it): the segment 404s\n    // naming the facet, and NEVER answers with the web root's own docs/.\n    let manifest = \"[adapter]\\nname = \\\"cc\\\"\\nversion = \\\"1\\\"\\nmin_spt_core_version = \\\"0\\\"\\n\";\n    std::fs::write(adapter_dir.join(\"manifest.toml\"), manifest).unwrap();\n    let (status, _, bytes) = http(port, \"GET\", \"/local/a/cc/docs/index.md\");\n    assert_eq!(status, 404);\n    let text = String::from_utf8(bytes).unwrap();\n    assert_eq!(text, \"NOT_FOUND: facet a adapter cc docs\\n\");\n    assert!(!text.contains(\"core web root\"), \"no fall-through to the served root\");\n    // Re-resolved per request, so the same URL answers again when the manifest\n    // comes back — no restart, no cached decision.\n    let manifest = \"[adapter]\\nname = \\\"cc\\\"\\nversion = \\\"1\\\"\\nmin_spt_core_version = \\\"0\\\"\\ndocs_dir = \\\"share/docs\\\"\\n\";\n    std::fs::write(adapter_dir.join(\"manifest.toml\"), manifest).unwrap();\n    assert_eq!(http(port, \"GET\", \"/local/a/cc/docs/index.md\").2, b\"# adapter docs bytes\");\n}"}],"details":{"totalLines":455,"displayContent":{"text":"fn the_adapter_docs_facet_serves_over_the_production_listener() {\n…\n    let docs_root = home.join(\"docs\");\r\n    std::fs::create_dir_all(&docs_root).unwrap();\r\n\r\n    // The adapter as the store lays it out, with its own docs beside its\r\n    // manifest — and a decoy `docs/` inside the core-owned web root.\r\n    let adapter_dir = home.join(\"adapters\").join(\"cc\");\r\n    std::fs::create_dir_all(adapter_dir.join(\"share\").join(\"docs\")).unwrap();\r\n    std::fs::write(\r\n        adapter_dir.join(\"share\").join(\"docs\").join(\"index.md\"),\r\n        b\"# adapter docs bytes\",\r\n    )\r\n    .unwrap();\r\n    let manifest = \"[adapter]\\nname = \\\"cc\\\"\\nversion = \\\"1\\\"\\nmin_spt_core_version = \\\"0\\\"\\ndocs_dir = \\\"share/docs\\\"\\n\";\r\n    std::fs::write(adapter_dir.join(\"manifest.toml\"), manifest).unwrap();\r\n    let web_root = spt_store::perch::adapter_web_dir_in(home, \"cc\").unwrap();\r\n    std::fs::create_dir_all(web_root.join(\"docs\")).unwrap();\r\n    std::fs::write(web_root.join(\"docs\").join(\"index.md\"), b\"core web root bytes\").unwrap();\r\n    std::fs::write(web_root.join(\"report.txt\"), b\"core web root report\").unwrap();\r\n    let registry_path = spt_store::perch::serving_registry_file_in(home);\r\n    let mut registry = ServingRegistry::load_at(&registry_path).unwrap();\r\n    registry.add_adapter(&web_root, \"cc\", None, 1).unwrap();\r\n    registry.save_at(&registry_path).unwrap();\r\n\r\n    let port = spt_daemon::docshost::start_serving(home.to_path_buf(), docs_root, \"LOCAL\".to_owned(), 0)\r\n        .expect(\"bind production serving listener\");\r\n\r\n    let (status, _, bytes) = http(port, \"GET\", \"/local/a/cc/docs/index.md\");\r\n    assert_eq!(status, 200);\r\n    assert_eq!(bytes, b\"# adapter docs bytes\");\r\n    // Beside, not instead: the facet root still serves the core-owned root.\r\n    assert_eq!(http(port, \"GET\", \"/local/a/cc/report.txt\").2, b\"core web root report\");\r\n\r\n    // The key goes away (an adapter update that drops it): the segment 404s\r\n    // naming the facet, and NEVER answers with the web root's own docs/.\r\n    let manifest = \"[adapter]\\nname = \\\"cc\\\"\\nversion = \\\"1\\\"\\nmin_spt_core_version = \\\"0\\\"\\n\";\r\n    std::fs::write(adapter_dir.join(\"manifest.toml\"), manifest).unwrap();\r\n    let (status, _, bytes) = http(port, \"GET\", \"/local/a/cc/docs/index.md\");\r\n    assert_eq!(status, 404);\r\n    let text = String::from_utf8(bytes).unwrap();\r\n    assert_eq!(text, \"NOT_FOUND: facet a adapter cc docs\\n\");\r\n    assert!(!text.contains(\"core web root\"), \"no fall-through to the served root\");\r\n    // Re-resolved per request, so the same URL answers again when the manifest\r\n    // comes back — no restart, no cached decision.\r\n    let manifest = \"[adapter]\\nname = \\\"cc\\\"\\nversion = \\\"1\\\"\\nmin_spt_core_version = \\\"0\\\"\\ndocs_dir = \\\"share/docs\\\"\\n\";\r\n    std::fs::write(adapter_dir.join(\"manifest.toml\"), manifest).unwrap();\r\n    assert_eq!(http(port, \"GET\", \"/local/a/cc/docs/index.md\").2, b\"# adapter docs bytes\");\r\n}\r","startLine":406,"lineNumbers":[406,null,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455]},"fileSize":25010,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\tests\\webserve_e2e.rs"}}},"isError":false,"timestamp":1789522980844}}
{"type":"message","id":"7d795659","parentId":"0812e9e8","timestamp":"2026-09-16T01:43:00.875Z","message":{"role":"toolResult","toolCallId":"call_kfJhLrLGfJpOtIv81bSvC4Pd|fc_0cdf94810c888815016aa9f424b73087d187118dfb0b065f9e","toolName":"read","content":[{"type":"text","text":"fn input_path_worker() -> &'static mpsc::SyncSender<InputPathWork> {\n    static WORKER: std::sync::LazyLock<mpsc::SyncSender<InputPathWork>> = std::sync::LazyLock::new(|| {\n…\n        std::thread::spawn(move || {\n            while let Ok(work) = rx.recv() {\n…\n                if timed_out {\n                    // The existing RPC bounds waiting with a helper thread. Do\n                    // not start further RPCs after one was abandoned: at most ONE\n                    // blocked transport thread can survive in this process.\n                    for queued in rx.try_iter() {\n                        let _ = queued.replies.try_send(decline_input_paths(\n                            queued.request, \"INPUT_PATH_OWNER_UNANSWERED\",\n                        ));\n                    }\n                    break;\n                }\n            }\n        });\n        tx\n    });\n    &WORKER\n}\n\n// [impl->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\nfn register_input_paths(request: InputPathRequest, audience: &str, deadline: Instant) -> (InputPathReply, bool) {\n    use spt_daemon::servehost::{call_with_timeout, ServeRequest, ServeResult};\n    let mut timed_out = false;\n    let mut results = Vec::with_capacity(request.paths.len());\n    let socket = spt_daemon::endpoint::seed_socket_name();\n    for path in request.paths {\n        let remaining = deadline.saturating_duration_since(Instant::now());\n        let outcome = if timed_out || remaining.is_zero() {\n            InputPathOutcome::Declined { reason: \"INPUT_PATH_OWNER_UNANSWERED\".into() }\n        } else {\n            match call_with_timeout(&socket, ServeRequest::AddInputReference {\n                path: std::path::PathBuf::from(&path),\n                receipt_id: request.receipt_id.clone(),\n                audience: audience.to_owned(),\n                received_at_ms: request.received_at_ms,\n            }, remaining.min(Duration::from_secs(1))) {\n                Ok(ServeResult::Missing) => InputPathOutcome::Missing,\n                Ok(ServeResult::Entry { entry }) => match crate::serveverb::node_and_port() {\n                    Ok((node, port)) => InputPathOutcome::Registered {\n                        url: spt_store::serving::entry_url(&node, port, &entry),\n                        expires_at_ms: entry.registered_at_ms.saturating_add(\n                            spt_store::serving::HELPER_ENTRY_TTL_MS,\n                        ),\n                    },\n                    Err(reason) => InputPathOutcome::Unanswered { reason },\n                },\n                Ok(ServeResult::Refused { error }) => InputPathOutcome::Declined { reason: error },\n                Ok(_) => InputPathOutcome::Declined { reason: \"INPUT_PATH_UNEXPECTED_REPLY\".into() },\n                Err(error) => {\n                    timed_out = error.kind() == std::io::ErrorKind::TimedOut;\n                    InputPathOutcome::Unanswered {\n                        reason: format!(\"INPUT_PATH_OWNER_UNANSWERED: {error}\"),\n                    }\n                }\n            }\n        };\n        results.push(InputPathResult { path, outcome });\n    }\n    (InputPathReply { receipt_id: request.receipt_id, results }, timed_out)\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    endpoint_id: &str,\n    remote_node: Option<&str>,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n    client_advisory: &mut ClientAdvisory,\n) -> Result<PumpEnd, String> {\n…\n}\n\n[Showing lines 3094-3168 of 6434. Use :3169 to continue]"}],"details":{"truncation":{"content":"                    // The existing RPC bounds waiting with a helper thread. Do\r\n                    // not start further RPCs after one was abandoned: at most ONE\r\n                    // blocked transport thread can survive in this process.\r\n                    for queued in rx.try_iter() {\r\n                        let _ = queued.replies.try_send(decline_input_paths(\r\n                            queued.request, \"INPUT_PATH_OWNER_UNANSWERED\",\r\n                        ));\r\n                    }\r\n                    break;\r\n                }\r\n            }\r\n        });\r\n        tx\r\n    });\r\n    &WORKER\r\n}\r\n\r\n// [impl->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\r\nfn register_input_paths(request: InputPathRequest, audience: &str, deadline: Instant) -> (InputPathReply, bool) {\r\n    use spt_daemon::servehost::{call_with_timeout, ServeRequest, ServeResult};\r\n    let mut timed_out = false;\r\n    let mut results = Vec::with_capacity(request.paths.len());\r\n    let socket = spt_daemon::endpoint::seed_socket_name();\r\n    for path in request.paths {\r\n        let remaining = deadline.saturating_duration_since(Instant::now());\r\n        let outcome = if timed_out || remaining.is_zero() {\r\n            InputPathOutcome::Declined { reason: \"INPUT_PATH_OWNER_UNANSWERED\".into() }\r\n        } else {\r\n            match call_with_timeout(&socket, ServeRequest::AddInputReference {\r\n                path: std::path::PathBuf::from(&path),\r\n                receipt_id: request.receipt_id.clone(),\r\n                audience: audience.to_owned(),\r\n                received_at_ms: request.received_at_ms,\r\n            }, remaining.min(Duration::from_secs(1))) {\r\n                Ok(ServeResult::Missing) => InputPathOutcome::Missing,\r\n                Ok(ServeResult::Entry { entry }) => match crate::serveverb::node_and_port() {\r\n                    Ok((node, port)) => InputPathOutcome::Registered {\r\n                        url: spt_store::serving::entry_url(&node, port, &entry),\r\n                        expires_at_ms: entry.registered_at_ms.saturating_add(\r\n                            spt_store::serving::HELPER_ENTRY_TTL_MS,\r\n                        ),\r\n                    },\r\n                    Err(reason) => InputPathOutcome::Unanswered { reason },\r\n                },\r\n                Ok(ServeResult::Refused { error }) => InputPathOutcome::Declined { reason: error },\r\n                Ok(_) => InputPathOutcome::Declined { reason: \"INPUT_PATH_UNEXPECTED_REPLY\".into() },\r\n                Err(error) => {\r\n                    timed_out = error.kind() == std::io::ErrorKind::TimedOut;\r\n                    InputPathOutcome::Unanswered {\r\n                        reason: format!(\"INPUT_PATH_OWNER_UNANSWERED: {error}\"),\r\n                    }\r\n                }\r\n            }\r\n        };\r\n        results.push(InputPathResult { path, outcome });\r\n    }\r\n    (InputPathReply { receipt_id: request.receipt_id, results }, timed_out)\r\n}\r\n\r\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\r\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\r\n#[allow(clippy::too_many_arguments)]\r\nfn pump(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    ops: &OpMinter,\r\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\r\n    decoder: &mut AttachDecoder,\r\n    stdout: &mut impl std::io::Write,\r\n    controller: bool,\r\n    endpoint_id: &str,\r\n    remote_node: Option<&str>,\r\n    mouse_mode: &MouseMode,\r\n    mut status: Option<&mut StatusRow>,\r\n    client_advisory: &mut ClientAdvisory,\r","truncated":true,"truncatedBy":"lines","totalLines":3341,"totalBytes":3506,"outputLines":75,"outputBytes":3506,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":6434,"displayContent":{"text":"fn input_path_worker() -> &'static mpsc::SyncSender<InputPathWork> {\n    static WORKER: std::sync::LazyLock<mpsc::SyncSender<InputPathWork>> = std::sync::LazyLock::new(|| {\n…\n        std::thread::spawn(move || {\n            while let Ok(work) = rx.recv() {\n…\n                if timed_out {\n                    // The existing RPC bounds waiting with a helper thread. Do\r\n                    // not start further RPCs after one was abandoned: at most ONE\r\n                    // blocked transport thread can survive in this process.\r\n                    for queued in rx.try_iter() {\r\n                        let _ = queued.replies.try_send(decline_input_paths(\r\n                            queued.request, \"INPUT_PATH_OWNER_UNANSWERED\",\r\n                        ));\r\n                    }\r\n                    break;\r\n                }\r\n            }\r\n        });\r\n        tx\r\n    });\r\n    &WORKER\r\n}\r\n\r\n// [impl->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT]\r\nfn register_input_paths(request: InputPathRequest, audience: &str, deadline: Instant) -> (InputPathReply, bool) {\r\n    use spt_daemon::servehost::{call_with_timeout, ServeRequest, ServeResult};\r\n    let mut timed_out = false;\r\n    let mut results = Vec::with_capacity(request.paths.len());\r\n    let socket = spt_daemon::endpoint::seed_socket_name();\r\n    for path in request.paths {\r\n        let remaining = deadline.saturating_duration_since(Instant::now());\r\n        let outcome = if timed_out || remaining.is_zero() {\r\n            InputPathOutcome::Declined { reason: \"INPUT_PATH_OWNER_UNANSWERED\".into() }\r\n        } else {\r\n            match call_with_timeout(&socket, ServeRequest::AddInputReference {\r\n                path: std::path::PathBuf::from(&path),\r\n                receipt_id: request.receipt_id.clone(),\r\n                audience: audience.to_owned(),\r\n                received_at_ms: request.received_at_ms,\r\n            }, remaining.min(Duration::from_secs(1))) {\r\n                Ok(ServeResult::Missing) => InputPathOutcome::Missing,\r\n                Ok(ServeResult::Entry { entry }) => match crate::serveverb::node_and_port() {\r\n                    Ok((node, port)) => InputPathOutcome::Registered {\r\n                        url: spt_store::serving::entry_url(&node, port, &entry),\r\n                        expires_at_ms: entry.registered_at_ms.saturating_add(\r\n                            spt_store::serving::HELPER_ENTRY_TTL_MS,\r\n                        ),\r\n                    },\r\n                    Err(reason) => InputPathOutcome::Unanswered { reason },\r\n                },\r\n                Ok(ServeResult::Refused { error }) => InputPathOutcome::Declined { reason: error },\r\n                Ok(_) => InputPathOutcome::Declined { reason: \"INPUT_PATH_UNEXPECTED_REPLY\".into() },\r\n                Err(error) => {\r\n                    timed_out = error.kind() == std::io::ErrorKind::TimedOut;\r\n                    InputPathOutcome::Unanswered {\r\n                        reason: format!(\"INPUT_PATH_OWNER_UNANSWERED: {error}\"),\r\n                    }\r\n                }\r\n            }\r\n        };\r\n        results.push(InputPathResult { path, outcome });\r\n    }\r\n    (InputPathReply { receipt_id: request.receipt_id, results }, timed_out)\r\n}\r\n\r\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\r\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\r\n#[allow(clippy::too_many_arguments)]\r\nfn pump(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    ops: &OpMinter,\r\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\r\n    decoder: &mut AttachDecoder,\r\n    stdout: &mut impl std::io::Write,\r\n    controller: bool,\r\n    endpoint_id: &str,\r\n    remote_node: Option<&str>,\r\n    mouse_mode: &MouseMode,\r\n    mut status: Option<&mut StatusRow>,\r\n    client_advisory: &mut ClientAdvisory,\r\n) -> Result<PumpEnd, String> {\n…\n}","startLine":3086,"lineNumbers":[3086,3087,null,3089,3090,null,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,null,3682]},"fileSize":316344,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6434,"totalBytes":3506,"outputLines":75,"outputBytes":3506,"shownRange":{"start":3094,"end":3168},"nextOffset":3169}}},"isError":false,"timestamp":1789522980874}}
{"type":"message","id":"d1864bf9","parentId":"7d795659","timestamp":"2026-09-16T01:43:00.877Z","message":{"role":"toolResult","toolCallId":"call_A3qlTjORnd0cu6V6XDIJCD2X|fc_0cdf94810c888815016aa9f424b74887d1b0c1c56aca451361","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 66|\n*67|    let dir = perch::identity_dir().join(\"registry\");\n 68|    std::fs::create_dir_all(&dir).unwrap();\n 69|    let mut reg = SubnetRegistry::new();\n 70|    reg.merge_instance(\n## hide_new_remote_rows_e2e.rs\n 127|    //    property EXCEPT ownership, which is the property under test.\n*128|    let registry_dir = perch::identity_dir().join(\"registry\");\n 129|    std::fs::create_dir_all(&registry_dir).expect(\"registry dir\");\n 130|    let snapshot = registry_dir.join(format!(\"{subnet}.json\"));\n 131|    let mut reg = SubnetRegistry::default();\n## knock_mutual_cross_node_e2e.rs\n 433|    reg.merge_node_label(&node_b, \"rig-b\".to_string(), 1);\n*434|    let reg_dir = home_a.path().join(\"identity\").join(\"registry\");\n 435|    std::fs::create_dir_all(&reg_dir).expect(\"registry dir\");\n 436|    std::fs::write(\n 437|        reg_dir.join(format!(\"{SUBNET}.json\")),\n## list_json_liveness_parity_e2e.rs\n 56|        .to_hex();\n*57|    let dir = perch::identity_dir().join(\"registry\");\n 58|    std::fs::create_dir_all(&dir).unwrap();\n 59|    let mut reg = SubnetRegistry::new();\n 60|    reg.merge_instance(\n## rc_attach_truth.rs\n 574|    );\n*575|    let reg_dir = perch::identity_dir().join(\"registry\");\n 576|    std::fs::create_dir_all(&reg_dir).unwrap();\n 577|    std::fs::write(reg_dir.join(\"solo.json\"), serde_json::to_string(&reg).unwrap()).unwrap();\n 578|\n## twohost_cli.rs\n 479|    let mut out = BTreeMap::new();\n*480|    let dir = perch::identity_dir().join(\"registry\");\n 481|    let Ok(entries) = std::fs::read_dir(&dir) else {\n 482|        return out;\n 483|    };\n## webserve_cross_node_e2e.rs\n 359|    use spt_net::net::registry::SubnetRegistry;\n*360|    let reg_dir = home.join(\"identity\").join(\"registry\");\n 361|    std::fs::create_dir_all(&reg_dir).expect(\"A's registry dir\");\n 362|    let mut reg = SubnetRegistry::new();\n 363|    reg.merge_node_label(node_b, OWNER_LABEL.to_owned(), 1);\n...\n 628|        let identity_b = home_b.path().join(\"identity\");\n*629|        let reg_dir = identity_b.join(\"registry\");\n 630|        std::fs::create_dir_all(&reg_dir).expect(\"B's registry dir\");\n 631|        let mut reg = SubnetRegistry::new();\n 632|        // B's view of WHO LIVES WHERE: the audience endpoint runs on A. This is"}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":8,"fileCount":7,"files":[".worktrees/308-registry-process-lock/crates/spt/tests/active_only_never_relay_e2e.rs",".worktrees/308-registry-process-lock/crates/spt/tests/hide_new_remote_rows_e2e.rs",".worktrees/308-registry-process-lock/crates/spt/tests/knock_mutual_cross_node_e2e.rs",".worktrees/308-registry-process-lock/crates/spt/tests/list_json_liveness_parity_e2e.rs",".worktrees/308-registry-process-lock/crates/spt/tests/rc_attach_truth.rs",".worktrees/308-registry-process-lock/crates/spt/tests/twohost_cli.rs",".worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt/tests/active_only_never_relay_e2e.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/hide_new_remote_rows_e2e.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/knock_mutual_cross_node_e2e.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/list_json_liveness_parity_e2e.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/rc_attach_truth.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/twohost_cli.rs","count":1},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs","count":2}],"truncated":false,"displayContent":"# .worktrees/308-registry-process-lock/crates/spt/tests/\n## active_only_never_relay_e2e.rs\n 66│\n*67│    let dir = perch::identity_dir().join(\"registry\");\n 68│    std::fs::create_dir_all(&dir).unwrap();\n 69│    let mut reg = SubnetRegistry::new();\n 70│    reg.merge_instance(\n## hide_new_remote_rows_e2e.rs\n 127│    //    property EXCEPT ownership, which is the property under test.\n*128│    let registry_dir = perch::identity_dir().join(\"registry\");\n 129│    std::fs::create_dir_all(&registry_dir).expect(\"registry dir\");\n 130│    let snapshot = registry_dir.join(format!(\"{subnet}.json\"));\n 131│    let mut reg = SubnetRegistry::default();\n## knock_mutual_cross_node_e2e.rs\n 433│    reg.merge_node_label(&node_b, \"rig-b\".to_string(), 1);\n*434│    let reg_dir = home_a.path().join(\"identity\").join(\"registry\");\n 435│    std::fs::create_dir_all(&reg_dir).expect(\"registry dir\");\n 436│    std::fs::write(\n 437│        reg_dir.join(format!(\"{SUBNET}.json\")),\n## list_json_liveness_parity_e2e.rs\n 56│        .to_hex();\n*57│    let dir = perch::identity_dir().join(\"registry\");\n 58│    std::fs::create_dir_all(&dir).unwrap();\n 59│    let mut reg = SubnetRegistry::new();\n 60│    reg.merge_instance(\n## rc_attach_truth.rs\n 574│    );\n*575│    let reg_dir = perch::identity_dir().join(\"registry\");\n 576│    std::fs::create_dir_all(&reg_dir).unwrap();\n 577│    std::fs::write(reg_dir.join(\"solo.json\"), serde_json::to_string(&reg).unwrap()).unwrap();\n 578│\n## twohost_cli.rs\n 479│    let mut out = BTreeMap::new();\n*480│    let dir = perch::identity_dir().join(\"registry\");\n 481│    let Ok(entries) = std::fs::read_dir(&dir) else {\n 482│        return out;\n 483│    };\n## webserve_cross_node_e2e.rs\n 359│    use spt_net::net::registry::SubnetRegistry;\n*360│    let reg_dir = home.join(\"identity\").join(\"registry\");\n 361│    std::fs::create_dir_all(&reg_dir).expect(\"A's registry dir\");\n 362│    let mut reg = SubnetRegistry::new();\n 363│    reg.merge_node_label(node_b, OWNER_LABEL.to_owned(), 1);\n    │...\n 628│        let identity_b = home_b.path().join(\"identity\");\n*629│        let reg_dir = identity_b.join(\"registry\");\n 630│        std::fs::create_dir_all(&reg_dir).expect(\"B's registry dir\");\n 631│        let mut reg = SubnetRegistry::new();\n 632│        // B's view of WHO LIVES WHERE: the audience endpoint runs on A. This is"},"isError":false,"timestamp":1789522980877}}
{"type":"message","id":"9c008a1e","parentId":"d1864bf9","timestamp":"2026-09-16T01:43:00.902Z","message":{"role":"toolResult","toolCallId":"call_CahXSOkc5UO8Zzz3WKGwBIO8|fc_0cdf94810c888815016aa9f424b74487d1a9883c291c53e06a","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-store/\n\n### tests/\n#### serving_registry_two_process_int.rs\n 195|// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n*196|#[test]\n*197|fn loaded_reaper_serializes_registration_without_resurrecting_expiry() {\n 198|    overlap(\"reap\", false);\n 199|}\n 200|\n...\n 202|// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n*203|#[test]\n*204|fn later_reap_preserves_a_registration_published_while_it_was_waiting() {\n 205|    overlap(\"register\", false);\n 206|}\n 207|\n 208|// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n*209|#[test]\n*210|fn abrupt_holder_exit_releases_the_sentinel_for_the_waiting_reaper() {\n 211|    overlap(\"register\", true);\n 212|}\n\n### src/\n#### serving.rs\n 877|\n*878|    fn registry(home: &TempDir) -> ServingRegistry {\n 879|        ServingRegistry::load_at(&crate::perch::serving_registry_file_in(home.path())).unwrap()\n 880|    }\n 881|\n*882|    fn source(home: &TempDir, relative: &str) -> PathBuf {\n 883|        let path = home.path().join(relative);\n 884|        fs::create_dir_all(path.parent().unwrap()).unwrap();\n 885|        fs::write(&path, b\"original\").unwrap();\n...\n 888|\n*889|    fn reload(home: &TempDir, store: &ServingRegistry) -> ServingRegistry {\n 890|        store.save_at(&crate::perch::serving_registry_file_in(home.path())).unwrap();\n 891|        registry(home)\n 892|    }\n...\n 896|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*897|    #[test]\n*898|    fn lifetime_and_audience_survive_storage_for_every_entry_kind() {\n 899|        let home = tempdir().unwrap();\n 900|        let mut store = registry(&home);\n 901|        for (name, kind) in [\n...\n 944|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*945|    #[test]\n*946|    fn first_bare_then_suffix_before_final_extension_per_family() {\n 947|        let home = tempdir().unwrap();\n 948|        let mut store = registry(&home);\n 949|        for (relative, expected) in [\n...\n 962|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*963|    #[test]\n*964|    fn persisted_assignment_retains_identity_and_next_suffix() {\n 965|        let home = tempdir().unwrap();\n 966|        let mut store = registry(&home);\n 967|        let first = store.add_reference(&source(&home, \"one/report.md\"), None, Some(\"writer\"), 7).unwrap();\n...\n 977|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*978|    #[test]\n*979|    fn retired_names_remain_not_found_after_restart_and_new_registration() {\n 980|        let home = tempdir().unwrap();\n 981|        let mut store = registry(&home);\n 982|        let first = store.add_reference(&source(&home, \"one/report.md\"), None, None, 1).unwrap();\n...\n 993|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*994|    #[test]\n*995|    fn retired_names_return_only_to_their_prior_paths_without_rewinding_suffixes() {\n 996|        let home = tempdir().unwrap();\n 997|        let first_path = source(&home, \"one/report.md\");\n 998|        let second_path = source(&home, \"two/report.md\");\n...\n 1014|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1015|    #[test]\n*1016|    fn a_changed_kind_cannot_reclaim_a_paths_prior_name() {\n 1017|        let home = tempdir().unwrap();\n 1018|        let path = source(&home, \"report\");\n 1019|        let mut store = registry(&home);\n...\n 1037|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1038|    #[test]\n*1039|    fn same_absolute_path_preserves_entry_even_after_source_disappears() {\n 1040|        let home = tempdir().unwrap();\n 1041|        let path = source(&home, \"report.md\");\n 1042|        let mut store = registry(&home);\n...\n 1051|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1052|    #[test]\n*1053|    fn explicit_names_and_source_names_share_one_allocator() {\n 1054|        let home = tempdir().unwrap();\n 1055|        let mut store = registry(&home);\n 1056|        let explicit = store.add_reference(&source(&home, \"original.txt\"), Some(\"report.md\"), None, 1).unwrap();\n...\n 1065|    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1066|    #[test]\n\n## spt/tests/\n### webserve_attachment_e2e.rs\n 16|//!\n*17|//! ## The arms (one `#[test]`, sequential — `SPT_HOME` is process-global)\n 18|//!\n 19|//! send with an attachment → the ATTACHED line names the URL · fetch it back\n 20|//! byte-equal · the source EDITED after the send does not change what is served\n...\n 54|/// below: leaving a daemon behind is what made a fixed port unsafe.\n*55|fn rig_docs_port() -> u16 {\n 56|    static PORT: std::sync::OnceLock<u16> = std::sync::OnceLock::new();\n 57|    *PORT.get_or_init(|| {\n 58|        std::net::TcpListener::bind((\"127.0.0.1\", 0))\n...\n 72|impl Drop for DaemonReaper {\n*73|    fn drop(&mut self) {\n 74|        let out = run(&self.home, &[\"daemon\", \"stop\"], None);\n 75|        eprintln!(\n 76|            \"=== rig daemon stop === status={}\n...\n 89|\n*90|fn spt() -> PathBuf {\n 91|    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n 92|}\n 93|\n*94|fn run(home: &Path, args: &[&str], stdin: Option<&str>) -> Output {\n 95|    let mut cmd = Command::new(spt());\n 96|    cmd.no_window()\n 97|        .args(args)\n...\n 148|/// itself — a fetch failure and a port disagreement look identical from ARM 2.\n*149|fn bound_docs_port(home: &Path) -> u16 {\n 150|    let deadline = std::time::Instant::now() + Duration::from_secs(60);\n 151|    loop {\n 152|        let log = std::fs::read_to_string(spt_daemon::stderrlog::sink_path(home)).unwrap_or_default();\n...\n 177|/// over the surface.\n*178|fn http_get(port: u16, path: &str) -> (u16, String) {\n 179|    use std::io::{Read as _, Write as _};\n 180|    // Deliberately the SAME shape as `webserve_cross_node_e2e`'s `http()`, which\n 181|    // is proven against this very server: a read timeout, `Host` carrying the\n...\n 201|\n*202|fn stderr(out: &Output) -> String {\n 203|    String::from_utf8_lossy(&out.stderr).into_owned()\n 204|}\n 205|\n*206|fn stdout(out: &Output) -> String {\n 207|    String::from_utf8_lossy(&out.stdout).into_owned()\n 208|}\n 209|\n 210|/// The URL out of an `ATTACHED:<name>: <url> (…)` line.\n*211|fn attached_url(out: &Output) -> String {\n 212|    let text = stderr(out);\n 213|    let line = text\n 214|        .lines()\n...\n 223|/// EVERY url on the send's `ATTACHED:` lines, in order.\n*224|fn attached_urls(out: &Output) -> Vec<String> {\n 225|    stderr(out)\n 226|        .lines()\n 227|        .filter(|line| line.starts_with(\"ATTACHED:\"))\n...\n 237|/// \"one token names this message everywhere\".\n*238|fn last_msg_out_id(home: &Path, owner: &str) -> String {\n 239|    // Read through the io-log's OWN reader rather than opening a filename and\n 240|    // parsing lines here. The rig used to do the latter against `io.log`; the\n 241|    // file is `io-events.log` (`iolog::IO_LOG_FILE`, whose doc calls itself\n...\n 270|// what proves ONE token rather than two that happen to agree.\n*271|#[test]\n*272|fn an_attachment_is_snapshot_served_fetched_back_and_named_by_its_message() {\n 273|    let home = tempfile::tempdir().expect(\"home\");\n 274|    let home = home.path();\n 275|    let work = tempfile::tempdir().expect(\"work\");\n### webserve_cross_node_e2e.rs\n 46|//!\n*47|//! ## The arms (one `#[test]`, sequential — `SPT_HOME` is process-global)\n 48|//!\n 49|//! byte-equal fetch · edit visible (no cache) · HEAD · Range → 206 · owner's\n 50|//! 404 carried as 404 · WEB deny rule on the owner → 403 naming the surface ·\n...\n 84|\n*85|fn now_ms() -> u64 {\n 86|    std::time::SystemTime::now()\n 87|        .duration_since(std::time::UNIX_EPOCH)\n 88|        .expect(\"clock\")\n...\n 92|/// Run `f` with the process-global `SPT_HOME` pointed at `home`. Sound only\n*93|/// because this binary holds exactly ONE `#[test]` (KNOWN-HAZARDS 5.19).\n*94|fn in_home<T>(home: &Path, f: impl FnOnce() -> T) -> T {\n 95|    std::env::set_var(\"SPT_HOME\", home);\n 96|    f()\n 97|}\n 98|\n*99|fn ready_pid(path: &Path) -> Option<u32> {\n 100|    let s = std::fs::read_to_string(path).ok()?;\n 101|    serde_json::from_str::<serde_json::Value>(&s)\n 102|        .ok()?\n...\n 107|\n*108|fn kill_pid(pid: u32) {\n 109|    #[cfg(windows)]\n 110|    let _ = Command::new(\"taskkill\")\n 111|        .no_window()\n...\n 128|impl DaemonGuard {\n*129|    fn start(home: &Path, spt: &Path, tag: &str) -> DaemonGuard {\n 130|        let ready = home.join(\"brain.ready\");\n 131|        let _ = std::fs::remove_file(&ready);\n 132|        let log = home.join(format!(\"brain-{tag}.stderr.log\"));\n...\n 171|    /// durable sink `daemon run` redirects into (where the announcements land).\n*172|    fn stderr(&self) -> String {\n 173|        let mut text = std::fs::read_to_string(&self.log).unwrap_or_default();\n 174|        text.push_str(&std::fs::read_to_string(spt_daemon::stderrlog::sink_path(&self.home)).unwrap_or_default());\n 175|        text\n...\n 179|    /// `DOCS_SERVER_UP: http://localhost:<port>/<node>/` line.\n*180|    fn docs_port(&self) -> u16 {\n 181|        let deadline = Instant::now() + Duration::from_secs(60);\n 182|        loop {\n 183|            let log = self.stderr();\n...\n 201|\n*202|    fn reap_pids(&mut self) {\n 203|        if let Some(p) = self.brain_pid.take() {\n 204|            kill_pid(p);\n 205|        }\n...\n 212|    /// Stop this daemon now (the \"owner gone\" arm), scoped exactly as drop is.\n*213|    fn stop(&mut self) {\n 214|        let mut cmd = Command::new(&self.spt);\n 215|        cmd.no_window()\n 216|            .args([\"daemon\", \"stop\"])\n...\n 227|impl Drop for DaemonGuard {\n*228|    fn drop(&mut self) {\n 229|        if self.broker.is_some() || self.brain_pid.is_some() {\n 230|            self.stop();\n 231|        }\n...\n 236|/// key, a hermetic daemon config, and membership in the rig subnet.\n*237|fn seed_home(home: &Path) -> String {\n 238|    in_home(home, || {\n 239|        let node = spt_store::nodeid::load_or_create_at(&perch::identity_dir().join(\"node.key\"))\n 240|            .expect(\"node identity\")\n...\n 254|/// The running daemon's OWN dialable address, asked of the daemon itself.\n*255|fn own_addr(home: &Path) -> (String, serde_json::Value) {\n 256|    in_home(home, || {\n 257|        let deadline = Instant::now() + Duration::from_secs(60);\n 258|        loop {\n...\n 278|/// inside it (the arm that times one asserts its own bound).\n*279|fn http(port: u16, method: &str, path: &str, extra: &[(&str, &str)]) -> (u16, String, Vec<u8>) {\n 280|    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\n 281|    stream.set_read_timeout(Some(Duration::from_secs(120))).unwrap();\n 282|    stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();\n...\n 296|\n*297|fn header(headers: &str, name: &str) -> Option<String> {\n 298|    headers\n 299|        .lines()\n 300|        .find_map(|line| line.strip_prefix(&format!(\"{name}: \")))\n...\n 305|/// the single registry writer, so this goes through the real verb.\n*306|fn serve_add(spt: &Path, home: &Path, source: &Path, name: &str, bound_port: u16, other_port: u16) {\n 307|    let mut cmd = Command::new(spt);\n 308|    cmd.no_window()\n 309|        .args([\"serve\", \"add\", source.to_str().unwrap(), \"--as\", name])\n...\n 339|/// whether a sender may author a `user-msg` at all.\n*340|fn typed_perch(home: &Path, id: &str, state: &str) {\n 341|    let perch = home.join(\"owlery\").join(id);\n 342|    std::fs::create_dir_all(&perch).expect(\"perch dir\");\n 343|    spt_store::info::write_info(\n...\n 357|/// green before the thing it measures has happened is not a meter.\n*358|fn seed_a_registry(home: &Path, node_b: &str) {\n 359|    use spt_net::net::registry::SubnetRegistry;\n 360|    let reg_dir = home.join(\"identity\").join(\"registry\");\n 361|    std::fs::create_dir_all(&reg_dir).expect(\"A's registry dir\");\n...\n 375|/// exactly the property arm (iii) varies.\n*376|fn send_as(\n 377|    spt: &Path,\n 378|    home: &Path,\n 379|    as_id: &str,"}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs, .worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs, .worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs, .worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":60,"fileCount":4,"files":[".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs",".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs",".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs",".worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt-store/tests/serving_registry_two_process_int.rs","count":6},{"path":".worktrees/308-registry-process-lock/crates/spt-store/src/serving.rs","count":20},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_attachment_e2e.rs","count":14},{"path":".worktrees/308-registry-process-lock/crates/spt/tests/webserve_cross_node_e2e.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/308-registry-process-lock/crates/\n\n## spt-store/\n\n### tests/\n#### serving_registry_two_process_int.rs\n 195│// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n*196│#[test]\n*197│fn loaded_reaper_serializes_registration_without_resurrecting_expiry() {\n 198│    overlap(\"reap\", false);\n 199│}\n 200│\n    │...\n 202│// [int->REQ-WEB-SERVING-REGISTRY] [int->REQ-WEB-ATTACHMENT-PULL]\n*203│#[test]\n*204│fn later_reap_preserves_a_registration_published_while_it_was_waiting() {\n 205│    overlap(\"register\", false);\n 206│}\n 207│\n 208│// [int->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE]\n*209│#[test]\n*210│fn abrupt_holder_exit_releases_the_sentinel_for_the_waiting_reaper() {\n 211│    overlap(\"register\", true);\n 212│}\n\n### src/\n#### serving.rs\n  877│\n *878│    fn registry(home: &TempDir) -> ServingRegistry {\n  879│        ServingRegistry::load_at(&crate::perch::serving_registry_file_in(home.path())).unwrap()\n  880│    }\n  881│\n *882│    fn source(home: &TempDir, relative: &str) -> PathBuf {\n  883│        let path = home.path().join(relative);\n  884│        fs::create_dir_all(path.parent().unwrap()).unwrap();\n  885│        fs::write(&path, b\"original\").unwrap();\n     │...\n  888│\n *889│    fn reload(home: &TempDir, store: &ServingRegistry) -> ServingRegistry {\n  890│        store.save_at(&crate::perch::serving_registry_file_in(home.path())).unwrap();\n  891│        registry(home)\n  892│    }\n     │...\n  896│    // [unit->REQ-WEB-SERVING-REGISTRY]\n *897│    #[test]\n *898│    fn lifetime_and_audience_survive_storage_for_every_entry_kind() {\n  899│        let home = tempdir().unwrap();\n  900│        let mut store = registry(&home);\n  901│        for (name, kind) in [\n     │...\n  944│    // [unit->REQ-WEB-SERVING-REGISTRY]\n *945│    #[test]\n *946│    fn first_bare_then_suffix_before_final_extension_per_family() {\n  947│        let home = tempdir().unwrap();\n  948│        let mut store = registry(&home);\n  949│        for (relative, expected) in [\n     │...\n  962│    // [unit->REQ-WEB-SERVING-REGISTRY]\n *963│    #[test]\n *964│    fn persisted_assignment_retains_identity_and_next_suffix() {\n  965│        let home = tempdir().unwrap();\n  966│        let mut store = registry(&home);\n  967│        let first = store.add_reference(&source(&home, \"one/report.md\"), None, Some(\"writer\"), 7).unwrap();\n     │...\n  977│    // [unit->REQ-WEB-SERVING-REGISTRY]\n *978│    #[test]\n *979│    fn retired_names_remain_not_found_after_restart_and_new_registration() {\n  980│        let home = tempdir().unwrap();\n  981│        let mut store = registry(&home);\n  982│        let first = store.add_reference(&source(&home, \"one/report.md\"), None, None, 1).unwrap();\n     │...\n  993│    // [unit->REQ-WEB-SERVING-REGISTRY]\n *994│    #[test]\n *995│    fn retired_names_return_only_to_their_prior_paths_without_rewinding_suffixes() {\n  996│        let home = tempdir().unwrap();\n  997│        let first_path = source(&home, \"one/report.md\");\n  998│        let second_path = source(&home, \"two/report.md\");\n     │...\n 1014│    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1015│    #[test]\n*1016│    fn a_changed_kind_cannot_reclaim_a_paths_prior_name() {\n 1017│        let home = tempdir().unwrap();\n 1018│        let path = source(&home, \"report\");\n 1019│        let mut store = registry(&home);\n     │...\n 1037│    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1038│    #[test]\n*1039│    fn same_absolute_path_preserves_entry_even_after_source_disappears() {\n 1040│        let home = tempdir().unwrap();\n 1041│        let path = source(&home, \"report.md\");\n 1042│        let mut store = registry(&home);\n     │...\n 1051│    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1052│    #[test]\n*1053│    fn explicit_names_and_source_names_share_one_allocator() {\n 1054│        let home = tempdir().unwrap();\n 1055│        let mut store = registry(&home);\n 1056│        let explicit = store.add_reference(&source(&home, \"original.txt\"), Some(\"report.md\"), None, 1).unwrap();\n     │...\n 1065│    // [unit->REQ-WEB-SERVING-REGISTRY]\n*1066│    #[test]\n\n## spt/tests/\n### webserve_attachment_e2e.rs\n  16│//!\n *17│//! ## The arms (one `#[test]`, sequential — `SPT_HOME` is process-global)\n  18│//!\n  19│//! send with an attachment → the ATTACHED line names the URL · fetch it back\n  20│//! byte-equal · the source EDITED after the send does not change what is served\n    │...\n  54│/// below: leaving a daemon behind is what made a fixed port unsafe.\n *55│fn rig_docs_port() -> u16 {\n  56│    static PORT: std::sync::OnceLock<u16> = std::sync::OnceLock::new();\n  57│    *PORT.get_or_init(|| {\n  58│        std::net::TcpListener::bind((\"127.0.0.1\", 0))\n    │...\n  72│impl Drop for DaemonReaper {\n *73│    fn drop(&mut self) {\n  74│        let out = run(&self.home, &[\"daemon\", \"stop\"], None);\n  75│        eprintln!(\n  76│            \"=== rig daemon stop === status={}\n    │...\n  89│\n *90│fn spt() -> PathBuf {\n  91│    PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"))\n  92│}\n  93│\n *94│fn run(home: &Path, args: &[&str], stdin: Option<&str>) -> Output {\n  95│    let mut cmd = Command::new(spt());\n  96│    cmd.no_window()\n  97│        .args(args)\n    │...\n 148│/// itself — a fetch failure and a port disagreement look identical from ARM 2.\n*149│fn bound_docs_port(home: &Path) -> u16 {\n 150│    let deadline = std::time::Instant::now() + Duration::from_secs(60);\n 151│    loop {\n 152│        let log = std::fs::read_to_string(spt_daemon::stderrlog::sink_path(home)).unwrap_or_default();\n    │...\n 177│/// over the surface.\n*178│fn http_get(port: u16, path: &str) -> (u16, String) {\n 179│    use std::io::{Read as _, Write as _};\n 180│    // Deliberately the SAME shape as `webserve_cross_node_e2e`'s `http()`, which\n 181│    // is proven against this very server: a read timeout, `Host` carrying the\n    │...\n 201│\n*202│fn stderr(out: &Output) -> String {\n 203│    String::from_utf8_lossy(&out.stderr).into_owned()\n 204│}\n 205│\n*206│fn stdout(out: &Output) -> String {\n 207│    String::from_utf8_lossy(&out.stdout).into_owned()\n 208│}\n 209│\n 210│/// The URL out of an `ATTACHED:<name>: <url> (…)` line.\n*211│fn attached_url(out: &Output) -> String {\n 212│    let text = stderr(out);\n 213│    let line = text\n 214│        .lines()\n    │...\n 223│/// EVERY url on the send's `ATTACHED:` lines, in order.\n*224│fn attached_urls(out: &Output) -> Vec<String> {\n 225│    stderr(out)\n 226│        .lines()\n 227│        .filter(|line| line.starts_with(\"ATTACHED:\"))\n    │...\n 237│/// \"one token names this message everywhere\".\n*238│fn last_msg_out_id(home: &Path, owner: &str) -> String {\n 239│    // Read through the io-log's OWN reader rather than opening a filename and\n 240│    // parsing lines here. The rig used to do the latter against `io.log`; the\n 241│    // file is `io-events.log` (`iolog::IO_LOG_FILE`, whose doc calls itself\n    │...\n 270│// what proves ONE token rather than two that happen to agree.\n*271│#[test]\n*272│fn an_attachment_is_snapshot_served_fetched_back_and_named_by_its_message() {\n 273│    let home = tempfile::tempdir().expect(\"home\");\n 274│    let home = home.path();\n 275│    let work = tempfile::tempdir().expect(\"work\");\n### webserve_cross_node_e2e.rs\n  46│//!\n *47│//! ## The arms (one `#[test]`, sequential — `SPT_HOME` is process-global)\n  48│//!\n  49│//! byte-equal fetch · edit visible (no cache) · HEAD · Range → 206 · owner's\n  50│//! 404 carried as 404 · WEB deny rule on the owner → 403 naming the surface ·\n    │...\n  84│\n *85│fn now_ms() -> u64 {\n  86│    std::time::SystemTime::now()\n  87│        .duration_since(std::time::UNIX_EPOCH)\n  88│        .expect(\"clock\")\n    │...\n  92│/// Run `f` with the process-global `SPT_HOME` pointed at `home`. Sound only\n *93│/// because this binary holds exactly ONE `#[test]` (KNOWN-HAZARDS 5.19).\n *94│fn in_home<T>(home: &Path, f: impl FnOnce() -> T) -> T {\n  95│    std::env::set_var(\"SPT_HOME\", home);\n  96│    f()\n  97│}\n  98│\n *99│fn ready_pid(path: &Path) -> Option<u32> {\n 100│    let s = std::fs::read_to_string(path).ok()?;\n 101│    serde_json::from_str::<serde_json::Value>(&s)\n 102│        .ok()?\n    │...\n 107│\n*108│fn kill_pid(pid: u32) {\n 109│    #[cfg(windows)]\n 110│    let _ = Command::new(\"taskkill\")\n 111│        .no_window()\n    │...\n 128│impl DaemonGuard {\n*129│    fn start(home: &Path, spt: &Path, tag: &str) -> DaemonGuard {\n 130│        let ready = home.join(\"brain.ready\");\n 131│        let _ = std::fs::remove_file(&ready);\n 132│        let log = home.join(format!(\"brain-{tag}.stderr.log\"));\n    │...\n 171│    /// durable sink `daemon run` redirects into (where the announcements land).\n*172│    fn stderr(&self) -> String {\n 173│        let mut text = std::fs::read_to_string(&self.log).unwrap_or_default();\n 174│        text.push_str(&std::fs::read_to_string(spt_daemon::stderrlog::sink_path(&self.home)).unwrap_or_default());\n 175│        text\n    │...\n 179│    /// `DOCS_SERVER_UP: http://localhost:<port>/<node>/` line.\n*180│    fn docs_port(&self) -> u16 {\n 181│        let deadline = Instant::now() + Duration::from_secs(60);\n 182│        loop {\n 183│            let log = self.stderr();\n    │...\n 201│\n*202│    fn reap_pids(&mut self) {\n 203│        if let Some(p) = self.brain_pid.take() {\n 204│            kill_pid(p);\n 205│        }\n    │...\n 212│    /// Stop this daemon now (the \"owner gone\" arm), scoped exactly as drop is.\n*213│    fn stop(&mut self) {\n 214│        let mut cmd = Command::new(&self.spt);\n 215│        cmd.no_window()\n 216│            .args([\"daemon\", \"stop\"])\n    │...\n 227│impl Drop for DaemonGuard {\n*228│    fn drop(&mut self) {\n 229│        if self.broker.is_some() || self.brain_pid.is_some() {\n 230│            self.stop();\n 231│        }\n    │...\n 236│/// key, a hermetic daemon config, and membership in the rig subnet.\n*237│fn seed_home(home: &Path) -> String {\n 238│    in_home(home, || {\n 239│        let node = spt_store::nodeid::load_or_create_at(&perch::identity_dir().join(\"node.key\"))\n 240│            .expect(\"node identity\")\n    │...\n 254│/// The running daemon's OWN dialable address, asked of the daemon itself.\n*255│fn own_addr(home: &Path) -> (String, serde_json::Value) {\n 256│    in_home(home, || {\n 257│        let deadline = Instant::now() + Duration::from_secs(60);\n 258│        loop {\n    │...\n 278│/// inside it (the arm that times one asserts its own bound).\n*279│fn http(port: u16, method: &str, path: &str, extra: &[(&str, &str)]) -> (u16, String, Vec<u8>) {\n 280│    let mut stream = TcpStream::connect((\"127.0.0.1\", port)).expect(\"connect serving listener\");\n 281│    stream.set_read_timeout(Some(Duration::from_secs(120))).unwrap();\n 282│    stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();\n    │...\n 296│\n*297│fn header(headers: &str, name: &str) -> Option<String> {\n 298│    headers\n 299│        .lines()\n 300│        .find_map(|line| line.strip_prefix(&format!(\"{name}: \")))\n    │...\n 305│/// the single registry writer, so this goes through the real verb.\n*306│fn serve_add(spt: &Path, home: &Path, source: &Path, name: &str, bound_port: u16, other_port: u16) {\n 307│    let mut cmd = Command::new(spt);\n 308│    cmd.no_window()\n 309│        .args([\"serve\", \"add\", source.to_str().unwrap(), \"--as\", name])\n    │...\n 339│/// whether a sender may author a `user-msg` at all.\n*340│fn typed_perch(home: &Path, id: &str, state: &str) {\n 341│    let perch = home.join(\"owlery\").join(id);\n 342│    std::fs::create_dir_all(&perch).expect(\"perch dir\");\n 343│    spt_store::info::write_info(\n    │...\n 357│/// green before the thing it measures has happened is not a meter.\n*358│fn seed_a_registry(home: &Path, node_b: &str) {\n 359│    use spt_net::net::registry::SubnetRegistry;\n 360│    let reg_dir = home.join(\"identity\").join(\"registry\");\n 361│    std::fs::create_dir_all(&reg_dir).expect(\"A's registry dir\");\n    │...\n 375│/// exactly the property arm (iii) varies.\n*376│fn send_as(\n 377│    spt: &Path,\n 378│    home: &Path,\n 379│    as_id: &str,"},"isError":false,"timestamp":1789522980901}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gbMGsAmhqTSl6YPX5xaWrk10|fc_0cdf94810c888815016aa9f437c16087d18b693fc980af6a10","toolName":"yield","startedAt":"2026-09-16T01:43:28.002Z"},"id":"d2aa72ce","parentId":"9c008a1e","timestamp":"2026-09-16T01:43:28.002Z"}
{"type":"message","id":"ca17dcbc","parentId":"d2aa72ce","timestamp":"2026-09-16T01:43:28.003Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Checking lock test coverage**\n\n**Finalizing source review**\n\n**Reviewing test coverage**"},{"type":"toolCall","id":"call_gbMGsAmhqTSl6YPX5xaWrk10|fc_0cdf94810c888815016aa9f437c16087d18b693fc980af6a10","name":"yield","arguments":{"type":null,"data":{"overall_correctness":"correct","explanation":"No blocking findings: crates/spt-store/src/serving.rs:149-163 uses an fs2-supported write-only handle and a stable appended .lock sentinel; crates/spt-daemon/src/servehost.rs:147-267 and livehost.rs:1282-1336 acquire before loading, fail closed, retain guards through snapshot side effects/publication, and preserve explicit-save/no-op behavior, with no remaining concurrent production-writer gaps found. Source coverage includes the fail-closed unit at servehost.rs:635-651, all three process regressions at crates/spt-store/tests/serving_registry_two_process_int.rs:193-212, and guarded live-daemon fixtures at crates/spt/tests/webserve_attachment_e2e.rs:666-731 and webserve_cross_node_e2e.rs:661-775; the independent contention witness in tests/fixtures/serving_registry_worker.rs:31-58 deterministically rejects a holder with only its OS-lock acquisition disabled, while the documented storage-API-versus-production-schedule limitation remains accurate. traceable-reqs.toml:7749-7752 is active without the obsolete PREP-ONLY comment, documentation correctly requires both broker and brain upgrades, and this was exclusively a text-source review: no files changed and no builds, tests, linters, analyzers, or validation commands run.","confidence":0.95,"findings":[]},"error":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":7647,"output":838,"cacheRead":78592,"cacheWrite":0,"totalTokens":87077,"reasoningTokens":516,"cost":{"input":0.07647000000000001,"output":0.0419,"cacheRead":0.078592,"cacheWrite":0,"total":0.196962}},"stopReason":"toolUse","timestamp":1789522980913,"responseId":"resp_0cdf94810c888815016aa9f4251f5487d1899df2d1b7db276d","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0cdf94810c888815016aa9f426d40887d1a89f60f09fb5f5e0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqfQ3S0we57Tf7q9F9gc1U00Ki36tB62MIMjJeD_qByCMICnjMM-gUCM90ffYkeI9p8w-FHGUWWy8PKVkgyV1qNUiUOZfVfcJffqET0jN5VCZcQkvvdZ8ioCD6zXENpL9zqjH47fl8DzX9bNJ08htsMcSxQAn7rozO983v146GCUNjoy2ABJUiuM9SHZqVrHaH0bwtca9QPUg3TV4w-XSQfEsHR6VJISjW7GZcqeHddOAeDdtcdD8exppf2QJt5ppbfVTPVDkeduRF6ADCiDiq03KbTJBa9ZyAd6GuG6rZEb9Q1JwBM24Jl4QeXi4xBRyYMSvWeVQgX1lbQ7cJUln7Vhf5Pfy6-9mO7OU3sYD8hZkBpLrPsveZ0fTX81ncjp0f_mZztVjpsv7ewDplwvvLCfG8Y2LPwLccSBLbmSEPmUa88DcbMkODJDIaaJtgqjlavcm0asdr151aHgP0iRP2oBwdJ7tj1TthiH5gzXOLrkiOJvnRVyysHv0VIr0edEPZSmzeLFHKhKF2dtSKv6Gm802MdzqjrwPMgTV4tKMi0XBqMdHxRrvhREsSm4XvNfwd_bE_FYbUAWMu6hXjeAlhUI7B3Xy5_TtmmH7tSf6eMqjdYsuoCImoOCdUf---VYwzfUEN3ktQssKOcUSlNzevomOOS80JeOsXnuGiUpCLDJ_Lw0El6ES9pvJ20WaUkepFeBEeZ5r9y0GtI-SnPBpHs6kuF4VDZjJveGuS-HCLrxCb8x9G3IaxvaOu0g5nBc_rNi5lY1_FnBTK95uknMY8NjXgB8oC0k6gxlb3JaBNjCfBb1Kc5M-JJxhyzKM8Z4YJRleBFy904_gz98k3Ze76Gb-uhYowBBSg6pTNGWhGQs0p_o4Sz57PUHov1DZwjdyUlSws1ji_eQ5Tu3Xzful6Z17WhLI3ZUfAAd_1-eWAa1bAVyF1huS54OviXRDQVbFdydBiMHp_OjDPsGH9Yy89m5sygcPjcibho6fDN4NpbOhHHbOfb8dvDJyOi82jSvIwAbFAVORv_MVPpgHXnlxNqID7Jy_DzCGyfKiVoKhIg3xRjiHqP2_rPmp7ApcAApbqMl1TZmSKT4LF-WEcE0kxmJ6DaJk6pV3PPI9ZHX2O3sI4rBlqtNr9TAOq14TEz3iKnNrXmqzHJPhl9wyh7f3ZOXJU9YNo41BaavZzG--3FhemSnnyQodH2T9O_u53Q4Csvv5rsLcIRbDnN9sFORHfW9SGkYCeRncPVxkC59dld3h5RAnGfXV6UKoeskZOXz5E4VwDTJzMa8WvXH20o44hMgQRsYp09Tdpp-apLkOOfvIV88SFsp2X51FYH1whDRUldut58I7b15wQP53OQD2RjFH54_4Zw8DQ2cvmsnFNJQujwhuDIfbdIINKKibNPB6nn95p3ATjSxR33krvKBZaELiuvJXnfuZ4l6TDZnPh2rhZf2ZlHYzoi19OC8otiI6-wlkmShgud_EywgyLfLxX2mnLJTVHM0Ah-AwMiO48Opqh9G1oynSaO5trtqOVdro5t-QDbP0jWwriVfUjuush1ogtLiAiQJ8LOmBgVq-IhL24mReQHGAyJgwADa5-fDRxrEH9qhJ8SNIbAteJnBegLFqKE0W-v69UFS-0ICt__A9L0cvwPqknZgmKXKhv2mGoZd60YAbdWo5IDk0lFsRYmCljVUhbtXgPO4YWfLGHue3Eoqx8fWBoroXIzvYwVqt4JAPRu_D8WmDlt59TDbHMnRHU2K2tAaq4M6xslG19kdizSMBpF2cGAbJoQynA7Yl8sLgIGT179_4QZEfTc3gWKcOtIqjbvsgDgx163xLDqgqVQP7pfY37owej4pubu7K9KueNwHYIe0nqxyPcddddY8ylX2uoWJpxStTYHrRKTwFIfnKUdxeuH9w6GysDYlMQprpgrJUdftTdV1xgs_eyFMf50vrBvKnrXT6rzyuzrB0rhuxgtcw_rqUAX7fut0O2ZODGwvtsWSyh-_hihxu2cG1Kd22lQH9e0xv7QXZyu2ws6xSWU--Y5vTEDAresshGbUf0jU_1RrScIX1HYjJJeheHrESAKWcO_ZzRvac_jusb_S90e-Syt5czzENNnVX9rYYmSmrgiwyDeYaOqjOJ1fvfgZYop6nbWl8j672ZFQsbgufpKQoghHmiENLk1mZD8pck4e3_CIO9Ed-5cdN09ppAurTTKcU9vXdZFxd_83MuUCenv-bZSlmTXWN6ox-fC7QmEdMiCv_l7ylyTybeZu-E5KCiyuzm6EZ1kTGi77vYUqleNm0BTf6AZmgoL2FO_rsxUFSMnrPzLAhbBHa1XEfR2U_U0ITFFXVCRyOONKURMj4lyoDCZxpktXRKA8vVZSxCG38rD9lmNhDr7bYTtMSSw5TK_ezw9ayNUyzA6etIIt19RJKTzgFx0BwjrUsYiwwYnJlkVG9_cZR3BbREGph-Kr7k72vpkzytJoGYmu1mF9fmfLfrshYvdCaFrIV5ret2zufDDlyUHq6emzr05YnehMOIYnHI5TdYA9pypE07x3RGcsWDFzMZvxS6yzDe-tacjRk0hcMdq0BcpCY7VYRfucJIFZfJ4-xDnDC31MlkgxCsB-Y0O7EGNND8xL67HfeToY_gzRjWKT5jd80gVwFhopUVgD15DEHWx5PxVJ7Wr2xjfSQAxDYHSFEAeCdFMjyNoae1YPNnmsS_XTz94UQMy_HmP4iBdOFgtKrpoJhYURPdF8oziOwgGA36U5Pj9D44fUYc-b_n0lWpCtO8YJaUDDTtrHap2csPN7uYeHPaoLLHgO-jkg14_J2NPQZKSsuocKxPzK5N29JgCLc7ejocSwx8R47v8u_tWsd0qEVVvqWkUCl-yT3cLTre5kVMIwZfgJC52XvDlOIqATiJxYa29xFx4GJ1jG40TpC6CTz8m0f20bJvB22otitPp7U8w6Ut0fZHOYAnuIuwDm5T4iulfU94eEsGjZIscq62Gbk8jIOHeUOElAqBNsNlMT6APIEJCcNRsWts8H1iU6PI8NBXSvH7x6-fyOKIRqglTEdu0F0hggOEinwNwFfpjsYXApeYQ95bAwSBI_EOddmUe1yecqcdg3Y6S9vj0nPiMzVYW_YWPBzrjPyiDNpNHcdhFLjok_g4i05cwRjQaoIOTi7xzc6Ags4ruoC7KsTdX36yyc_gwdbi6-NS4YLtw9Oi-IpUFV4ZCSZoc4b3QBWJR2aK8s2N7VGBuJyPaX5IX2MI_sFXGRpMrSBsCvI7p4t7YX9XbOGnuH9lJow6baOssuIztoWwfDS2-NWovcmWyLEHWRQ_0sGk0M6CMjd18RaJ9u1YTuMmqxQ54epGjs5CRFMBAEXbHBgmYiquJSVxnDVU7LiVDK8MUJj-JhBFvAB1tr9W691Eyj35MhzlY6mcXXkFTL9WVQJLziqHUSDVUfOKXCvcWgrwTi4c7zUGGgqOCdDOn9t66IeDPC5zxb7xhkGfTxzoCVjgcU5Kyf3aSCcCfJ8nBh9PyiFrTpUfRivuOMCGz_ogPc02_-AWYbVinKTalf3ASmj2FmbkXhAOc-LGrDjuT0zkjQO_z_0Alf6o4HyPmq3WexAlCQBazRla2agoqLHr7ywtVtSejwo1Q4eElSxjiScb52rHDW7hkyXddSK_bkwhXbtVxC6XK9eZjujCp6e0f8Q6MBwqF4o7PG7W5s7JNZ8Au60YRY6dJOZ6BPNiDuM9Fi5eIvZVGhyxeznI4MaUmtEg67cdIdcQWPJr-cmuBfX0pCLd59JB76T3eUYu3B7UylpxVcUKW1tkJYxpf1uhgBwgVV4I6M8M3HlkG06uyrHUA7KA6k8xR0DIinyoPYt7Y5Ptzq6QunsLRotIkMJ_PXQu-BTVfRTS55PGMeLId9M3MkZuYJmFxhNdCRA3UYnkMxFaK0p46gWwmqS0BQl8Ei_xssfVSMK_TgKJUJsKLspDCbz3aN8Y0_99J5_9sgdjNJCgoUxa2y-1-2kUsMXoPcgVOAJg27a6yIFTjr0SgTfeJJ5nNY4Ae07HGso5uNhsYsSyWafcYaTW8GB3j_kOo64og1owWRL0o2zRYk39TfL4NtZJ3aU177_HyJpYGGkFs1bCIfJuIQ1hsgfU0NmlQrWa8bogdo0HcuWqV40-v-iJknu6cjF1QITmhFjwhH2P5j-J0EAO6nDGkymR2XF7lKO7_EdJwASbrkYCI5aj7eDA9it7SR8mJ9viTtcJRJP6Q0i-Kc5UDSsfia6mXkTZ4XLQuBmzC07qcZ84ParC35s4ZGIR0_6OgGu4FPJD2W43SPqB_eqhFkqEQ3VQ4s6XT885IVzx798xSuDglCfZn7iqr4r8e9Hhk_TbiHqBMnGrAtS_ob-guk70mSMbAbgWvT9rVMTevCe3yWgJg9tUr0NbPbIG4uNfp8ADGVKgV7izg_hBSkJg7reYm49CDzz7DKuMeqVWuM1m2hUbC1dIjRBabpEQj_B6fJXKuEM3gI6nmQMeg64k9I-A_aUT0wkmO106iETFQTLbmlAi8rJH624nW7ley8OSLxi5BqqiDyy6VPuCwsQOjZTYMwh-oqBqA7yBN1s5VBjgGlTnt-JOXNmjeZg_faIeEWKxoJbH0LhgMYeNYa5FFArAm_lKFL-kCiujgkONUBDrKIABVZyDguwsyL-4HyWhBCzguBBMWMhM3FcGRLCZYICeHRHWwTeJntirMEP3ae-1XcA0NyToSKR8IsepJfoX9wvcxK_5i03EpHoF2k7OAwJUhW2-tvTjZ5EvqVRjDYuqwa1xCUYbwuyir3f1KrLxaJJABv9OR-Ni7IkWg94aAa1KX5AX-A_zZ9U-fNNLMM_2qQo_nYHaiZSeg2MTr4MkXXR_bPczA6gJdR9s4Tb6E3FPIeQG9xzcK7ZMehk4_40HGc1BKpkHIUBUOGtdy-SrMkaR7ZjdAaRcJTIFntJN_WodYyRYcVr_bWsFoKxjKCdDyVqi3l5yQiVvH7eGDBViaRQ5SOdvdZO0G7AzHnPS4-BmWu4Jw==","internal_chat_message_metadata_passthrough":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"summary":[{"type":"summary_text","text":"**Checking lock test coverage**"},{"type":"summary_text","text":"**Finalizing source review**"},{"type":"summary_text","text":"**Reviewing test coverage**"}],"metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}},{"id":"fc_0cdf94810c888815016aa9f437c16087d18b693fc980af6a10","type":"function_call","status":"completed","arguments":"{\"type\":null,\"data\":{\"overall_correctness\":\"correct\",\"explanation\":\"No blocking findings: crates/spt-store/src/serving.rs:149-163 uses an fs2-supported write-only handle and a stable appended .lock sentinel; crates/spt-daemon/src/servehost.rs:147-267 and livehost.rs:1282-1336 acquire before loading, fail closed, retain guards through snapshot side effects/publication, and preserve explicit-save/no-op behavior, with no remaining concurrent production-writer gaps found. Source coverage includes the fail-closed unit at servehost.rs:635-651, all three process regressions at crates/spt-store/tests/serving_registry_two_process_int.rs:193-212, and guarded live-daemon fixtures at crates/spt/tests/webserve_attachment_e2e.rs:666-731 and webserve_cross_node_e2e.rs:661-775; the independent contention witness in tests/fixtures/serving_registry_worker.rs:31-58 deterministically rejects a holder with only its OS-lock acquisition disabled, while the documented storage-API-versus-production-schedule limitation remains accurate. traceable-reqs.toml:7749-7752 is active without the obsolete PREP-ONLY comment, documentation correctly requires both broker and brain upgrades, and this was exclusively a text-source review: no files changed and no builds, tests, linters, analyzers, or validation commands run.\",\"confidence\":0.95,\"findings\":[]},\"error\":null}","call_id":"call_gbMGsAmhqTSl6YPX5xaWrk10","internal_chat_message_metadata_passthrough":{"create_time":1789522981.828846,"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"},"name":"yield","metadata":{"turn_id":"635bd661-7fc2-4cd4-9888-bf2411fab563"}}]},"duration":27086.407600000035,"ttft":1943.4580999999307,"completedAt":1789523008001,"contextSnapshot":{"promptTokens":86239,"nonMessageTokens":13543,"compactionEpoch":0}}}
{"type":"message","id":"1c1706f5","parentId":"ca17dcbc","timestamp":"2026-09-16T01:43:28.006Z","message":{"role":"toolResult","toolCallId":"call_gbMGsAmhqTSl6YPX5xaWrk10|fc_0cdf94810c888815016aa9f437c16087d18b693fc980af6a10","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"overall_correctness":"correct","explanation":"No blocking findings: crates/spt-store/src/serving.rs:149-163 uses an fs2-supported write-only handle and a stable appended .lock sentinel; crates/spt-daemon/src/servehost.rs:147-267 and livehost.rs:1282-1336 acquire before loading, fail closed, retain guards through snapshot side effects/publication, and preserve explicit-save/no-op behavior, with no remaining concurrent production-writer gaps found. Source coverage includes the fail-closed unit at servehost.rs:635-651, all three process regressions at crates/spt-store/tests/serving_registry_two_process_int.rs:193-212, and guarded live-daemon fixtures at crates/spt/tests/webserve_attachment_e2e.rs:666-731 and webserve_cross_node_e2e.rs:661-775; the independent contention witness in tests/fixtures/serving_registry_worker.rs:31-58 deterministically rejects a holder with only its OS-lock acquisition disabled, while the documented storage-API-versus-production-schedule limitation remains accurate. traceable-reqs.toml:7749-7752 is active without the obsolete PREP-ONLY comment, documentation correctly requires both broker and brain upgrades, and this was exclusively a text-source review: no files changed and no builds, tests, linters, analyzers, or validation commands run.","confidence":0.95,"findings":[]},"status":"success"},"isError":false,"timestamp":1789523008004}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-09-16T01:50:28.049Z"},"id":"c514e21e","parentId":"1c1706f5","timestamp":"2026-09-16T01:50:28.049Z"}
