---
phase: 16-vrchat-osc-bridge
plan: "02"
subsystem: driver
tags: [job-object, watchdog, graceful-exit, named-pipe, daemon-lifecycle, steamvr, 90hz]

# Dependency graph
requires:
  - phase: 16-01
    provides: beyond_backglow_ctl.exe daemon binary with PipeClient + OSC server
  - phase: 14-usb-serial-foundation-and-led-control
    provides: InitBackglow, LedController, named pipe server, DeviceProvider skeleton
  - phase: 15-wifi-ddp-fallback-and-transport-selection
    provides: transport selection (USB/DDP), degraded-state gating

provides:
  - Windows Job Object tethered daemon spawn from DeviceProvider::InitBackglow success path
  - 3-phase graceful shutdown (stdin-EOF -> 250ms wait -> Job Object close) in Cleanup
  - Watchdog thread with exponential respawn backoff (1s/2s/4s, cap 3)
  - Dedicated inbound daemon pipe (\\.\pipe\beyond_backglow_daemon) avoiding FlushFileBuffers hang
  - LedController cv-wait retuned 33ms -> 11ms for 90 Hz daemon pipe traffic

affects: [16-03, 16-04, driver-lifecycle]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Job Object kill-on-close: CreateJobObject + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + AssignProcessToJobObject ensures daemon is tethered to vrserver lifetime"
    - "Dedicated daemon pipe: PIPE_ACCESS_INBOUND + PIPE_NOWAIT avoids FlushFileBuffers block on fire-and-forget 90 Hz writers"
    - "Breakaway-or-retry spawn: CREATE_BREAKAWAY_FROM_JOB first, retry without on ERROR_ACCESS_DENIED (enterprise Job Object nesting)"
    - "Absolute path via GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) prevents DLL hijack on daemon spawn"

key-files:
  created: []
  modified:
    - src/driver/device_provider.h
    - src/driver/device_provider.cpp
    - src/led/led_controller.cpp
    - src/backglow_ctl/pipe_client.h
    - src/backglow_ctl/pipe_client.cpp

key-decisions:
  - "Dedicated daemon pipe (beyond_backglow_daemon) rather than reusing CLI pipe: FlushFileBuffers on CLI pipe blocks server_main at 90 Hz fire-and-forget writes, tripping vrserver 12s watchdog"
  - "PIPE_ACCESS_INBOUND + PIPE_NOWAIT on daemon pipe: daemon only writes, driver only reads; non-blocking ReadFile in RunFrame bounded at 32 messages/tick"
  - "StopBackglowDaemon called BEFORE ShutdownAllOff in Cleanup: daemon must stop issuing pipe writes before LedController pipe server tears down"
  - "SpawnBackglowDaemon only on InitBackglow success path, never in degraded-disabled state (D-02)"
  - "LedController cv-wait 33ms -> 11ms (D-26): writer thread must service 90 Hz daemon pipe traffic without starvation"

patterns-established:
  - "Daemon pipe poll: PollDaemonPipe() drains up to 32 messages per RunFrame; bounded to prevent server_main overtime"
  - "Watchdog thread owns respawn: single long-lived thread waits on hProcess, handles exit code + uptime reset + backoff + respawn cap"
  - "3-phase stop: stdin-close (graceful signal) -> bounded WaitForSingleObject(250ms) -> Job Object close (kernel kill)"

requirements-completed: [VRCH-01]

# Metrics
duration: 95min
completed: 2026-04-19
---

# Phase 16 Plan 02: Driver Lifecycle Summary

**Windows Job Object tethered daemon spawn with 3-phase graceful stop, watchdog respawn backoff (1s/2s/4s cap 3), and dedicated inbound daemon pipe eliminating FlushFileBuffers server_main hang at 90 Hz**

## Performance

- **Duration:** ~95 min (across multiple agent sessions)
- **Started:** 2026-04-19T19:00:00Z (approx)
- **Completed:** 2026-04-19T22:32:00Z
- **Tasks:** 4 (including 1 checkpoint verified on hardware)
- **Files modified:** 5

## Accomplishments

- Daemon spawned on every SteamVR launch via DeviceProvider::InitBackglow success tail; pid visible in vrserver.txt within 300ms of transport online
- Job Object kill-on-close verified on hardware: force-killing vrserver terminates daemon within 2s (VRCH-01b green)
- Graceful stop verified: vrmonitor exit triggers `Backglow: daemon stopped gracefully` (VRCH-01c green)
- Root-cause bug found and fixed: dedicated daemon pipe eliminates FlushFileBuffers hang that caused vrserver 12s watchdog abort (dde0824)
- Zero watchdog timeout errors in UAT session log

## Task Commits

