feat(rmp): full status-page reportability for distributed-quorum reviews #9

Merged
hibryda merged 7 commits from feat/rmp-distributed-leg-observability into main 2026-06-03 11:33:01 +02:00
Owner

Brings the distributed production mode to full status-page reportability parity with the in-process quorum. Operating distributed previously showed only the MAIN active-job state — the progress-line frontier, ETA, per-leg Quorum column, and token/cost ribbon were all blind.

Gaps closed (src/rmp/worker-bridge.ts + src/engine-telemetry.ts)

  • Progress line + ETA. The distributed module's phase vocabulary (distributing/arbitrating) isn't in the serving PHASE_ORDER, so the ledger couldn't position it (no frontier) and PHASE_ORDER.includes → null ETA. The bridge sink now folds module phase keys onto the serving taxonomy (distributing/arbitratingsdk-streaming, parsing passes). Arbitration stays surfaced in the Quorum column (PHASE_ORDER has no slot for a distinct arbitrating phase — same as in-process).
  • Per-leg A/B/arbiter Quorum column. makePerLegSink derives per-leg state from the leg-tagged stream and writes the SAME writeQuorumLegState/writeQuorumArbiterState hash the in-process quorum uses. Explicit think/stream handling matches the in-process SessionSupervisor. distributed-quorum.ts emits a leg-done marker on each leg's terminal capture.
  • Token/cost ribbon. The legs (+ arbiter) stream in separate containers → the engine's in-process observeSdkMessageForTelemetry tap never sees them. New recordRmpTokenUsage/recordRmpRateLimit fold cross-process usage (from the parsed {t:"telemetry"} events) into the same ribbon counters. No double-count: the arbiter doesn't tap the ribbon directly and no in-process reviewer runs in distributed mode.
  • Callbacks typed SessionState/ArbiterState (guarded arbiter parse) → no writer cast. Closes both prior review findings (think fall-through + the unchecked casts).

Tests

+5 (phase-mapping ⊂ PHASE_ORDER, ribbon-feed delta, the two new engine-telemetry recorders); full suite 2148; typecheck clean. This PR is itself reviewed via the live distributed path.

Brings the distributed production mode to **full status-page reportability parity** with the in-process quorum. Operating distributed previously showed only the MAIN active-job state — the progress-line frontier, ETA, per-leg Quorum column, and token/cost ribbon were all blind. ## Gaps closed (`src/rmp/worker-bridge.ts` + `src/engine-telemetry.ts`) - **Progress line + ETA.** The distributed module's phase vocabulary (`distributing`/`arbitrating`) isn't in the serving `PHASE_ORDER`, so the ledger couldn't position it (no frontier) and `PHASE_ORDER.includes` → null ETA. The bridge sink now folds module phase keys onto the serving taxonomy (`distributing`/`arbitrating` → `sdk-streaming`, `parsing` passes). Arbitration stays surfaced in the Quorum column (PHASE_ORDER has no slot for a distinct arbitrating phase — same as in-process). - **Per-leg A/B/arbiter Quorum column.** `makePerLegSink` derives per-leg state from the leg-tagged stream and writes the SAME `writeQuorumLegState`/`writeQuorumArbiterState` hash the in-process quorum uses. Explicit `think`/`stream` handling matches the in-process `SessionSupervisor`. `distributed-quorum.ts` emits a leg-done marker on each leg's terminal capture. - **Token/cost ribbon.** The legs (+ arbiter) stream in separate containers → the engine's in-process `observeSdkMessageForTelemetry` tap never sees them. New `recordRmpTokenUsage`/`recordRmpRateLimit` fold cross-process usage (from the parsed `{t:"telemetry"}` events) into the same ribbon counters. No double-count: the arbiter doesn't tap the ribbon directly and no in-process reviewer runs in distributed mode. - Callbacks typed `SessionState`/`ArbiterState` (guarded arbiter parse) → no writer cast. **Closes both prior review findings** (`think` fall-through + the unchecked casts). ## Tests +5 (phase-mapping ⊂ PHASE_ORDER, ribbon-feed delta, the two new engine-telemetry recorders); full suite **2148**; typecheck clean. This PR is itself reviewed via the live distributed path.
feat(rmp): distributed quorum drives the per-leg status-page Quorum column
All checks were successful
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 22s
pr-reviewer CI / vitest (pull_request) Successful in 1m0s
pr-reviewer CI / tsc build (pull_request) Successful in 36s
18ffb5e73b
The distributed path drove the MAIN active-job state but not the per-leg
A/B/arbiter column (only the in-process quorum did) — a distributed review
showed no per-leg visibility.

- distributed-quorum.ts: consumeLegEvent emits a leg-done `round` marker
  (leg-delivered / leg-died) on each leg's terminal capture (the terminal is
  consumed by the orchestrator, not re-emitted, so the serving layer had no
  delivered/died signal).
- worker-bridge.ts: makePerLegSink wraps the watchdog sink, deriving per-leg
  state from the leg-tagged stream (tool activity → tool-executing, other
  activity → streaming, the leg-done markers → delivered/died) + the arbiter
  leg's `arbiter:<state>` → the arbiter state. New onLegState/onArbiterState ctx
  callbacks.
- worker.ts: the distributed branch wires them to writeQuorumLegState /
  writeQuorumArbiterState — the SAME hash the in-process quorum writes, so the
  status page's Quorum column renders for distributed reviews unchanged.

+test (per-leg + arbiter state derivation); full suite 2143.

Superseded by round 2.

Show previous round

hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer)

Round 1 — head 18ffb5e73b4f, base main, trigger opened

TL;DR: CONDITIONAL_APPROVE — verified and kept unique-to-B minor (think→streaming divergence from LivenessWatchdog) and unique-to-A info (string-typed callbacks with as-casts); no blocking issues.

Summary

Both findings are unique-to-one-reviewer but verified against the actual source. A (info, kept): worker-bridge.ts lines 148/150 confirmed — onLegState and onArbiterState callbacks are typed as string; worker.ts lines 3125/3136 confirmed — both cast with as SessionState / as ArbiterState. No runtime bug today but zero compile-time protection against future state-string divergence. B (minor, kept): worker-bridge.ts lines 177–184 confirmed — the bare else branch catches kind:"think" events and forces state = "streaming" / tool = null, diverging from LivenessWatchdog at liveness-watchdog.ts line 185 which explicitly documents kind:"think" → progress only, no state change. If a leg emits a think event while in tool-executing (possible with interleaved assistant text), the status-page Quorum column flashes streaming and drops the tool name mid-execution. Two verified, independent findings kept; no findings dropped.

Blast Radius

Changes are behind the RMP_DISTRIBUTED flag, limited to worker-bridge.ts (new makePerLegSink wrapper + extended DistributedReviewDriverCtx) and a single call site in worker.ts. The distributed quorum path is additive; the in-process quorum and default flag-off path are entirely unaffected.

BLAST_SCORE: 3/10

Risk Indicators

Indicator Value
Sensitive functions writeQuorumLegState, writeQuorumArbiterState
Migration touched
Test delta +36 / -1 lines in test files
Dependency changes

CI status (head 18ffb5e73b4f)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / tsc --noEmit (pull_request) pending details
pr-reviewer CI / vitest (pull_request) pending details
pr-reviewer CI / tsc build (pull_request) pending details

Findings (2)

[MINOR] kind: "think" activity falls through to streaming branch, diverging from LivenessWatchdog semantics

src/rmp/worker-bridge.ts:181

In makePerLegSink the inner conditional for event.t === "activity" is:

if (event.kind === "tool") {
  state = "tool-executing";
  tool = event.detail ?? null;
} else {                   // ← line 181 — catches "think" too
  state = "streaming";
  tool = null;
}

LivenessWatchdog at liveness-watchdog.ts line 185 explicitly documents // kind:"think" → progress only, no state change, and its observeInner switch leaves this.state and this.activeTool untouched for think. The current else branch silently maps a think event to streaming and clears tool. If a leg emits think while in tool-executing (e.g. interleaved assistant-text before the tool_result arrives), the status-page Quorum column flashes streaming and loses the tool name mid-execution.

Fix: replace the bare else with an explicit else if (event.kind === "stream") and add a think no-op comment matching the watchdog:

if (event.kind === "tool") {
  state = "tool-executing";
  tool = event.detail ?? null;
} else if (event.kind === "stream") {
  // tool_result → back to streaming, clear active tool
  state = "streaming";
  tool = null;
}
// kind:"think" → no state change (mirrors LivenessWatchdog behaviour)

[INFO] onLegState / onArbiterState typed as string — unchecked as casts at call site

src/rmp/worker-bridge.ts:148

The two optional callbacks in DistributedReviewDriverCtx are typed (leg, state: string, ...) => void (lines 148–150). At the call site in worker.ts (lines 3125 and 3136) the strings are re-widened with as SessionState / as ArbiterState, which TypeScript treats as a no-op assertion — it won't catch a future state not in those unions.

