Skip to content

feat(v2): A1 reconnect + history recovery (V1.1 — Item 1)#2

Merged
LightDriverCS merged 2 commits into
mainfrom
feat/v1.1-a1-reconnect
May 6, 2026
Merged

feat(v2): A1 reconnect + history recovery (V1.1 — Item 1)#2
LightDriverCS merged 2 commits into
mainfrom
feat/v1.1-a1-reconnect

Conversation

@LightDriverCS

Copy link
Copy Markdown
Contributor

Summary

V1.1 Item 1 from the benchagi-v2-cloud-brain-pickup runbook: keep the CLI alive across network drops, replay missed events on reconnect, warn on seq gaps.

Stacks on #1 (V1.0-beta). Diff shown is just the A1 changes.

Transport (src/v2/transport/local-gateway.ts)

  • Differentiate user-initiated close from network-initiated close. User close drains events(); network close keeps it alive.
  • New setReconnectListeners({ onDisconnected, onReconnecting, onReconnected }) lifecycle hook.
  • Auto-reconnect loop with backoff sequence (1s, 2s, 5s, 10s, 30s, cap at 30s) per new src/v2/transport/backoff.ts:nextBackoffMs.
  • Pending requests at disconnect are still rejected so callers decide retry. events() does NOT exit.

Router (src/v2/render/event-router.ts)

  • New optional onSeqGap(runId, prevSeq, nextSeq) handler. Fires once per gap per runId. Existing dedupe runs first so out-of-order replay events do NOT cause spurious gap warnings.

Chat-runner (src/v2/chat-runner.ts)

  • Tracks highest EventFrame.seq per session.
  • On reconnect, calls chat.history { sessionKey, sinceSeq } and dispatches replay frames through the existing router. Dedupe key (prefix, runId, seq, stream, sub) collapses live + replay events to a single render. Gap-warning is suppressed during replay since replay IS the recovery for the gap.
  • Reconnect lifecycle surfaces to the user as dim status lines.

Tests (44 total, +10)

  • src/v2/test/backoff.test.ts: 6 cases for nextBackoffMs (sequence, cap, non-finite/non-positive inputs, fractional flooring).
  • src/v2/test/event-router.test.ts: +4 tests — seq-gap fires on jump, NOT on +1 advance, NOT on dedupe-replay; chat.history replay does not double-render.

SPEC §13 coverage

SPEC §13 test Where landed
Reconnect: network drop backoff sequence backoff.test.ts
Renderer: seq gap warning event-router.test.ts
Reconnect: in-flight run recovery via chat.history router-level dedupe test in event-router.test.ts; transport-level integration test deferred to a future PR that introduces transport DI in ChatRunner

Anvil Handoff

Per the runbook's V1.1 Item 1 Anvil prompt:

"Walk through reconnect scenarios: idle disconnect, mid-message drop, server-shutdown, seq gap, gateway restart. What's missing in the recovery contract? Are duplicates possible under any race? What's the worst UX moment during a 30-second reconnect attempt?"

Codex Anvil will fire on the anvil label.

Test plan

  • npm install && npm run build && npm run test:v2 → 44/44 pass from clean clone
  • npm run lint → silent success
  • benchagi doctor against brew openclaw 2026.5.2 → regression check passes
  • Live reconnect smoke (manual): start benchagi REPL, kill openclaw gateway with launchctl stop, observe "(connection lost — reconnecting…)" + backoff status lines, restart gateway, observe "(reconnected)" + history replay
  • Codex Anvil pass posts a review

🤖 Generated with Claude Code

@LightDriverCS LightDriverCS added the anvil Queue for Codex Anvil smoke testing (Hammer-Anvil PR handoff) label May 6, 2026
@LightDriverCS
LightDriverCS changed the base branch from feat/v2-streaming-cli to main May 6, 2026 17:52
@LightDriverCS
LightDriverCS changed the base branch from main to feat/v2-streaming-cli May 6, 2026 17:53
@LightDriverCS
LightDriverCS changed the base branch from feat/v2-streaming-cli to main May 6, 2026 18:03
LightDriverCS and others added 2 commits May 6, 2026 12:04
V1.1 Item 1 from `~/.openclaw/wiki/main/_boards/runbooks/platform/benchagi-v2-cloud-brain-pickup.md`:
keep the CLI alive across network drops; replay missed events on
reconnect; warn the user when seq numbers gap.

