diff --git a/bridged/docs/CB-307-Push-Loop.md b/bridged/docs/CB-307-Push-Loop.md index faa4f2a..934528d 100644 --- a/bridged/docs/CB-307-Push-Loop.md +++ b/bridged/docs/CB-307-Push-Loop.md @@ -78,9 +78,15 @@ A `ReplyPushLoop` component, notified at the single no-waiter call site - **Ack = drain.** The primary draining (`drainReplies` = peek + ack) is the acknowledgement. The loop's **stop condition is `inbox.peek(target).isEmpty()`** — the reply is gone from the inbox because it was acked. No new `bridge_ack` tool needed for v1 (see Increment 3). -- **Status-gated injection.** Never inject mid-turn. Reuse the `Injector`/`StatusPoller` - discipline: deliver only when the primary's terminal samples **injectable** (IDLE/BLOCKED), - one message per turn. **Readiness caveat (below).** +- **Status-gated injection (mechanism (b), chosen).** A dedicated lightweight scheduled loop, + **not** the worker `Injector`. It injects via `AgentControl.send(primaryTerminal, nudge)` + (the same herdr `agent.send` = `pane send-text` + submit that delivers to workers) only when + `AgentControl.status(primaryTerminal).injectable()` (IDLE/BLOCKED) — never mid-turn. This keeps + the primary path fully isolated from `WorkerPresence`/`StatusPoller` (which are worker-scoped), + and makes it unit-testable with a fake `AgentControl` + an injected clock (per the CB-306 + `LongSupplier` clock + `Runnable` sleeper seam). Rejected (a) reuse-the-Injector: it would force + the primary terminal into the worker poller set and couple to worker-presence semantics — more + integration surface, harder to test, no real gain for a bounded reminder. - **Bounded reminder / backoff.** While `peek(target)` stays non-empty, re-inject on a backoff schedule up to a cap (N reminders or a max duration; config `primary.push_reminders` / `primary.push_backoff_ms`). After the cap, **stop reminding** — @@ -102,16 +108,15 @@ in v1; the stop-on-empty loop is sufficient. and needs no new env var or argument (identity stays connection-derived, per the existing `BridgeMcp` invariant). -2. **Readiness-gate mismatch.** The existing `Injector` gates delivery on +2. **Readiness-gate mismatch → dedicated loop.** The existing `Injector` gates delivery on `ready.test(target)` = `WorkerPresence` (the *worker's* MCP connected). The primary is not - in `WorkerPresence`. So the primary push path uses a **different liveness signal**: the - primary is provably MCP-connected at the moment it calls us (that's how we learned its - terminal), and we still status-gate on its terminal sampling **injectable** via the poller. - Concretely, the primary push path either (a) uses a dedicated `Injector` instance whose - `ready` predicate is "primary terminal is known" (always true once registered), or - (b) a lighter direct `AgentControl.send` guarded by a `StatusPoller` injectable sample. - Decision: **(a)** — reuse the `Injector` queue/one-per-turn/backoff machinery with a - primary-appropriate `ready` predicate, rather than reimplement gating. + in `WorkerPresence`, so reusing `Injector` would mean forcing the primary terminal into the + worker `StatusPoller` set and swapping the `ready` predicate — extra integration surface with + worker-scoped machinery. Decision: **mechanism (b)** — a small dedicated scheduled loop that + calls `AgentControl.status(primaryTerminal).injectable()` then `AgentControl.send(...)`, with + an injected clock. Isolated from worker presence, trivially unit-testable, sufficient for a + bounded reminder. (Verified live: this primary resolves to `term_656c8cc03e1f0b1`, pane + `w2:pY` — the primary genuinely runs in a herdr pane on this host, so the path is exercisable.) ## Boundary note