All current values produced by the state machine ("streaming", "tool-executing", "delivered", "died") are valid SessionState members, and the arbiter detail originates from typed ArbiterState values, so there is no runtime bug today. But the as casts provide zero compile-time safety.

If the worker-bridge.tsworker.ts decoupling is intentional, a narrow string-literal union ("streaming" | "tool-executing" | "delivered" | "died") on the callback would give TypeScript something to check without importing from session-supervisor.ts. If decoupling is not a concern, importing and using SessionState / ArbiterState directly would eliminate the casts entirely.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 1 • 2 findings (1m/1i) • 2026-06-03T08:28:23.028Z → 2026-06-03T08:35:04.288Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 2._ <details> <summary>Show previous round</summary> <!-- hib-pr-reviewer round:1 --> ## hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer) **Round 1** — head `18ffb5e73b4f`, base `main`, trigger `opened` **TL;DR:** CONDITIONAL_APPROVE — verified and kept unique-to-B minor (think→streaming divergence from LivenessWatchdog) and unique-to-A info (string-typed callbacks with as-casts); no blocking issues. ### Summary Both findings are unique-to-one-reviewer but verified against the actual source. **A (info, kept):** `worker-bridge.ts` lines 148/150 confirmed — `onLegState` and `onArbiterState` callbacks are typed as `string`; `worker.ts` lines 3125/3136 confirmed — both cast with `as SessionState` / `as ArbiterState`. No runtime bug today but zero compile-time protection against future state-string divergence. **B (minor, kept):** `worker-bridge.ts` lines 177–184 confirmed — the bare `else` branch catches `kind:"think"` events and forces `state = "streaming"` / `tool = null`, diverging from `LivenessWatchdog` at `liveness-watchdog.ts` line 185 which explicitly documents `kind:"think" → progress only, no state change`. If a leg emits a `think` event while in `tool-executing` (possible with interleaved assistant text), the status-page Quorum column flashes `streaming` and drops the tool name mid-execution. Two verified, independent findings kept; no findings dropped. ### Blast Radius Changes are behind the `RMP_DISTRIBUTED` flag, limited to `worker-bridge.ts` (new `makePerLegSink` wrapper + extended `DistributedReviewDriverCtx`) and a single call site in `worker.ts`. The distributed quorum path is additive; the in-process quorum and default flag-off path are entirely unaffected. **BLAST_SCORE: 3/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `writeQuorumLegState`, `writeQuorumArbiterState` | | Migration touched | — | | Test delta | +36 / -1 lines in test files | | Dependency changes | — | ### CI status (head `18ffb5e73b4f`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/71/jobs/0) | | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/71/jobs/1) | | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/71/jobs/2) | ### Findings (2) #### **[MINOR]** `kind: "think"` activity falls through to `streaming` branch, diverging from LivenessWatchdog semantics _src/rmp/worker-bridge.ts:181_ In `makePerLegSink` the inner conditional for `event.t === "activity"` is: ```ts if (event.kind === "tool") { state = "tool-executing"; tool = event.detail ?? null; } else { // ← line 181 — catches "think" too state = "streaming"; tool = null; } ``` `LivenessWatchdog` at `liveness-watchdog.ts` line 185 explicitly documents `// kind:"think" → progress only, no state change`, and its `observeInner` switch leaves `this.state` and `this.activeTool` untouched for `think`. The current `else` branch silently maps a `think` event to `streaming` **and clears `tool`**. If a leg emits `think` while in `tool-executing` (e.g. interleaved assistant-text before the `tool_result` arrives), the status-page Quorum column flashes `streaming` and loses the tool name mid-execution. Fix: replace the bare `else` with an explicit `else if (event.kind === "stream")` and add a `think` no-op comment matching the watchdog: ```ts if (event.kind === "tool") { state = "tool-executing"; tool = event.detail ?? null; } else if (event.kind === "stream") { // tool_result → back to streaming, clear active tool state = "streaming"; tool = null; } // kind:"think" → no state change (mirrors LivenessWatchdog behaviour) ``` #### **[INFO]** `onLegState` / `onArbiterState` typed as `string` — unchecked `as` casts at call site _src/rmp/worker-bridge.ts:148_ The two optional callbacks in `DistributedReviewDriverCtx` are typed `(leg, state: string, ...) => void` (lines 148–150). At the call site in `worker.ts` (lines 3125 and 3136) the strings are re-widened with `as SessionState` / `as ArbiterState`, which TypeScript treats as a no-op assertion — it won't catch a future state not in those unions. All current values produced by the state machine (`"streaming"`, `"tool-executing"`, `"delivered"`, `"died"`) are valid `SessionState` members, and the arbiter detail originates from typed `ArbiterState` values, so there is no runtime bug today. But the `as` casts provide zero compile-time safety. If the `worker-bridge.ts` ↔ `worker.ts` decoupling is intentional, a narrow string-literal union `("streaming" | "tool-executing" | "delivered" | "died")` on the callback would give TypeScript something to check without importing from `session-supervisor.ts`. If decoupling is not a concern, importing and using `SessionState` / `ArbiterState` directly would eliminate the casts entirely. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 1 • 2 findings (1m/1i) • 2026-06-03T08:28:23.028Z → 2026-06-03T08:35:04.288Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
feat(rmp): full status-page reportability for distributed-quorum reviews
All checks were successful
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 22s
pr-reviewer CI / vitest (pull_request) Successful in 1m0s
pr-reviewer CI / tsc build (pull_request) Successful in 35s
b166ebfd71
Operating the distributed production mode showed only the MAIN active-job
state; the progress-line frontier, ETA, per-leg Quorum column, and token/cost
ribbon were all blind. Brings distributed parity with the in-process quorum.

- Progress line + ETA: the distributed module's phase vocabulary
  (distributing/arbitrating) isn't in the serving PHASE_ORDER, so the ledger
  couldn't position it (no frontier) and PHASE_ORDER.includes → null ETA. The
  bridge sink folds module phase keys onto the serving taxonomy
  (distributing/arbitrating → sdk-streaming, parsing passes) so both work as the
  single/in-process path. Arbitration stays in the Quorum column.
- Token/cost ribbon: the legs (+ arbiter) stream in separate containers, so the
  engine's in-process observeSdkMessageForTelemetry tap never sees them. New
  recordRmpTokenUsage / recordRmpRateLimit (twins of the SDK-message recorders,
  fed from the already-parsed {t:"telemetry"} RMP events) fold cross-process
  usage into the same ribbon counters — no double-count (the arbiter doesn't tap
  the ribbon directly; no in-process reviewer runs in distributed mode).
- Per-leg column: explicit think/stream handling (matches the in-process
  SessionSupervisor); callbacks typed SessionState/ArbiterState (guarded arbiter
  parse) so the writer needs no cast — closes the two PR-9 review findings.

+5 tests; full suite 2148; typecheck clean.
hibryda changed title from feat(rmp): distributed quorum drives the per-leg status-page Quorum column to feat(rmp): full status-page reportability for distributed-quorum reviews 2026-06-03 10:48:32 +02:00

Superseded by round 3.

Show previous round

hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer)

Round 2 — head b166ebfd7148, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 agreed finding (JSDoc orphan), verified and kept 2 unique findings (missing awaiting-rate-limit-reset transition, now-injection bypass); no blocking issues.

Summary

Arbitration — Round 2

Agreed finding (both A and B): The JSDoc block at lines 181–188 is displaced before ARBITER_STATES and DEFAULT_LEG_STATE, leaving makePerLegSink at line 203 with no attached doc. Verified by reading lines 181–207 directly: the multi-line JSDoc opens at 181, a separate single-line JSDoc at 189–190 precedes ARBITER_STATES, and makePerLegSink at 203 follows a blank line with no JSDoc. Kept as minor.

Unique to A (line 216/229–244): The leg-state machine handles only activity and round events; a telemetry event with rateLimit.throttled:true for a known leg bumps the ribbon counter but never transitions legState[leg].state to awaiting-rate-limit-reset. Verified by reading lines 229–244 (no telemetry branch present) and cross-checking LivenessWatchdog lines 154–159 (which does apply the transition) and SessionState at session-supervisor.ts lines 47–53 (which includes awaiting-rate-limit-reset). The gap is real and produces a display inconsistency under throttle. Kept as minor.

Unique to B (line 222): recordRmpRateLimit is called with new Date().toISOString() directly while DistributedReviewDriverCtx carries a now?: () => string injection hook. Verified by reading lines 178 (ctx.now) and 222 (hardcoded new Date()). makePerLegSink receives no now parameter, so the rate-limit timestamp stored in lastRateLimitAt is always wall-clock in tests. Low practical impact (no test currently asserts this timestamp), but it breaks the existing injection pattern established elsewhere in the same file. Kept as info.

Result: 1 agreed finding kept, 2 unique findings verified and kept. Total: 3 findings (2 minor, 1 info).

Blast Radius