## Transport (`src/v2/transport/local-gateway.ts`)
- Differentiate user-initiated close from network-initiated close.
  User close drains the events() iterator; network close keeps it
  alive across reconnect.
- New `setReconnectListeners({ onDisconnected, onReconnecting,
  onReconnected })` lifecycle.
- Auto-reconnect loop with backoff sequence (1s, 2s, 5s, 10s, 30s,
  cap at 30s) per `src/v2/transport/backoff.ts:nextBackoffMs`.
- Pending requests at disconnect time are still rejected so callers
  can decide whether to retry. The events() iterator does NOT exit.

## Router (`src/v2/render/event-router.ts`)
- New optional `onSeqGap(runId, prevSeq, nextSeq)` handler. Fires
  exactly once per gap, per runId, when an agent-payload seq jumps
  by more than 1. Existing dedupe runs first, so out-of-order replay
  events are filtered before gap-detection runs.

## Chat-runner (`src/v2/chat-runner.ts`)
- Tracks the highest `EventFrame.seq` per session.
- On reconnect, calls `chat.history { sessionKey, sinceSeq }` and
  dispatches replay frames through the existing router. Dedupe key
  `(prefix, runId, seq, stream, sub)` collapses live + replay events
  to a single render. Gap-warning is suppressed during replay since
  replay IS the recovery for the gap.
- Reconnect lifecycle is surfaced to the user as dim status lines
  ("connection lost — reconnecting…", "reconnect attempt N in Ns",
  "reconnected").

## Tests (44 total, +10 from this PR)
- `src/v2/test/backoff.test.ts`: 6 cases for nextBackoffMs (sequence,
  cap, non-finite/non-positive inputs, fractional flooring).
- `src/v2/test/event-router.test.ts`: +4 tests covering seq-gap
  warning fires on jump, NOT on +1 advance, NOT on dedupe-replay,
  AND chat.history replay does not double-render previously-seen
  events.

