---
name: worker-perch-leak-diagnosis
description: "2026-07-06 worker-perch leak diagnosed — 3 core bugs (stop-no-destroy, sid-drop/token-only auth, no orphan reap) + adapter token-discard gap; operator ruled token flow OUT; filed to doyle for bugfix milestone; P-1 psyche_dir fill gap killing flynn's psyche"
metadata: 
  node_type: memory
  type: project
  originSessionId: 92f4e8f1-7e75-4907-a410-923bc13d4448
---

**Worker-perch leak (operator /diagnose 2026-07-06, HFENDULEAM core 0.25.0 / adapter 0.14.1).** Symptom: flynn's CC Task subagents left 6 OFFLINE worker perches in `spt endpoint list`. Diagnosed by manual verb-loop (start/stop a probe worker under flynn's live sid). All 6 + probe purged (`endpoint purge --force` = the cleanup lever); list clean.

Findings (filed to doyle for immediate triage → followup bugfix milestone):
- **W-1 core: worker-stop doesn't destroy the record** — token-authed WORKER_STOPPED rc=0 left info.json byte-identical + still listed. Symptom root; even perfect adapter leaves rows.
- **W-2 core: registration drops the sid; auth is token-only** — worker-start AUTHS --session-id but stores `""`; same sid → AUTH_REFUSED at stop; only the start's stdout `token=` works. Asymmetric with all sibling id-scoped verbs.
- **W-3 core: no orphan reap** — dead-pid workers persist forever; SubagentStop-never-fires kill-paths guarantee leaks without reap/TTL.
- **Adapter gap (mine, HELD):** worker-start stdout token discarded; AUTH_REFUSED at stop silently swallowed (non-strict env.spt); missing-agent_id stop = silent no-op. NOT building token capture — **operator RULED tokens out of the worker verbs** (too much adapter-dev burden; sid-symmetric auth). Under that ruling the adapter's current emission becomes contract-correct; remaining adapter slice = **loud-refusal + loud-skip logging**, cut AFTER doyle's triage settles verb shapes.

Key evidence details: leaked worker ids = CC agent_ids (17-hex `a…`); worker record pids = ephemeral hook-caller pids (all distinct, all dead); session_id `""` all records. My own session subagents ride LEGACY owl (perri-w4 working perch, torn down correctly) — couldn't repro in-session; probed the verbs directly instead.