The diff touches worker-bridge.ts (the coordination layer for the distributed-quorum production path), engine-telemetry.ts (shared ribbon counters read by the status page), and distributed-quorum.ts (the leg orchestrator). The distributed path is flag-gated (RMP_DISTRIBUTED=1) so blast radius is bounded to that mode; the in-process quorum is unchanged. Changes export new functions from engine-telemetry.ts that any module could call.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions recordRmpRateLimit, recordRmpTokenUsage, runDistributedReviewViaDriver
Migration touched
Test delta +113 / -3 lines in test files
Dependency changes

CI status (head b166ebfd7148)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / tsc --noEmit (pull_request) pending details
pr-reviewer CI / vitest (pull_request) pending details
pr-reviewer CI / tsc build (pull_request) pending details

Findings (3)

[MINOR] JSDoc for makePerLegSink is orphaned — attaches to ARBITER_STATES instead of the function

src/rmp/worker-bridge.ts:181

Lines 181–188 contain a /** … */ block describing makePerLegSink, but two declarations are interleaved before the function: ARBITER_STATES (line 191, with its own JSDoc at 189–190) and DEFAULT_LEG_STATE (line 198). TypeScript tooling and TypeDoc attach a doc-comment to the immediately following declaration, so the block at 181–188 is orphaned on ARBITER_STATES while makePerLegSink at line 203 has no attached documentation (it is preceded only by a blank line). Editor hover-docs and generated API docs for the function are lost.

Fix: move the JSDoc block to immediately above function makePerLegSink (line 203), after DEFAULT_LEG_STATE. The comment at line 189 already correctly documents ARBITER_STATES and can stay.

[MINOR] makePerLegSink leg-state machine has no telemetry branch — a throttled distributed leg never transitions to awaiting-rate-limit-reset

src/rmp/worker-bridge.ts:229

The state machine at lines 229–244 handles event.t === "activity" (→ tool-executing or streaming) and event.t === "round" (→ delivered/died), but has no telemetry case. When a distributed leg emits {t:"telemetry", rateLimit:{throttled:true}, leg:"a"}, the ribbon counter is correctly bumped at line 222, but legState[leg].state remains whatever it was (typically "streaming"), so the Quorum column for that leg never shows awaiting-rate-limit-reset.

The in-process path via LivenessWatchdog (liveness-watchdog.ts lines 154–159) does make this transition, and SessionState (session-supervisor.ts lines 47–53) explicitly includes "awaiting-rate-limit-reset". During a throttled distributed review the two paths diverge visually.

Suggested fix — add a telemetry branch inside the leg === "a" || leg === "b" block:

} else if (event.t === "telemetry" && event.rateLimit) {
  if (event.rateLimit.throttled && !(["delivered","died"] as SessionState[]).includes(state)) {
    state = "awaiting-rate-limit-reset";
  } else if (state === "awaiting-rate-limit-reset" && !event.rateLimit.throttled) {
    state = "streaming";
  }
}

This mirrors the LivenessWatchdog's telemetry→rate-limit handling and closes the display inconsistency.

[INFO] recordRmpRateLimit call uses new Date() directly, bypassing the now injection hook on DistributedReviewDriverCtx

src/rmp/worker-bridge.ts:222

Line 222: recordRmpRateLimit(event.rateLimit, new Date().toISOString()). DistributedReviewDriverCtx carries now?: () => string for test-clock injection (line 178), used by runDistributedQuorum and runReviewViaProducer, but makePerLegSink receives no now parameter so the rate-limit timestamp stored in lastRateLimitAt is always wall-clock — even in tests.

Practical impact is low today (no test currently asserts the rate-limit timestamp from this path), but it is an inconsistency with the established injection pattern in the same file.

Consider threading now through makePerLegSink:

function makePerLegSink(
  base: DriverSink,
  onLegState:  | undefined,
  onArbiterState:  | undefined,
  now: () => string = () => new Date().toISOString(),
): DriverSink {
  
  if (event.rateLimit) recordRmpRateLimit(event.rateLimit, now());

And pass ctx.now ?? (() => new Date().toISOString()) at the call site in runDistributedReviewViaDriver.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 2 • 3 findings (2m/1i) • 2026-06-03T08:48:06.212Z → 2026-06-03T08:53:51.957Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 3._ <details> <summary>Show previous round</summary> <!-- hib-pr-reviewer round:2 --> ## hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer) **Round 2** — head `b166ebfd7148`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 agreed finding (JSDoc orphan), verified and kept 2 unique findings (missing awaiting-rate-limit-reset transition, now-injection bypass); no blocking issues. ### Summary ## Arbitration — Round 2 **Agreed finding (both A and B):** The JSDoc block at lines 181–188 is displaced before `ARBITER_STATES` and `DEFAULT_LEG_STATE`, leaving `makePerLegSink` at line 203 with no attached doc. Verified by reading lines 181–207 directly: the multi-line JSDoc opens at 181, a separate single-line JSDoc at 189–190 precedes `ARBITER_STATES`, and `makePerLegSink` at 203 follows a blank line with no JSDoc. **Kept** as minor. **Unique to A (line 216/229–244):** The leg-state machine handles only `activity` and `round` events; a `telemetry` event with `rateLimit.throttled:true` for a known leg bumps the ribbon counter but never transitions `legState[leg].state` to `awaiting-rate-limit-reset`. Verified by reading lines 229–244 (no telemetry branch present) and cross-checking `LivenessWatchdog` lines 154–159 (which *does* apply the transition) and `SessionState` at session-supervisor.ts lines 47–53 (which includes `awaiting-rate-limit-reset`). The gap is real and produces a display inconsistency under throttle. **Kept** as minor. **Unique to B (line 222):** `recordRmpRateLimit` is called with `new Date().toISOString()` directly while `DistributedReviewDriverCtx` carries a `now?: () => string` injection hook. Verified by reading lines 178 (ctx.now) and 222 (hardcoded `new Date()`). `makePerLegSink` receives no `now` parameter, so the rate-limit timestamp stored in `lastRateLimitAt` is always wall-clock in tests. Low practical impact (no test currently asserts this timestamp), but it breaks the existing injection pattern established elsewhere in the same file. **Kept** as info. **Result:** 1 agreed finding kept, 2 unique findings verified and kept. Total: 3 findings (2 minor, 1 info). ### Blast Radius The diff touches `worker-bridge.ts` (the coordination layer for the distributed-quorum production path), `engine-telemetry.ts` (shared ribbon counters read by the status page), and `distributed-quorum.ts` (the leg orchestrator). The distributed path is flag-gated (`RMP_DISTRIBUTED=1`) so blast radius is bounded to that mode; the in-process quorum is unchanged. Changes export new functions from `engine-telemetry.ts` that any module could call. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `recordRmpRateLimit`, `recordRmpTokenUsage`, `runDistributedReviewViaDriver` | | Migration touched | — | | Test delta | +113 / -3 lines in test files | | Dependency changes | — | ### CI status (head `b166ebfd7148`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/72/jobs/0) | | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/72/jobs/1) | | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/72/jobs/2) | ### Related PRs - #4 - #5 ### Findings (3) #### **[MINOR]** JSDoc for `makePerLegSink` is orphaned — attaches to `ARBITER_STATES` instead of the function _src/rmp/worker-bridge.ts:181_ Lines 181–188 contain a `/** … */` block describing `makePerLegSink`, but two declarations are interleaved before the function: `ARBITER_STATES` (line 191, with its own JSDoc at 189–190) and `DEFAULT_LEG_STATE` (line 198). TypeScript tooling and TypeDoc attach a doc-comment to the **immediately following** declaration, so the block at 181–188 is orphaned on `ARBITER_STATES` while `makePerLegSink` at line 203 has no attached documentation (it is preceded only by a blank line). Editor hover-docs and generated API docs for the function are lost. **Fix:** move the JSDoc block to immediately above `function makePerLegSink` (line 203), after `DEFAULT_LEG_STATE`. The comment at line 189 already correctly documents `ARBITER_STATES` and can stay. #### **[MINOR]** `makePerLegSink` leg-state machine has no telemetry branch — a throttled distributed leg never transitions to `awaiting-rate-limit-reset` _src/rmp/worker-bridge.ts:229_ The state machine at lines 229–244 handles `event.t === "activity"` (→ `tool-executing` or `streaming`) and `event.t === "round"` (→ `delivered`/`died`), but has no `telemetry` case. When a distributed leg emits `{t:"telemetry", rateLimit:{throttled:true}, leg:"a"}`, the ribbon counter is correctly bumped at line 222, but `legState[leg].state` remains whatever it was (typically `"streaming"`), so the Quorum column for that leg never shows `awaiting-rate-limit-reset`. The in-process path via `LivenessWatchdog` (liveness-watchdog.ts lines 154–159) does make this transition, and `SessionState` (session-supervisor.ts lines 47–53) explicitly includes `"awaiting-rate-limit-reset"`. During a throttled distributed review the two paths diverge visually. Suggested fix — add a telemetry branch inside the `leg === "a" || leg === "b"` block: ```ts } else if (event.t === "telemetry" && event.rateLimit) { if (event.rateLimit.throttled && !(["delivered","died"] as SessionState[]).includes(state)) { state = "awaiting-rate-limit-reset"; } else if (state === "awaiting-rate-limit-reset" && !event.rateLimit.throttled) { state = "streaming"; } } ``` This mirrors the LivenessWatchdog's telemetry→rate-limit handling and closes the display inconsistency. #### **[INFO]** `recordRmpRateLimit` call uses `new Date()` directly, bypassing the `now` injection hook on `DistributedReviewDriverCtx` _src/rmp/worker-bridge.ts:222_ Line 222: `recordRmpRateLimit(event.rateLimit, new Date().toISOString())`. `DistributedReviewDriverCtx` carries `now?: () => string` for test-clock injection (line 178), used by `runDistributedQuorum` and `runReviewViaProducer`, but `makePerLegSink` receives no `now` parameter so the rate-limit timestamp stored in `lastRateLimitAt` is always wall-clock — even in tests. Practical impact is low today (no test currently asserts the rate-limit timestamp from this path), but it is an inconsistency with the established injection pattern in the same file. Consider threading `now` through `makePerLegSink`: ```ts function makePerLegSink( base: DriverSink, onLegState: … | undefined, onArbiterState: … | undefined, now: () => string = () => new Date().toISOString(), ): DriverSink { … if (event.rateLimit) recordRmpRateLimit(event.rateLimit, now()); ``` And pass `ctx.now ?? (() => new Date().toISOString())` at the call site in `runDistributedReviewViaDriver`. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 2 • 3 findings (2m/1i) • 2026-06-03T08:48:06.212Z → 2026-06-03T08:53:51.957Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
When RMP_DISTRIBUTED=1, the progress line + ETA, Quorum column, and engine
ribbon render identically to the in-process quorum — the bridge re-derives the
serving signals from the cross-process leg-tagged RMP event stream.
fix(rmp): address round-2 review findings on distributed reportability
All checks were successful
pr-reviewer CI / tsc build (pull_request) Successful in 35s
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 24s
pr-reviewer CI / vitest (pull_request) Successful in 1m0s
7bc1fee08c
- Reorder so the makePerLegSink JSDoc attaches to the function, not the
  ARBITER_STATES/DEFAULT_LEG_STATE consts (orphaned by the prior edit).
- Add the missing throttle branch to the per-leg state machine: a throttled
  {t:"telemetry"} (rate_limit_event, non-allowed) → awaiting-rate-limit-reset,
  mirroring the in-process SessionSupervisor; the next activity recovers it.
  Without it a rate-limited distributed leg silently showed "streaming".
- Thread an injectable `now` into makePerLegSink for the ribbon's rate-limit
  timestamp instead of calling new Date() directly.

+1 test (throttle → awaiting-rate-limit-reset → recover); full suite 2149.

Superseded by round 4.

Show previous round

hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer)