1. **Task 1: Retune LedController cv-wait 33ms -> 11ms** - `0b103a4` (fix)
2. **Task 2: Add daemon spawn + watchdog + stop infrastructure** - `ffe758a` (feat)
3. **Task 3: Wire InitBackglow + Cleanup call sites + deploy** - `506ead0` (feat)
4. **Deviation fix: Dedicated daemon pipe (FlushFileBuffers root cause)** - `dde0824` (fix)
5. **Task 4: On-hardware smoke gate (VRCH-01a + VRCH-01b)** - verified on hardware (no code commit)

## Files Created/Modified

- `src/driver/device_provider.h` - Added Phase 16 daemon lifecycle members (m_hBackglowJob, m_hBackglowProcess, m_hBackglowStdinWrite, m_backglowWatchdogThread, m_bStopBackglowWatchdog, m_backglowRespawnCount, m_backglowSpawnTime) + SpawnBackglowDaemon/StopBackglowDaemon/BackglowWatchdogThreadFunc declarations; also added dedicated daemon pipe members (m_hDaemonPipe, m_bDaemonPipeClientConnected) + CreateDaemonPipeServer/PollDaemonPipe/DestroyDaemonPipeServer
- `src/driver/device_provider.cpp` - SpawnBackglowDaemon (Job Object, breakaway-or-retry, absolute path, stdin pipe, watchdog launch), StopBackglowDaemon (3-phase shutdown), BackglowWatchdogThreadFunc (respawn backoff), CreateDaemonPipeServer/PollDaemonPipe/DestroyDaemonPipeServer (PIPE_ACCESS_INBOUND + PIPE_NOWAIT, 32-msg/tick drain); call sites at InitBackglow success tail and Cleanup
- `src/led/led_controller.cpp` - cv-wait retuned 33ms -> 11ms at line 78 (D-26 90 Hz)
- `src/backglow_ctl/pipe_client.h` - Default pipe name changed to `\\.\pipe\beyond_backglow_daemon`
- `src/backglow_ctl/pipe_client.cpp` - CreateFileA access mask reduced to GENERIC_WRITE (PIPE_ACCESS_INBOUND semantics)

## Decisions Made

- **Dedicated daemon pipe instead of reusing CLI pipe:** The CLI pipe (`\\.\pipe\beyond_proximity_ctl`) uses request/response mode with `FlushFileBuffers + DisconnectNamedPipe` per command. `FlushFileBuffers` on a named pipe blocks until the client reads the response. The daemon writes fire-and-forget at 90 Hz and never reads — so the first daemon message hung `server_main` inside `FlushFileBuffers` for the full duration, tripping vrserver's 12s watchdog. Fix: dedicated `\\.\pipe\beyond_backglow_daemon` with `PIPE_ACCESS_INBOUND + PIPE_NOWAIT`, no response, no per-message disconnect.
- **StopBackglowDaemon before ShutdownAllOff:** The daemon must stop issuing pipe writes (via the daemon pipe) before the LedController server-side pipe tears down. Without this ordering, a write arriving after `DestroyDaemonPipeServer` would hit a closed handle.
- **SpawnBackglowDaemon on success path only (D-02):** If transport init fails (COM port missing, DDP unreachable), backglow enters degraded-disabled state. Spawning the daemon in degraded state would waste resources and create an orphan with no pipe server to connect to.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Dedicated daemon pipe to fix FlushFileBuffers server_main hang**
- **Found during:** Task 3 / post-deploy UAT (first attempt)
- **Issue:** CLI pipe (`\\.\pipe\beyond_proximity_ctl`) uses request/response with `FlushFileBuffers + DisconnectNamedPipe` per command. `FlushFileBuffers` blocks until client reads response. Daemon writes fire-and-forget at 90 Hz and never reads — first daemon write hung `server_main` inside `FlushFileBuffers`, tripping vrserver's 12s watchdog. Observed in vrserver.txt: `Failed Watchdog timeout in thread server_main in BeyondProximity after 11.958746 seconds`.
- **Fix:** New pipe `\\.\pipe\beyond_backglow_daemon` with `PIPE_ACCESS_INBOUND` (daemon writes only), `PIPE_NOWAIT` (non-blocking server ReadFile), multi-message (no per-message disconnect), no response written. `PollDaemonPipe()` drains up to 32 messages per RunFrame tick. Daemon-side `PipeClient::Connect` default updated to new pipe; `CreateFileA` access mask changed to `GENERIC_WRITE` to match `PIPE_ACCESS_INBOUND` semantics (GENERIC_READ|GENERIC_WRITE on an inbound pipe returns ERROR_ACCESS_DENIED).
- **Files modified:** `src/driver/device_provider.h`, `src/driver/device_provider.cpp`, `src/backglow_ctl/pipe_client.h`, `src/backglow_ctl/pipe_client.cpp`
- **Verification:** UAT session shows zero watchdog timeout lines; `Backglow: daemon pipe client connected` and `Backglow: daemon stopped gracefully` present.
- **Committed in:** `dde0824`