**P-1 RESOLVED — was MINE, not core's (doyle verdict + v0.14.2 SHIPPED 2026-07-06 @5975254, node applied):** `{psyche_dir}` was NEVER in the F-030 freeze (retired resident-era key; freeze fills exactly {id, session_id, parent_session_id, psyche_context_file, node, subnet}) — my manifest carried `cwd="{psyche_dir}"` over from the retired psyche_init shape into BOTH psyche tables → every per-event turn faulted at substitution (flynn psyche dead). **Fix: cwd templates + psyche_dir keys dropped from both tables; SHIM chdirs to {psyche_context_file} PARENT dir** (= the psyche's nested perch dir, observed `owlery/<id>/nested/<id>-psyche/psyche-context.txt`) — sid+cwd transcript pair stays stable. My PSYCHE-EPHEMERAL-PLAN.md D3 draft had stale psyche_dir — plan≠freeze; ALWAYS diff manifest keys against the SHIPPED freeze block, not the plan. RC gap: exercised shim contract directly, never the daemon fill path. Core adds REQ-ADAPTER-TEMPLATE-KEY-VALIDATION (add/update refuses non-catalog keys) — psyche_init gate table pre-cleared for it. No-latch confirmed (doyle: pulse loop re-fires, manifest hot-reloads pre-tick, success clears stamp). **Post-fix watch fired: reason CHANGED → P-1 fix confirmed live (turns spawn, hot-reload proven), but P-2 minted: PSYCHE TURNS RUN UNAUTHENTICATED IN THE DEFAULT CONFIG ROOT.** flynn (ccs profile) transcript sits in ~/.claude (default root, NOT logged in on this box) — every turn answers "Not logged in · Please run /login" → exit 1 → Generic keep-custody strike loop, no reseed (session exists; auth absent). The ccs wrapper sets CLAUDE_CONFIG_DIR at PARENT launch only; daemon spawns the psyche shim bare → default root. Perch record CAPTURES read_env.CLAUDE_CONFIG_DIR (direction="read" seam) but it never reaches psyche spawn env. SessionEnd-hook-cancelled stderr = RED HERRING (benign on fast print-mode exits, repro'd exit-0). **P-2 RULED (doyle): CORE GAP, adapter needs NOTHING** — F-027 Half-B env-parity (read_env captured → stamped → threaded identically to spawns) is design-frozen BINDING but was NEVER IMPLEMENTED (parked post-F-029); P-2 = first field casualty. REQ-PSYCHE-SPAWN-ENV-PARITY registered (WORKER-TRUTH P-2 wave, work-order addendum @c25039b): daemon threads record read_env stamps into psyche_resume spawn env, harness-agnostic. Rides AFTER wave-1 (todlando N-1+W-2 code-complete). Full F-027 Half B stays parked, flagged next operator triage. Interim: `claude /login` in default ~/.claude root heals flynn today (billing = operator call; doyle offers re-order if operator wants code sooner). Post-slice: re-run flynn watch as live proof. **Registration gate incoming (same WORKER-TRUTH release): adapter add/update REFUSES (a) spawned-role templates referencing non-catalog keys (never-spawned roles like psyche_init skipped; parse stays lenient — installs never brick) and (b) the same var in BOTH [env] direction="read" AND env_remove (contradictory). claude-spt 0.14.2 pre-checked clean on both (env_remove = OWL_SESSION_ID/SPT_AGENT_ID, disjoint from [env] CLAUDE_CONFIG_DIR-read/SPT_ENDPOINT_ID-inject). Manifest-authoring rules from here: every spawned-role {key} must be catalog-real; keep env_remove disjoint from [env].** Debug ritual that cracked it: probe matrix (project cwd / nested cwd / no-config-root / resume-vs-fresh) + READ THE PSYCHE TRANSCRIPT (psyche turns DO leave transcripts — in whatever root they ran under; find the sid across ALL roots).

**WORKER-TRUTH triage (doyle @247333c, docs/NEXT-MILESTONE-WORKERTRUTH-TRIAGE.md, 6 REQ seeds):** W-1 = soft-stop record retention DELIBERATE (fix = list visibility + reap, not hard delete); W-2 root = worker.rs:44 hardcodes session_id="" (presented sid auths parent, never stored); W-3 reap = parent-session lifecycle + TTL (record pid = hook pid, dead by design — never a liveness signal). Riders: workers leave default endpoint list (--workers reveals), leave run picker, core-minted {parent}-w{N} names.

**MIGRATION SHIPPED: core v0.27.0 published (counter 47, @cb4a5ab) + adapter v0.15.0 SHIPPED same day (@2605b1a, tag Latest, byte-verified, REQ-DIST-WORKER-LIFECYCLE impl+unit, floor 0.27.0). NODE HELD at adapter 0.14.2 until box takes core 0.27.0 (operator window) — 0.15.0's verbs don't exist on box's 0.25.0. POST-BOUNCE WINDOW discharges FOUR proofs: (1) P-2 flynn psyche live-proof (0.27.0 stamps captured read_env → transcript should land in bigscreen root, stamp clears), (2) E-1 from-stamp check on first doyle exchange, (3) worker live int (Task subagent visible under `endpoint list --workers` during, GONE after — the leak scenario with destroy+reap; also worker_reap_ttl_secs 24h default), (4) v0.15.0 field verify. Also in 0.27.0: registration refuses non-catalog keys/unterminated-{/env-contradiction (0.14.2 pre-verified clean in doyle's gate).**

**Original frozen wave-1 verb shape (implemented above):** worker-start `<parent> --session-id <sid> [--agent-id][--agent-type]`, NO worker id positional (BREAKING: old form = loud clap error), NO token; **minted id rides STDOUT bare (empty = refusal), human line on STDERR — parse stdout**. worker-stop/poll `<minted-id> --session-id <sid>`, sid-symmetric (stored registration sid OR parent current sid — /clear covered). **Adapter slice (READY TO CUT on doyle's core-release ping + version number):** capture minted id → persist agent_id→minted-id in adapter state (stop hook only gets CC agent_id) → stop by minted id → loud-refusal/loud-skip logging → floor bump to shipping core → release TRAILS core (parked-work discipline). Banked skew: new-core × ≤0.14.2 adapter = loud worker-start fail, workers just unregistered (non-fatal). Related: [[v0260-core-wave]], [[psyche-ephemeral-redesign]].