Round 3 — head 7bc1fee08c02, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 agreed minor finding (missing nowMs in recordRmpTokenUsage call) and 1 verified unique-to-B info finding (missing rolling-window prune test); no blocking issues.

Summary

Arbitration — Round 3

Agreed finding (A ∩ B): Both reviewers independently flag recordRmpTokenUsage at src/rmp/worker-bridge.ts:220 for omitting nowMs, leaving the rolling-window ring pinned to real wall-clock time even though the injected now hook is in scope one line later. Verified: lines 220-224 show no second argument; line 212 declares now: () => string; line 225 already forwards now() to recordRmpRateLimit. Finding confirmed — kept at minor severity.

Unique-to-B finding (info): tests/engine-telemetry.test.ts:63-80 covers cumulative accumulation for recordRmpTokenUsage but not the rolling-window prune path. Verified: the existing prune test at line 141 is exclusive to recordSdkResultUsage; no analogous test exists for recordRmpTokenUsage despite both functions pushing into the same ring and calling pruneRecent. The gap is real — kept at info severity.

Dropped: Nothing. Both unique-to-B findings survived verification.

Kept: 2 findings (1 agreed minor, 1 unique-to-B info). Verdict: CONDITIONAL_APPROVE.

Blast Radius

The diff spans 8 files across the engine telemetry module, the distributed-quorum RMP bridge, the worker entrypoint, docs, and tests. The two new exported functions (recordRmpTokenUsage, recordRmpRateLimit) are consumed by worker-bridge.ts and extend shared mutable counters used by the status-page ribbon. The blast radius is moderate — touching exported telemetry surface shared by all job paths — but the changes are purely additive and the in-process quorum path is unaffected.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions recordRmpTokenUsage, recordRmpRateLimit, makePerLegSink
Migration touched
Test delta +131 / -3 lines in test files
Dependency changes

CI status (head 7bc1fee08c02)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / tsc build (pull_request) pending details
pr-reviewer CI / tsc --noEmit (pull_request) pending details
pr-reviewer CI / vitest (pull_request) pending details

Findings (2)

[MINOR] recordRmpTokenUsage call omits nowMs — rolling-window timestamp bypasses the injected clock

src/rmp/worker-bridge.ts:220

The round-2 fix correctly threaded the injected now clock into recordRmpRateLimit at line 225, but the adjacent recordRmpTokenUsage call at lines 220-224 passes no nowMs, so the rolling-window ring always falls back to the real Date.now() regardless of what now injects:

// lines 219-225 (current)
if (event.t === "telemetry") {
  recordRmpTokenUsage({           // ← nowMs not passed; defaults to Date.now()
    inputTokens: event.inputTokens,
    outputTokens: event.outputTokens,
    costUsd: event.costUsd,
  });
  if (event.rateLimit) recordRmpRateLimit(event.rateLimit, now()); // ✓ fixed R2
}

DistributedReviewDriverCtx.now is typed () => string (ISO format). The fix converts it to milliseconds at the call site:

recordRmpTokenUsage(
  { inputTokens: event.inputTokens, outputTokens: event.outputTokens, costUsd: event.costUsd },
  Date.parse(now()),   // or: new Date(now()).getTime() — equivalent
);

Without this, the rolling-window stat used by the status-page ribbon's recent-window display cannot be controlled in tests. Any integration test driving the cross-process path that tries to assert window-pruning behaviour at a deterministic timestamp will find entries pruned immediately (the ring entry uses wall clock while engineTelemetrySnapshot(nowMs) uses the injectable — the window check becomes inconsistent). No production-correctness impact: in production now() returns real time so the result is identical to Date.now().

[INFO] No rolling-window prune test for recordRmpTokenUsagerecentUsage boundary unverified for the new function

tests/engine-telemetry.test.ts:63

Lines 63-80 correctly test cumulative accumulation and edge cases for recordRmpTokenUsage, but the rolling-window prune behaviour (entries older than TELEMETRY_WINDOW_MS dropped from recentUsage) is only exercised for recordSdkResultUsage (see the symmetric prune test at line 141). Both functions push into the same recentUsage ring and call pruneRecent, so a boundary test should be symmetric. Suggested addition:

it("recordRmpTokenUsage respects TELEMETRY_WINDOW_MS pruning", () => {
  const t0 = 10_000_000_000;
  const hour = 60 * 60 * 1000;
  recordRmpTokenUsage({ inputTokens: 500, outputTokens: 200, costUsd: 0.05 }, t0 - 2 * hour); // outside window
  recordRmpTokenUsage({ inputTokens: 100, outputTokens: 40,  costUsd: 0.01 }, t0 - 30 * 60 * 1000); // inside
  const s = engineTelemetrySnapshot(t0);
  // cumulative includes both; window-rate only the second
  expect(s.windowInputTokens).toBe(100);
  expect(s.windowOutputTokens).toBe(40);
});

This is low-urgency (the pruneRecent logic itself is exercised by the existing recordSdkResultUsage test), but the gap means a future regression in how recordRmpTokenUsage calls pruneRecent would go undetected. Should be addressed in a follow-on commit.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 3 • 2 findings (1m/1i) • 2026-06-03T08:57:49.759Z → 2026-06-03T09:03:07.631Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 4._ <details> <summary>Show previous round</summary> <!-- hib-pr-reviewer round:3 --> ## hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer) **Round 3** — head `7bc1fee08c02`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 agreed minor finding (missing `nowMs` in `recordRmpTokenUsage` call) and 1 verified unique-to-B info finding (missing rolling-window prune test); no blocking issues. ### Summary ## Arbitration — Round 3 **Agreed finding (A ∩ B):** Both reviewers independently flag `recordRmpTokenUsage` at `src/rmp/worker-bridge.ts:220` for omitting `nowMs`, leaving the rolling-window ring pinned to real wall-clock time even though the injected `now` hook is in scope one line later. Verified: lines 220-224 show no second argument; line 212 declares `now: () => string`; line 225 already forwards `now()` to `recordRmpRateLimit`. Finding confirmed — kept at minor severity. **Unique-to-B finding (info):** `tests/engine-telemetry.test.ts:63-80` covers cumulative accumulation for `recordRmpTokenUsage` but not the rolling-window prune path. Verified: the existing prune test at line 141 is exclusive to `recordSdkResultUsage`; no analogous test exists for `recordRmpTokenUsage` despite both functions pushing into the same ring and calling `pruneRecent`. The gap is real — kept at info severity. **Dropped:** Nothing. Both unique-to-B findings survived verification. **Kept: 2 findings (1 agreed minor, 1 unique-to-B info). Verdict: CONDITIONAL_APPROVE.** ### Blast Radius The diff spans 8 files across the engine telemetry module, the distributed-quorum RMP bridge, the worker entrypoint, docs, and tests. The two new exported functions (`recordRmpTokenUsage`, `recordRmpRateLimit`) are consumed by `worker-bridge.ts` and extend shared mutable counters used by the status-page ribbon. The blast radius is moderate — touching exported telemetry surface shared by all job paths — but the changes are purely additive and the in-process quorum path is unaffected. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `recordRmpTokenUsage`, `recordRmpRateLimit`, `makePerLegSink` | | Migration touched | — | | Test delta | +131 / -3 lines in test files | | Dependency changes | — | ### CI status (head `7bc1fee08c02`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/73/jobs/2) | | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/73/jobs/0) | | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/73/jobs/1) | ### Related PRs - #4 - #5 ### Findings (2) #### **[MINOR]** `recordRmpTokenUsage` call omits `nowMs` — rolling-window timestamp bypasses the injected clock _src/rmp/worker-bridge.ts:220_ The round-2 fix correctly threaded the injected `now` clock into `recordRmpRateLimit` at line 225, but the adjacent `recordRmpTokenUsage` call at lines 220-224 passes no `nowMs`, so the rolling-window ring always falls back to the real `Date.now()` regardless of what `now` injects: ```typescript // lines 219-225 (current) if (event.t === "telemetry") { recordRmpTokenUsage({ // ← nowMs not passed; defaults to Date.now() inputTokens: event.inputTokens, outputTokens: event.outputTokens, costUsd: event.costUsd, }); if (event.rateLimit) recordRmpRateLimit(event.rateLimit, now()); // ✓ fixed R2 } ``` `DistributedReviewDriverCtx.now` is typed `() => string` (ISO format). The fix converts it to milliseconds at the call site: ```typescript recordRmpTokenUsage( { inputTokens: event.inputTokens, outputTokens: event.outputTokens, costUsd: event.costUsd }, Date.parse(now()), // or: new Date(now()).getTime() — equivalent ); ``` Without this, the rolling-window stat used by the status-page ribbon's recent-window display cannot be controlled in tests. Any integration test driving the cross-process path that tries to assert window-pruning behaviour at a deterministic timestamp will find entries pruned immediately (the ring entry uses wall clock while `engineTelemetrySnapshot(nowMs)` uses the injectable — the window check becomes inconsistent). No production-correctness impact: in production `now()` returns real time so the result is identical to `Date.now()`. #### **[INFO]** No rolling-window prune test for `recordRmpTokenUsage` — `recentUsage` boundary unverified for the new function _tests/engine-telemetry.test.ts:63_ Lines 63-80 correctly test cumulative accumulation and edge cases for `recordRmpTokenUsage`, but the rolling-window prune behaviour (entries older than `TELEMETRY_WINDOW_MS` dropped from `recentUsage`) is only exercised for `recordSdkResultUsage` (see the symmetric prune test at line 141). Both functions push into the same `recentUsage` ring and call `pruneRecent`, so a boundary test should be symmetric. Suggested addition: ```typescript it("recordRmpTokenUsage respects TELEMETRY_WINDOW_MS pruning", () => { const t0 = 10_000_000_000; const hour = 60 * 60 * 1000; recordRmpTokenUsage({ inputTokens: 500, outputTokens: 200, costUsd: 0.05 }, t0 - 2 * hour); // outside window recordRmpTokenUsage({ inputTokens: 100, outputTokens: 40, costUsd: 0.01 }, t0 - 30 * 60 * 1000); // inside const s = engineTelemetrySnapshot(t0); // cumulative includes both; window-rate only the second expect(s.windowInputTokens).toBe(100); expect(s.windowOutputTokens).toBe(40); }); ``` This is low-urgency (the `pruneRecent` logic itself is exercised by the existing `recordSdkResultUsage` test), but the gap means a future regression in how `recordRmpTokenUsage` calls `pruneRecent` would go undetected. Should be addressed in a follow-on commit. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 3 • 2 findings (1m/1i) • 2026-06-03T08:57:49.759Z → 2026-06-03T09:03:07.631Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
fix(rmp): thread the injectable clock into recordRmpTokenUsage + window test
All checks were successful
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 22s
pr-reviewer CI / vitest (pull_request) Successful in 1m1s
pr-reviewer CI / tsc build (pull_request) Successful in 35s
aa88ac1fb9
Round-3 review polish on the distributed reportability path:
- makePerLegSink now passes Date.parse(now()) as recordRmpTokenUsage's nowMs so
  the rolling-window timestamp uses the same injectable clock as the rate-limit
  time (was defaulting to Date.now() internally).
- Add the rolling-window prune test for recordRmpTokenUsage (entries outside
  TELEMETRY_WINDOW_MS drop from the window total while cumulative keeps all).

+1 test; full suite 2150.

Superseded by round 5.

Show previous round

hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer)

Round 4 — head aa88ac1fb9e5, base main, trigger synchronize

TL;DR: NEEDS_WORK — unique-to-B double-count finding verified across four files; arbiter runs in-process and both ribbon paths fire concurrently on divergence.

Summary

Arbitration — Round 4

Agreed findings: none (Reviewer A found 0, Reviewer B found 1).

Unique-to-B finding — verified. Reviewer B claims the in-process arbiter double-counts into the ribbon: once via observeSdkMessageForTelemetry inside streamSdkQueryInner, and again when its adapted {t:"telemetry"} events flow through makePerLegSink.observe with no arbiter exclusion guard.

Verification steps and results:

  1. Arbiter is in-process. worker.ts:3067 passes arbiter: new Arbiter({ sdkSemaphore, memoraMcpUrl }) — a live in-engine instance — into runDistributedReviewViaDriver. The two reviewer legs are cross-process; the arbiter is not.

  2. Arbiter's SDK loop calls observeSdkMessageForTelemetry. arbiter.ts:335 calls streamSdkQuerystreamSdkQueryInner. At reviewer.ts:1327, every message passing through streamSdkQueryInner is fed to observeSdkMessageForTelemetry. When the arbiter receives a {type:"result"} carrier, this calls recordSdkResultUsageribbon +1.

  3. Same messages re-enter the ribbon via makePerLegSink. distributed-quorum.ts:207 does for (const event of arbiterAdapter.adapt(msg)) emit(tagLeg(event, "arbiter")). The adapted {t:"telemetry", inputTokens:…, outputTokens:…, costUsd:…} event, tagged leg:"arbiter", is pushed into the event stream consumed by makePerLegSink.observe. At worker-bridge.ts:219, the check is if (event.t === "telemetry")no eventLeg(event) !== "arbiter" guard — so recordRmpTokenUsage is called → ribbon +2. Rate-limit events follow the same dual path via recordRmpRateLimit.

  4. JSDoc claim falsified. engine-telemetry.ts:166-167 states "No double-count: the arbiter does not tap the ribbon directly" — this is true for the two out-of-process leg reviewers but false for the in-process arbiter when legs diverge and arbitration runs.

Finding kept as major. Proposed fix (add && eventLeg(event) !== "arbiter" at worker-bridge.ts:219) is correct and also requires updating the docstring at engine-telemetry.ts:166-167.

Blast Radius

The double-count affects the engine-wide token/cost ribbon (engine-telemetry.ts module-level singletons) and is visible on the status page for all distributed-quorum reviews where legs diverge. The fix is a one-line guard in worker-bridge.ts plus a docstring update; no protocol or data-model changes required.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions recordRmpTokenUsage, recordRmpRateLimit, recordSdkResultUsage, observeSdkMessageForTelemetry, makePerLegSink
Migration touched
Test delta +144 / -3 lines in test files
Dependency changes

CI status (head aa88ac1fb9e5)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / tsc --noEmit (pull_request) pending details
pr-reviewer CI / vitest (pull_request) pending details
pr-reviewer CI / tsc build (pull_request) pending details

Findings (1)

[MAJOR] Arbiter telemetry double-counted in ribbon: makePerLegSink folds arbiter-tagged {t:"telemetry"} events but the in-process arbiter also taps the ribbon via observeSdkMessageForTelemetry

src/rmp/worker-bridge.ts:219