## SPEC §13 coverage delivered
- "Reconnect: network drop backoff sequence" — backoff.test.ts
- "Renderer: seq gap warning" — event-router.test.ts
- "Reconnect: in-flight run recovery via chat.history" — covered at
  the router level (replay-doesn't-double-render); the transport-
  level integration test is deferred to a future PR that introduces
  transport DI in ChatRunner.

## Verified
- `npm run build` clean
- `npm run test:v2` 44/44 green (was 34)
- `npm run lint` clean
- `benchagi doctor` smoke OK against brew openclaw 2026.5.2
  (regression check; doesn't exercise reconnect path live)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- **P1 (live-vs-history race)**: live event frames arriving while a
  chat.history replay is in flight are now buffered by `eventLoop`
  and drained in arrival order after replay completes. Without this,
  a fast gateway resuming streaming during chat.history could send
  the renderer events out of order and produce false seq-gap
  warnings. The replay flag is set BEFORE the chat.history request
  (not after) so frames during the request are also buffered. Drain
  loops to handle the tail case where new frames arrive during
  drain.
- **P1 (stale-socket close)**: a prior socket's close event MUST
  NOT poison the active socket if a reconnect already swapped
  `this.ws` out. Added `if (this.ws !== ws) return` guard at the
  top of the close handler.

Two remaining Codex findings deferred to V1.1 follow-up PRs (TODOs
inline):

- **P1 (chat.send acceptance race)**: re-issue chat.send with same
  idempotencyKey on reconnect, or confirm acceptance via
  chat.history/sessions.list before failing the send.
- **P2 (history retry)**: retry chat.history with bounded backoff
  before giving up on the recovery window.
- **P2 (shutdown-hint plumbing)**: feed `restartExpectedMs` from
  the shutdown event into `reconnectLoop`'s first-attempt floor so
  the reconnect UX respects the planned-restart hint.

Tests: 44/44 still green. The race-fix behavior is straightforward
to read; an integration-level test that exercises the
buffer-drain flow needs the transport-DI refactor that's queued for
a future PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@LightDriverCS
LightDriverCS force-pushed the feat/v1.1-a1-reconnect branch from 24956ae to 69cb8bc Compare May 6, 2026 18:04
@LightDriverCS
LightDriverCS merged commit bba4e45 into main May 6, 2026
2 checks passed
LightDriverCS added a commit that referenced this pull request May 6, 2026
Codex Anvil 4 finding: HOLD. Two P0s + two P1s + a missing-test
flag.

Most important: Codex couldn't find a readable local gateway path
that routes chat.send through cloud-brain when an agent's
deployment has runtime: 'remote-brain'. Without that dispatch
bridge, the smoke script will pass on local execution without
exercising cloud-brain at all. The bridge MAY be in W2 (#878)
which Codex couldn't see in full diff via the connector; needs
verification.

Other findings:
- P1: bench-cli's chat.history call expects events/frames but the
  readable OpenClaw chat.history handler returns transcript
  messages and ignores sinceSeq. V1.1 reconnect replay is
  best-effort no-op until this contract is reconciled.
- P1: Firestore doc id format assumption in the smoke
  (instances/{instanceId}/agentDeployments/{instanceId}_{agentId})
  may not match real W1 backfill output if deploymentIds differ.
- P2: Firestore failures recorded as skips can produce false
  exit-0 if no agents tested.
- P2: smoke assumes repo-root cwd.

Stronger smoke proposed: assert directive artifacts in Firestore
(relayDirectives doc with directiveType: llm_turn), assert directive
reaches completed, AND assert CLI saw normal chat/agent.lifecycle
frames. That proves both halves of ADR-006 transparency.

Doesn't change the smoke script in this commit — the structural
concern (missing dispatch bridge) needs resolution first. The
smoke script is preserved as-is (P0 #2 acknowledged as a review
finding) so Cory can decide which path to take.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
LightDriverCS added a commit that referenced this pull request May 6, 2026
…n (Switch 2-CLI) (#9)

* feat(v2): cloud-brain smoke script (Phase B prep)

Writes `scripts/cloud-brain-smoke.mjs` per V2 runbook §"Validation
script". Exercises the cloud-brain end-to-end path once Phase 1B
merges (BenchAGI #872 W1 + #874 W4 + #878 W2 + #988 relay +
openclaw#24 W3) and an agent's deployment is flipped to
runtime: 'remote-brain'.

What the script does:
- Lists known agents from the local openclaw gateway.
- Queries Firestore (admin REST + gcloud token per the documented
  recipe) for `agentDeployments/{instanceId}_{agentId}.runtime`.
- For each agent with `runtime === 'remote-brain'`, spawns
  `benchagi --agent <name> --liveness off "respond: smoke-ok"`
  with stdout captured and a 60s timeout.
- Asserts: exit 0, non-empty stdout, no error markers, latency
  under 60s.
- Emits JSON summary; exits 0 only if all tested agents passed.

Required env: INSTANCE_ID. Optional: GCP_PROJECT (default
benchagi-8ea90), SMOKE_AGENT_FILTER, SMOKE_PROMPT, SMOKE_TIMEOUT_MS.

Gated on cloud-brain Phase 1B merging + a deployment flipped to
remote-brain. Until then, the script reports "no remote-brain
agents found — Phase 1B may not be merged + flipped yet" and exits
0 (not a failure).

Stays as a draft PR until Cory merges Phase 1B and flips at least
one deployment, at which point we run the smoke + capture the
transcript for ANVIL-5.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* docs(v2): add ANVIL-4-REVIEW (cloud-brain readiness)

Codex Anvil 4 finding: HOLD. Two P0s + two P1s + a missing-test
flag.

Most important: Codex couldn't find a readable local gateway path
that routes chat.send through cloud-brain when an agent's
deployment has runtime: 'remote-brain'. Without that dispatch
bridge, the smoke script will pass on local execution without
exercising cloud-brain at all. The bridge MAY be in W2 (#878)
which Codex couldn't see in full diff via the connector; needs
verification.

Other findings:
- P1: bench-cli's chat.history call expects events/frames but the
  readable OpenClaw chat.history handler returns transcript
  messages and ignores sinceSeq. V1.1 reconnect replay is
  best-effort no-op until this contract is reconciled.
- P1: Firestore doc id format assumption in the smoke
  (instances/{instanceId}/agentDeployments/{instanceId}_{agentId})
  may not match real W1 backfill output if deploymentIds differ.
- P2: Firestore failures recorded as skips can produce false
  exit-0 if no agents tested.
- P2: smoke assumes repo-root cwd.

Stronger smoke proposed: assert directive artifacts in Firestore
(relayDirectives doc with directiveType: llm_turn), assert directive
reaches completed, AND assert CLI saw normal chat/agent.lifecycle
frames. That proves both halves of ADR-006 transparency.

Doesn't change the smoke script in this commit — the structural
concern (missing dispatch bridge) needs resolution first. The
smoke script is preserved as-is (P0 #2 acknowledged as a review
finding) so Cory can decide which path to take.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat(v2): Firebase token attachment + WS frame tracing for cloud-brain (Switch 2-CLI)

Pairs with the BenchAGI mono cloud bridge endpoint + the OpenClaw
gateway bridge to enable transparent cloud-brain dispatch from the
CLI. The CLI side is small: attach the user's Firebase ID token to
chat.send so the gateway can authenticate to the cloud bridge.

## What's new

- `src/v2/auth/firebase-token.ts` — loads Firebase Direct creds
  from the existing keychain. Refreshes ID token using the public
  Firebase Identity Toolkit when one of these is set:
    BENCHAGI_FIREBASE_API_KEY
    NEXT_PUBLIC_FIREBASE_API_KEY
    FIREBASE_API_KEY
- `src/v2/chat-runner.ts` — sendMessage now resolves the Firebase
  token (best-effort) and adds `cloudAuth.firebaseIdToken` to the
  chat.send request. If no creds are available, chat.send fires
  without the field and the gateway falls back to local dispatch
  per ADR-006's transparency contract.
- `src/v2/transport/local-gateway.ts` — accepts the cloudAuth field
  in chat.send param shape; passes through unchanged.
- `src/v2/cli.ts` — adds raw WS frame tracing for diagnostics:
    --trace-frames <path>
    BENCHAGI_TRACE_FRAMES=<path>
  Each WS frame the CLI receives is appended as a JSONL line to
  the file. Useful for correlating CLI events to relayDirectives
  during the smoke test.

## Verification

- `npm run build` — clean
- Existing test suite still passes (no behavior change for the
  default-local path; only adds the cloudAuth field when a token
  is available).

## Limitations / follow-ups

- chat.send acceptance race (V1.1 ANVIL-2 P1 deferred) — still not
  fixed in this PR. Re-issuing chat.send on reconnect with same
  idempotencyKey is a separate cycle.
- Token refresh path silently no-ops if no Firebase API key env
  var is set. CLI users who don't have one configured will fall
  through to local-only dispatch even on remote-brain agents
  (which is the safe default).
- Frame trace file is append-only; no rotation. Each smoke run
  should use a fresh path.

## Pairs with

- BenchAGI mono PR [#1016](BenchAGI/BenchAGI_Mono_Repo#1016) — cloud bridge endpoint
- OpenClaw fork PR [#26](BenchAGI/openclaw#26) — gateway-side dispatch + polling

---------

Co-authored-by: LightDriverCS <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

anvil Queue for Codex Anvil smoke testing (Hammer-Anvil PR handoff)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant