---
quick: 260513-63n
title: Wire TCP wake into psyche-wrapper inner poll via --once argv
type: quick
status: complete
date: 2026-05-13
commits:
  - db08bf5
files-modified:
  - src/live/wrapper/mod.rs
line-delta: "+39 / -1"
tests-added:
  - "src/live/wrapper/mod.rs::tests::poll_psyche_argv_contains_once_flag"
requirements:
  - REQ-WAKE-01
  - REQ-WAKE-02
---

# Quick 260513-63n: Wire TCP wake into psyche-wrapper inner poll — Summary

## One-Liner

Added `"--once"` to the psyche-wrapper inner poll subprocess argv so inbound TCP communes (and echo-commune spool writes) to the `<psyche_id>` perch drain to the wrapper's iteration loop within seconds instead of being buffered in the captured stdout pipe until the 20-min `--pulse-interval` expiry.

## Root Cause Recap

The wrapper spawns `owl poll <psyche_id> listen --psyche --pulse-interval <N>` via `Command::output()` (Unix) / `win_spawn::spawn_capture_no_inherit` (Windows) — both APIs read stdout to EOF after child exit. Under `once=false` (the default), the inner poll IS woken by OS-level TCP readiness (`libc::poll` / `WSAPoll` via `common::listener::poll_ready`), prints the message to stdout, but **stays in the loop** — so stdout sits in the OS pipe buffer until the child eventually exits at pulse-deadline. The `--once` flag flips the inner poll into exit-on-first-delivery at `src/owl/poll.rs:419-422` (and parallel arms at `:481-484`, `:277-280`, `:289-292`), draining the buffered stdout to the wrapper's `Command::output()` immediately.

## Files Changed

| File | Lines | Delta | Description |
|---|---|---|---|
| `src/live/wrapper/mod.rs` | 346-353 (production), 614+ (test) | +39 / -1 | Argv literal `[&str; 6]` → `[&str; 7]` with `"--once"` as final element; new `poll_psyche_argv_contains_once_flag` regression unit test. |

No other files modified.

## Tests Added

- **`poll_psyche_argv_contains_once_flag`** (unit, in `src/live/wrapper/mod.rs::tests`) — Pure-string contract check: constructs the exact 7-slot argv that `poll_psyche` builds using fixed `psyche_id = "t-psyche"` and `period_str = "1200"`, then asserts each slot by index (`args[6] == "--once"`). Independent of `WrapperState` construction and `select_pulse_period` mutations. Codifies the contract so any future refactor that reverts to `[&str; 6]` or drops `--once` fails this test first.

## Verification Results

| Gate | Command | Result |
|---|---|---|
| New regression test | `cargo test --lib live::wrapper -- poll_psyche_argv_contains_once_flag` | ✅ pass |
| Wrapper module suite | `cargo test --lib live::wrapper` | ✅ 25 passed / 0 failed |
| Full lib suite (serialized) | `cargo test --lib -- --test-threads=1` | ✅ 221 passed / 0 failed |
| Release build | `cargo build --release` | ✅ clean (3 pre-existing warnings unchanged) |

### Note on parallel-run failures (out of scope)

`cargo test` (default parallel) shows 13 failures in `pick_spec`, `cleanup`, `commune`, `context`, `signoff`, `owlery`, and one wrapper D-10 filter test. These are the documented pre-existing SPT_HOME parallel-test race class (see Phase 18.7 P01 decision: "process-global env race would clobber concurrent tests") — they all disappear under `--test-threads=1` and are unrelated to this argv change. Out of scope per the executor SCOPE BOUNDARY rule.

## Deviations from Plan

**None.** The plan was executed exactly as written:

- Argv literal bumped from `[&str; 6]` to `[&str; 7]` with `"--once"` appended (Action step 1-2).
- No other lines in `poll_psyche` touched — `Command::args(args)`, `win_spawn::spawn_capture_no_inherit`, `OWL_UNDER_WRAPPER` env passing, `Stdio::piped`, stderr trim, and the exit-code-2 / `HANDOFF_DEFER:` branch all preserved (Action step 3).
- Regression test appended after the four existing `select_pulse_period_*` tests and the two Plan 02 contract tests; pure-string slice construction with literal `"t-psyche"` and `"1200"`, not using `mk_state` (Action step 4).
- No `build_psyche_poll_argv` helper extracted (Action step 5).
- `src/owl/poll.rs` not modified (Action step 6).

## Authentication Gates

None.

## Self-Check

- ✅ Production line `"--once",` present in `src/live/wrapper/mod.rs` at the `poll_psyche` argv literal.
- ✅ Test line `args[6], "--once"` assertion present in `src/live/wrapper/mod.rs::tests::poll_psyche_argv_contains_once_flag`.
- ✅ Commit `db08bf5` found in `git log`.
- ✅ No other files modified in commit (`git show --stat db08bf5` shows 1 file changed).
- ✅ No deletions in commit (`git diff --diff-filter=D HEAD~1 HEAD` empty).

## Self-Check: PASSED

## Behavior Note (additive only)

This fix is **purely additive at the argv layer** — one slot bump, one new flag string, one new test. It changes no error paths, no handoff semantics, no Phase 18.4 wrapper-state.json schema, no Phase 18.5 exit-code-2 contract, no Phase 18.7 listener-owned timed-pulse firing, and no Phase 18.8 echo-commune cursor/excerpt logic. It does not interact with the `MAX_CONSECUTIVE_EMPTY_EXITS=3` empty-stdout backstop (every `--once` exit produces non-empty stdout: either a delivered message or the `PULSE_TRIGGER (ts)` body). UAT is deferred to user post-deploy per project convention: trace recipe is RESEARCH Cycle 4 — send `$OWL deliver <psyche_id> ...` to a running psyche wrapper and verify the message reaches `psyche.md` / next resume within seconds (not bounded by 20-min `--pulse-interval`).