Confirmed double-count path (triggered when legs diverge and the in-process arbiter runs):

  1. worker.ts:3067 constructs new Arbiter(...) — the arbiter runs in the engine process, not in a separate container.
  2. arbiter.ts:335 calls streamSdkQuerystreamSdkQueryInner. At reviewer.ts:1327, observeSdkMessageForTelemetry(message) is called for every SDK message. When the arbiter's {type:"result"} carrier arrives, this calls recordSdkResultUsage. Ribbon +1.
  3. The onMessage callback supplied to arbiter.run at distributed-quorum.ts:206-207 does for (const event of arbiterAdapter.adapt(msg)) emit(tagLeg(event, "arbiter")). The adapted {t:"telemetry", inputTokens, outputTokens, costUsd} event flows into makePerLegSink.observe.
  4. At worker-bridge.ts:219, the guard is if (event.t === "telemetry") with no arbiter exclusion. recordRmpTokenUsage is called unconditionally. Ribbon +2. The same dual path applies to rate-limit events via recordRmpRateLimit.

The JSDoc at engine-telemetry.ts:166-167 — "No double-count: the arbiter does not tap the ribbon directly" — is false when legs diverge; it is only true for the two out-of-process leg reviewers.

Fix. Guard line 219 with the already-imported eventLeg:

