.omo/evidence/omo-senpi-adapter/20260722-config-watch-rejection/notes.md
Change: packages/omo-senpi/src/components/config-watch/ — stop the
synchronous re-registration recursion on config-watch:rejected and stop
emitting watch targets that senpi's config-reload host deterministically
rejects.
Root cause: when cwd is under $HOME, the ancestor walk emitted a bare-$HOME
creation target. That target covers the senpi agent dir's protected paths
(<agent>/auth.json, sessions/, logs/), so senpi's restricted-target check
rejected the registration EVERY time. The omo REJECTED handler re-emitted
config-watch:register synchronously on the same stack, recursing with senpi's
synchronous rejection until RangeError: Maximum call stack size exceeded.
RangeError: Maximum call stack size exceeded after 7,954 synchronous
re-registrations. See emit-sequence-before.log.emit-sequence-after.log.bun test packages/omo-senpi/src/components/config-watch ->
29 pass / 0 fail (3 new + 1 updated case in index.test.ts, 2 new cases in
paths.test.ts). See test-and-typecheck.log.tsgo --noEmit -p packages/omo-senpi/tsconfig.json ->
exit 0. See test-and-typecheck.log.New/updated unit cases:
<home>/.senpi/agent/{auth.json,sessions,logs}; the bare-HOME
ancestor creation target is dropped while cwd..work ancestors stay watched.SENPI_CODING_AGENT_DIR under HOME -> covering ancestor
dropped; existing tests pin the agent dir OUTSIDE home so the full
cwd-to-home walk stays covered.retry budget exhausted warning and no stack overflow.{"result": "crashed", "error": "RangeError: Maximum call stack size exceeded.", "registerCount": 7954}.{"result": "survived", "registerCount": 4} — sequence
REGISTER#1..#4 only; each retry fires on its own macrotask
(setTimeout(0)), so the host's synchronous rejection can never recurse.The recursion had exactly two ingredients, and both are now pinned by tests: the omo handler never re-registers on the rejection's own stack (deferred + capped retry), and the resolution never emits a target the senpi host must reject deterministically (restricted-path filter). The unit suite covers the wire behavior against the same fake-event harness the pre-existing tests use; the repro driver covers the real component end to end against the exact host behavior that caused the crash. No live senpi session is required: the host contract exercised (synchronous deterministic REJECTED) is the documented senpi-side behavior and is reproduced verbatim by the fake host.
The first pushed revision scheduled the deferred retry with
setTimeout(0) + retryTimer.unref(). On the Windows CI runner (Bun
1.3.12) the unref'd one-shot timer never fired, so the first async test
awaiting the deferred emit hung 61 minutes until the job was cancelled
(attempt-1 step log: four config-watch tests pass, then silence from
06:47:26Z to cancellation at 07:48:29Z). macOS and Ubuntu were unaffected.
Fix: drop .unref() — a 0ms retry timer is self-draining and dispose clears
it, so unref bought nothing. The three async tests also carry a 10s per-test
timeout now, so any future starvation fails fast instead of hanging the job.
test-and-typecheck.log re-captured after this fix; repro driver output
unchanged (survived, 4 register emissions).