---

**Total deviations:** 1 auto-fixed (Rule 1 — Bug)
**Impact on plan:** The fix is the core correctness requirement for VRCH-01. Without it, every SteamVR launch would trip the watchdog and force the driver into safe-mode. No scope creep — all changes directly address the FlushFileBuffers root cause.

## Safe-Mode State

**Location:** `C:/Program Files (x86)/Steam/config/steamvr.vrsettings`
**Field:** `driver_BeyondProximity."blocked_by_safe_mode"`
**Current value:** `false`

The prior UAT failure (watchdog abort) had set this to `true`. It was cleared (manually or by SteamVR on successful subsequent launch) before this UAT session. The value was confirmed `false` before launching SteamVR for UAT.

## UAT Evidence (vrserver.txt excerpts)

Session 1 (22:30 UTC-7, fresh SteamVR launch):
```
Sun Apr 19 2026 22:30:34.562 [Info] - BeyondProximity: Backglow: transport=usb ddp_host='' ceiling=50 com_port='COM11'
Sun Apr 19 2026 22:30:34.866 [Info] - BeyondProximity: Backglow: online via USB on COM11
Sun Apr 19 2026 22:30:34.885 [Info] - BeyondProximity: Backglow: daemon spawned (pid=44236)
Sun Apr 19 2026 22:30:34.885 [Info] - BeyondProximity: Backglow: daemon pipe created \\.\pipe\beyond_backglow_daemon
Sun Apr 19 2026 22:30:35.180 [Info] - BeyondProximity: Backglow: daemon pipe client connected
```

VRCH-01a: PASS — `Backglow: daemon spawned (pid=44236)` present.
VRCH-01a Phase 14/15 regression: PASS — `Backglow: online via USB on COM11` present.

VRCH-01b (force-kill vrserver → daemon terminated, vrmonitor relaunched vrserver with new daemon):
```
Sun Apr 19 2026 22:31:12.837 [Info] - BeyondProximity: Backglow: transport=usb ddp_host='' ceiling=50 com_port='COM11'
Sun Apr 19 2026 22:31:13.142 [Info] - BeyondProximity: Backglow: online via USB on COM11
Sun Apr 19 2026 22:31:13.145 [Info] - BeyondProximity: Backglow: daemon spawned (pid=47884)
Sun Apr 19 2026 22:31:13.145 [Info] - BeyondProximity: Backglow: daemon pipe created \\.\pipe\beyond_backglow_daemon
Sun Apr 19 2026 22:31:13.425 [Info] - BeyondProximity: Backglow: daemon pipe client connected
```

VRCH-01b: PASS — original daemon (44236) was terminated by Job Object kill-on-close when vrserver exited; new vrserver spawned fresh daemon (47884) confirming kill-on-close worked. `Get-Process beyond_backglow_ctl` showed pid=47884 (not 44236) after vrserver restart.

VRCH-01c (graceful stop — vrmonitor force-killed):
```
Sun Apr 19 2026 22:31:30.376 [Info] - BeyondProximity: Backglow: daemon stopped gracefully
Sun Apr 19 2026 22:31:30.376 [Info] - BeyondProximity: Backglow: daemon pipe destroyed
```

VRCH-01c: PASS — `Backglow: daemon stopped gracefully` present.

Watchdog timeout check: PASS — `grep 'Failed Watchdog timeout.*BeyondProximity'` returned empty across full vrserver.txt.

## Issues Encountered

- **FlushFileBuffers hang on first UAT attempt** (prior agent session): Daemon connected to CLI pipe (`\\.\pipe\beyond_proximity_ctl`) which uses request/response semantics. `FlushFileBuffers` blocked `server_main` for 11.96s, triggering vrserver watchdog abort and setting `blocked_by_safe_mode=true`. Root cause identified and fixed in `dde0824` (dedicated daemon pipe). Documented above under Deviations.

## User Setup Required

None — no external service configuration required.

## Next Phase Readiness

- VRCH-01 fully closed: spawn, Job Object kill-on-close, watchdog respawn, graceful stop all verified on hardware
- Plan 16-03 (OSCQuery / VRChat parameter bridge) can now rely on the daemon being present whenever SteamVR is running
- Daemon pipe infrastructure is stable and ready for 16-03 to extend the OSC parameter handling

---
*Phase: 16-vrchat-osc-bridge*
*Completed: 2026-04-19*