// was:
if (event.t === "telemetry") {
// fix:
if (event.t === "telemetry" && eventLeg(event) !== "arbiter") {

Also update engine-telemetry.ts:166-167 to read: "No double-count: arbiter telemetry is excluded from recordRmpTokenUsage (guarded by eventLeg !== 'arbiter'); the in-process arbiter accumulates exclusively via observeSdkMessageForTelemetry."

This applies equally to the recordRmpRateLimit call at line 231 — the rate-limit guard at line 249 (event.t === "telemetry" && event.rateLimit?.throttled) also lacks an arbiter exclusion and should be updated in tandem.

Verdict

NEEDS_WORK


hib-pr-reviewer • round 4 • 1 finding (1M) • 2026-06-03T09:08:31.510Z → 2026-06-03T09:19:07.139Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 5._ <details> <summary>Show previous round</summary> <!-- hib-pr-reviewer round:4 --> ## hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer) **Round 4** — head `aa88ac1fb9e5`, base `main`, trigger `synchronize` **TL;DR:** NEEDS_WORK — unique-to-B double-count finding verified across four files; arbiter runs in-process and both ribbon paths fire concurrently on divergence. ### Summary ## Arbitration — Round 4 **Agreed findings:** none (Reviewer A found 0, Reviewer B found 1). **Unique-to-B finding — verified.** Reviewer B claims the in-process arbiter double-counts into the ribbon: once via `observeSdkMessageForTelemetry` inside `streamSdkQueryInner`, and again when its adapted `{t:"telemetry"}` events flow through `makePerLegSink.observe` with no arbiter exclusion guard. Verification steps and results: 1. **Arbiter is in-process.** `worker.ts:3067` passes `arbiter: new Arbiter({ sdkSemaphore, memoraMcpUrl })` — a live in-engine instance — into `runDistributedReviewViaDriver`. The two reviewer *legs* are cross-process; the arbiter is not. 2. **Arbiter's SDK loop calls `observeSdkMessageForTelemetry`.** `arbiter.ts:335` calls `streamSdkQuery` → `streamSdkQueryInner`. At `reviewer.ts:1327`, every message passing through `streamSdkQueryInner` is fed to `observeSdkMessageForTelemetry`. When the arbiter receives a `{type:"result"}` carrier, this calls `recordSdkResultUsage` → **ribbon +1**. 3. **Same messages re-enter the ribbon via `makePerLegSink`.** `distributed-quorum.ts:207` does `for (const event of arbiterAdapter.adapt(msg)) emit(tagLeg(event, "arbiter"))`. The adapted `{t:"telemetry", inputTokens:…, outputTokens:…, costUsd:…}` event, tagged `leg:"arbiter"`, is pushed into the event stream consumed by `makePerLegSink.observe`. At `worker-bridge.ts:219`, the check is `if (event.t === "telemetry")` — **no `eventLeg(event) !== "arbiter"` guard** — so `recordRmpTokenUsage` is called → **ribbon +2**. Rate-limit events follow the same dual path via `recordRmpRateLimit`. 4. **JSDoc claim falsified.** `engine-telemetry.ts:166-167` states "No double-count: the arbiter does not tap the ribbon directly" — this is true for the two out-of-process leg reviewers but false for the in-process arbiter when legs diverge and arbitration runs. Finding kept as **major**. Proposed fix (add `&& eventLeg(event) !== "arbiter"` at `worker-bridge.ts:219`) is correct and also requires updating the docstring at `engine-telemetry.ts:166-167`. ### Blast Radius The double-count affects the engine-wide token/cost ribbon (`engine-telemetry.ts` module-level singletons) and is visible on the status page for all distributed-quorum reviews where legs diverge. The fix is a one-line guard in `worker-bridge.ts` plus a docstring update; no protocol or data-model changes required. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `recordRmpTokenUsage`, `recordRmpRateLimit`, `recordSdkResultUsage`, `observeSdkMessageForTelemetry`, `makePerLegSink` | | Migration touched | — | | Test delta | +144 / -3 lines in test files | | Dependency changes | — | ### CI status (head `aa88ac1fb9e5`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/74/jobs/0) | | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/74/jobs/1) | | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/74/jobs/2) | ### Findings (1) #### **[MAJOR]** Arbiter telemetry double-counted in ribbon: `makePerLegSink` folds arbiter-tagged `{t:"telemetry"}` events but the in-process arbiter also taps the ribbon via `observeSdkMessageForTelemetry` _src/rmp/worker-bridge.ts:219_ **Confirmed double-count path** (triggered when legs diverge and the in-process arbiter runs): 1. `worker.ts:3067` constructs `new Arbiter(...)` — the arbiter runs in the engine process, not in a separate container. 2. `arbiter.ts:335` calls `streamSdkQuery` → `streamSdkQueryInner`. At `reviewer.ts:1327`, `observeSdkMessageForTelemetry(message)` is called for every SDK message. When the arbiter's `{type:"result"}` carrier arrives, this calls `recordSdkResultUsage`. **Ribbon +1.** 3. The `onMessage` callback supplied to `arbiter.run` at `distributed-quorum.ts:206-207` does `for (const event of arbiterAdapter.adapt(msg)) emit(tagLeg(event, "arbiter"))`. The adapted `{t:"telemetry", inputTokens, outputTokens, costUsd}` event flows into `makePerLegSink.observe`. 4. At `worker-bridge.ts:219`, the guard is `if (event.t === "telemetry")` with no arbiter exclusion. `recordRmpTokenUsage` is called unconditionally. **Ribbon +2.** The same dual path applies to rate-limit events via `recordRmpRateLimit`. The JSDoc at `engine-telemetry.ts:166-167` — "No double-count: the arbiter does not tap the ribbon directly" — is false when legs diverge; it is only true for the two out-of-process leg reviewers. **Fix.** Guard line 219 with the already-imported `eventLeg`: ```ts // was: if (event.t === "telemetry") { // fix: if (event.t === "telemetry" && eventLeg(event) !== "arbiter") { ``` Also update `engine-telemetry.ts:166-167` to read: "No double-count: arbiter telemetry is excluded from `recordRmpTokenUsage` (guarded by `eventLeg !== 'arbiter'`); the in-process arbiter accumulates exclusively via `observeSdkMessageForTelemetry`." This applies equally to the `recordRmpRateLimit` call at line 231 — the rate-limit guard at line 249 (`event.t === "telemetry" && event.rateLimit?.throttled`) also lacks an arbiter exclusion and should be updated in tandem. ### Verdict **NEEDS_WORK** --- <sub>hib-pr-reviewer • round 4 • 1 finding (1M) • 2026-06-03T09:08:31.510Z → 2026-06-03T09:19:07.139Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
fix(rmp): don't double-count in-engine arbiter telemetry in the ribbon
All checks were successful
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 21s
pr-reviewer CI / vitest (pull_request) Successful in 1m0s
pr-reviewer CI / tsc build (pull_request) Successful in 36s
a6ab5a073b
Round-4 review caught a real MAJOR bug. makePerLegSink folded EVERY
{t:"telemetry"} event (legs a/b AND the arbiter) into the engine ribbon. But
the arbiter runs in-engine, and its SDK loop (arbiter.ts → streamSdkQuery →
streamSdkQueryInner, reviewer.ts:1327) ALREADY taps the ribbon via
observeSdkMessageForTelemetry. So on divergence (arbiter runs) its tokens were
counted twice.

Fix: fold telemetry into the ribbon ONLY for the out-of-process legs (a/b),
whose tokens reach the engine solely via the RMP event stream. The arbiter's
leg-tagged telemetry is left to its in-engine direct tap. The legs run in
separate containers (their own ribbon is irrelevant), so no leg is double-tapped.

+1 regression test (arbiter usage counted exactly once); full suite 2151.

Superseded by round 6.

Show previous round

hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer)

Round 5 — head a6ab5a073b93, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 2 agreed-or-verified minor findings and 1 agreed info; no blocking issues.

Summary

Arbitration round 5 — reconciliation

All three unique findings verified against HEAD. Kept all three.

Finding-by-finding

Agreed (A+B): CHANGELOG.md typo — line 10
Confirmed at /var/lib/pr-reviewer/repos/hib/hib-pr-reviewer/1538/CHANGELOG.md:10: the bullet reads `{t:"telemetry}` (closing " absent before }). Both reviewers grounded on the same file and line. Kept as info.

Unique to A: double now() in makePerLegSink
Read src/rmp/worker-bridge.ts lines 228 and 237. now() is called at line 228 (const nowMs = Date.parse(now())) and again at line 237 (recordRmpRateLimit(event.rateLimit, now())). Both are inside the same if (event.t === "telemetry") block with recordRmpTokenUsage between them. In production the two ISO strings can diverge by the time spent in recordRmpTokenUsage. Finding confirmed; kept as minor.

Unique to B: integration test missing ribbon counter assertion
Read tests/rmp-worker-bridge-distributed.test.ts lines 243–259. The test correctly emits a {t:"telemetry", rateLimit:{throttled:true}} event and asserts leg-state transitions (awaiting-rate-limit-resetstreaming), but there is no engineTelemetrySnapshot().rateLimitEvents before/after assertion. The wiring from the cross-process throttle event through to recordRmpRateLimit → ribbon counter is unverified at integration level. Finding confirmed; kept as minor.

Result: 2 minor + 1 info kept; no findings dropped.

Blast Radius

The diff spans 8 files across engine telemetry, the distributed worker bridge, distributed quorum logic, docs, and tests. Core changes are additive (new exported functions in engine-telemetry.ts, new sink logic in worker-bridge.ts); the arbiter double-count guard and the rolling-window/rate-limit paths are the only non-trivial mutation surfaces. Impact is bounded to the RMP_DISTRIBUTED flag-gated path — the in-process quorum and single-reviewer paths are untouched.

BLAST_SCORE: 4/10

Risk Indicators

Indicator Value
Sensitive functions recordRmpTokenUsage, recordRmpRateLimit, recordRateLimitEvent, recordSdkResultUsage, makePerLegSink
Migration touched
Test delta +180 / -3 lines in test files
Dependency changes

CI status (head a6ab5a073b93)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / tsc --noEmit (pull_request) pending details
pr-reviewer CI / vitest (pull_request) pending details
pr-reviewer CI / tsc build (pull_request) pending details

Findings (3)

[MINOR] now() called twice per telemetry event — rolling-window timestamp and rate-limit ISO string can skew

src/rmp/worker-bridge.ts:228

Inside the event.t === "telemetry" branch of makePerLegSink, now() is invoked at line 228 (const nowMs = Date.parse(now())) and again at line 237 (recordRmpRateLimit(event.rateLimit, now())). In production the two calls can return timestamps separated by the time spent executing recordRmpTokenUsage, meaning the rolling-window bucket and lastRateLimitAt are stamped at different instants for the same event.

Capture once:

const nowStr = now();
const nowMs = Date.parse(nowStr);
recordRmpTokenUsage({ ... }, nowMs);
if (event.rateLimit) recordRmpRateLimit(event.rateLimit, nowStr);

This also avoids a second new Date().toISOString() allocation and makes both timestamps provably consistent.

[MINOR] Throttle integration test verifies leg-state transitions but not ribbon rate-limit counter

tests/rmp-worker-bridge-distributed.test.ts:258

The test at line 243 exercises the throttled-telemetry path (leg-a emits {t:"telemetry", rateLimit:{throttled:true, reason:"rejected"}}), confirms awaiting-rate-limit-reset in aStates, and confirms recovery — but never asserts that engineTelemetrySnapshot().rateLimitEvents incremented. This is the only integration-level coverage of the recordRmpRateLimit wiring in makePerLegSink line 237; unit tests in engine-telemetry.test.ts cover the function itself but not the cross-process dispatch path.

Add before/after snapshot assertions:

const before = engineTelemetrySnapshot();
await runDistributedReviewViaDriver(ctx);
// ...existing aStates assertions...
const after = engineTelemetrySnapshot();
expect(after.rateLimitEvents - before.rateLimitEvents).toBe(1);
expect(after.lastRateLimitStatus).toBe("rejected");

[INFO] Typo — missing closing " in `{t:"telemetry}`

CHANGELOG.md:10

Line 10 (Per-leg A/B/arbiter Quorum column bullet) contains the malformed literal `{t:"telemetry}` — the closing " after telemetry is absent. No runtime impact; only the shipped changelog is affected. Should read `{t:"telemetry"}`.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 5 • 3 findings (2m/1i) • 2026-06-03T09:24:17.376Z → 2026-06-03T09:28:22.272Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 6._ <details> <summary>Show previous round</summary> <!-- hib-pr-reviewer round:5 --> ## hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer) **Round 5** — head `a6ab5a073b93`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 2 agreed-or-verified minor findings and 1 agreed info; no blocking issues. ### Summary ## Arbitration round 5 — reconciliation All three unique findings verified against HEAD. Kept all three. ### Finding-by-finding **Agreed (A+B): CHANGELOG.md typo — line 10** Confirmed at `/var/lib/pr-reviewer/repos/hib/hib-pr-reviewer/1538/CHANGELOG.md:10`: the bullet reads `` `{t:"telemetry}` `` (closing `"` absent before `}`). Both reviewers grounded on the same file and line. Kept as info. **Unique to A: double `now()` in `makePerLegSink`** Read `src/rmp/worker-bridge.ts` lines 228 and 237. `now()` is called at line 228 (`const nowMs = Date.parse(now())`) and again at line 237 (`recordRmpRateLimit(event.rateLimit, now())`). Both are inside the same `if (event.t === "telemetry")` block with `recordRmpTokenUsage` between them. In production the two ISO strings can diverge by the time spent in `recordRmpTokenUsage`. Finding confirmed; kept as minor. **Unique to B: integration test missing ribbon counter assertion** Read `tests/rmp-worker-bridge-distributed.test.ts` lines 243–259. The test correctly emits a `{t:"telemetry", rateLimit:{throttled:true}}` event and asserts leg-state transitions (`awaiting-rate-limit-reset` → `streaming`), but there is no `engineTelemetrySnapshot().rateLimitEvents` before/after assertion. The wiring from the cross-process throttle event through to `recordRmpRateLimit` → ribbon counter is unverified at integration level. Finding confirmed; kept as minor. Result: 2 minor + 1 info kept; no findings dropped. ### Blast Radius The diff spans 8 files across engine telemetry, the distributed worker bridge, distributed quorum logic, docs, and tests. Core changes are additive (new exported functions in engine-telemetry.ts, new sink logic in worker-bridge.ts); the arbiter double-count guard and the rolling-window/rate-limit paths are the only non-trivial mutation surfaces. Impact is bounded to the RMP_DISTRIBUTED flag-gated path — the in-process quorum and single-reviewer paths are untouched. **BLAST_SCORE: 4/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `recordRmpTokenUsage`, `recordRmpRateLimit`, `recordRateLimitEvent`, `recordSdkResultUsage`, `makePerLegSink` | | Migration touched | — | | Test delta | +180 / -3 lines in test files | | Dependency changes | — | ### CI status (head `a6ab5a073b93`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/75/jobs/0) | | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/75/jobs/1) | | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/75/jobs/2) | ### Related PRs - #4 - #5 ### Findings (3) #### **[MINOR]** `now()` called twice per telemetry event — rolling-window timestamp and rate-limit ISO string can skew _src/rmp/worker-bridge.ts:228_ Inside the `event.t === "telemetry"` branch of `makePerLegSink`, `now()` is invoked at line 228 (`const nowMs = Date.parse(now())`) and again at line 237 (`recordRmpRateLimit(event.rateLimit, now())`). In production the two calls can return timestamps separated by the time spent executing `recordRmpTokenUsage`, meaning the rolling-window bucket and `lastRateLimitAt` are stamped at different instants for the same event. Capture once: ```ts const nowStr = now(); const nowMs = Date.parse(nowStr); recordRmpTokenUsage({ ... }, nowMs); if (event.rateLimit) recordRmpRateLimit(event.rateLimit, nowStr); ``` This also avoids a second `new Date().toISOString()` allocation and makes both timestamps provably consistent. #### **[MINOR]** Throttle integration test verifies leg-state transitions but not ribbon rate-limit counter _tests/rmp-worker-bridge-distributed.test.ts:258_ The test at line 243 exercises the throttled-telemetry path (leg-a emits `{t:"telemetry", rateLimit:{throttled:true, reason:"rejected"}}`), confirms `awaiting-rate-limit-reset` in `aStates`, and confirms recovery — but never asserts that `engineTelemetrySnapshot().rateLimitEvents` incremented. This is the only integration-level coverage of the `recordRmpRateLimit` wiring in `makePerLegSink` line 237; unit tests in `engine-telemetry.test.ts` cover the function itself but not the cross-process dispatch path. Add before/after snapshot assertions: ```typescript const before = engineTelemetrySnapshot(); await runDistributedReviewViaDriver(ctx); // ...existing aStates assertions... const after = engineTelemetrySnapshot(); expect(after.rateLimitEvents - before.rateLimitEvents).toBe(1); expect(after.lastRateLimitStatus).toBe("rejected"); ``` #### **[INFO]** Typo — missing closing `"` in `` `{t:"telemetry}` `` _CHANGELOG.md:10_ Line 10 (Per-leg A/B/arbiter Quorum column bullet) contains the malformed literal `` `{t:"telemetry}` `` — the closing `"` after `telemetry` is absent. No runtime impact; only the shipped changelog is affected. Should read `` `{t:"telemetry"}` ``. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 5 • 3 findings (2m/1i) • 2026-06-03T09:24:17.376Z → 2026-06-03T09:28:22.272Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
fix(rmp): round-5 polish — single clock read + ribbon-throttle test + typo
All checks were successful
pr-reviewer CI / tsc --noEmit (pull_request) Successful in 22s
pr-reviewer CI / vitest (pull_request) Successful in 1m0s
pr-reviewer CI / tsc build (pull_request) Successful in 35s
f65090534b
- makePerLegSink reads now() once per telemetry event and reuses the ISO for
  both the rolling-window ms timestamp and the rate-limit time (was calling
  now() twice, which could read two slightly different instants).
- Extend the throttle test to assert the ribbon's rate-limit counter +
  lastRateLimitStatus, not just the per-leg state transition.
- Fix CHANGELOG typo (missing closing quote in `{t:"telemetry"}`).

full suite 2151.
hibryda deleted branch feat/rmp-distributed-leg-observability 2026-06-03 11:33:01 +02:00

hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer)

Round 6 — head f65090534b46, base main, trigger synchronize

TL;DR: APPROVE — all 11 prior-round findings resolved; no new actionable issues.

Summary

Round 6 review — all prior findings resolved

Every open finding from rounds 1–5 has been addressed in this synchronise push. Verified against HEAD:

Prior finding Status
R1 kind:"think" falls through to wrong branch Fixed — line 246 now think || stream → streaming
R1 onLegState/onArbiterState typed as string Fixed — typed SessionState/ArbiterState (lines 173-175)
R2 JSDoc orphaned on ARBITER_STATES Fixed — JSDoc at 195-207 now correctly precedes makePerLegSink (208)
R2 No throttle branch → leg never enters awaiting-rate-limit-reset Fixed — lines 253-257 add the telemetry && rateLimit.throttled branch
R2/R3 new Date() bypass of injected clock Fixed — const iso = now() at line 228; Date.parse(iso) at 235
R3 recordRmpTokenUsage omits nowMs Fixed — Date.parse(iso) passed at line 235
R3 No rolling-window prune test Fixed — test at lines 82-93 covers window boundary
R4 Arbiter telemetry double-counted in ribbon Fixed — ribbon fold guarded leg === "a" || leg === "b" (line 219)
R5 now() called twice per event (clock skew) Fixed — single const iso = now() reused for both consumers (line 228)
R5 Throttle test missing ribbon rate-limit counter assertion Fixed — lines 260-263 verify rateLimitEvents delta + lastRateLimitStatus
R5 CHANGELOG typo {t:"telemetry} missing closing " Fixed — CHANGELOG line 10 reads {t:"telemetry"}

No new actionable issues found. The implementation is coherent end-to-end: clock injection threads from DistributedReviewDriverCtx.now through makePerLegSink (line 301 fallback to wall-clock for production), single clock read per telemetry event, arbiter correctly excluded from the ribbon fold, and makeWatchdogSink phase mapping via toServingPhase correctly bridges the distributed vocabulary to PHASE_ORDER.

Blast Radius

Changes touch the RMP worker-bridge (cross-process telemetry sink + phase mapping), the engine-telemetry module (two new exported functions), distributed-quorum (leg-done signals), and the worker entrypoint (Quorum-column wiring for the distributed path). All changes are additive; the RMP_DISTRIBUTED=0 fast path is provably unaffected (the new sink and callbacks are only wired inside the distributed branch). Exported surface of engine-telemetry.ts grows by two functions used only from worker-bridge.ts.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions
Migration touched
Test delta +185 / -3 lines in test files
Dependency changes

CI status (head f65090534b46)

Overall: ✓ success

3 checks: 3 pending

Check State Link
pr-reviewer CI / tsc --noEmit (pull_request) pending details
pr-reviewer CI / vitest (pull_request) pending details
pr-reviewer CI / tsc build (pull_request) pending details
  • #4 (distributed quorum / RMP promotion)
  • #5 (live cross-process validation)

Findings

No new findings this round.

Claude reviewed and found nothing actionable.

Verdict

APPROVE


hib-pr-reviewer • round 6 • 0 findings • 2026-06-03T09:30:47.168Z → 2026-06-03T09:33:24.249Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer round:6 --> ## hib-pr-reviewer review — PR #9 (hib/hib-pr-reviewer) **Round 6** — head `f65090534b46`, base `main`, trigger `synchronize` **TL;DR:** APPROVE — all 11 prior-round findings resolved; no new actionable issues. ### Summary ## Round 6 review — all prior findings resolved Every open finding from rounds 1–5 has been addressed in this synchronise push. Verified against HEAD: | Prior finding | Status | |---|---| | R1 `kind:"think"` falls through to wrong branch | ✅ Fixed — line 246 now `think \|\| stream → streaming` | | R1 `onLegState`/`onArbiterState` typed as `string` | ✅ Fixed — typed `SessionState`/`ArbiterState` (lines 173-175) | | R2 JSDoc orphaned on `ARBITER_STATES` | ✅ Fixed — JSDoc at 195-207 now correctly precedes `makePerLegSink` (208) | | R2 No throttle branch → leg never enters `awaiting-rate-limit-reset` | ✅ Fixed — lines 253-257 add the `telemetry && rateLimit.throttled` branch | | R2/R3 `new Date()` bypass of injected clock | ✅ Fixed — `const iso = now()` at line 228; `Date.parse(iso)` at 235 | | R3 `recordRmpTokenUsage` omits `nowMs` | ✅ Fixed — `Date.parse(iso)` passed at line 235 | | R3 No rolling-window prune test | ✅ Fixed — test at lines 82-93 covers window boundary | | R4 Arbiter telemetry double-counted in ribbon | ✅ Fixed — ribbon fold guarded `leg === "a" \|\| leg === "b"` (line 219) | | R5 `now()` called twice per event (clock skew) | ✅ Fixed — single `const iso = now()` reused for both consumers (line 228) | | R5 Throttle test missing ribbon rate-limit counter assertion | ✅ Fixed — lines 260-263 verify `rateLimitEvents` delta + `lastRateLimitStatus` | | R5 CHANGELOG typo `{t:"telemetry}` missing closing `"` | ✅ Fixed — CHANGELOG line 10 reads `{t:"telemetry"}` | No new actionable issues found. The implementation is coherent end-to-end: clock injection threads from `DistributedReviewDriverCtx.now` through `makePerLegSink` (line 301 fallback to wall-clock for production), single clock read per telemetry event, arbiter correctly excluded from the ribbon fold, and `makeWatchdogSink` phase mapping via `toServingPhase` correctly bridges the distributed vocabulary to `PHASE_ORDER`. ### Blast Radius Changes touch the RMP worker-bridge (cross-process telemetry sink + phase mapping), the engine-telemetry module (two new exported functions), distributed-quorum (leg-done signals), and the worker entrypoint (Quorum-column wiring for the distributed path). All changes are additive; the `RMP_DISTRIBUTED=0` fast path is provably unaffected (the new sink and callbacks are only wired inside the distributed branch). Exported surface of `engine-telemetry.ts` grows by two functions used only from `worker-bridge.ts`. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | — | | Migration touched | — | | Test delta | +185 / -3 lines in test files | | Dependency changes | — | ### CI status (head `f65090534b46`) **Overall: ✓ success** 3 checks: 3 pending | Check | State | Link | |---|---|---| | pr-reviewer CI / tsc --noEmit (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/76/jobs/0) | | pr-reviewer CI / vitest (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/76/jobs/1) | | pr-reviewer CI / tsc build (pull_request) | ⏳ pending | [details](/hib/hib-pr-reviewer/actions/runs/76/jobs/2) | ### Related PRs - #4 (distributed quorum / RMP promotion) - #5 (live cross-process validation) ### Findings **No new findings this round.** _Claude reviewed and found nothing actionable._ ### Verdict **APPROVE** --- <sub>hib-pr-reviewer • round 6 • 0 findings • 2026-06-03T09:30:47.168Z → 2026-06-03T09:33:24.249Z • posted-as: pr-reviewer-bot • model: auto</sub>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
hib/hib-pr-reviewer!9
No description provided.