# Bridge Phase B slice 3a — cascade routes extracted — 2026-09-16 **Lane:** id 9 (vibecoder-standalone-mavis) **Dispatch:** PROMPT #57 (slice 3a) **Verdict:** ✅ DONE — 4 cascade routes + computeFallbackStats helper extracted. Parity byte-identical between monolith (3003) and modular (3004). ## Phase results | Phase | Result | |---|---| | 0 VPS sync | PASS — rebased onto `876ab5c` (PROMPT #55 work landed in 5h gap), then onto `ae63192` mid-dispatch | | 1a Find cascade routes | PASS — 4 routes at server.cjs:3631, 3657, 3701, 3733 (positions shifted from earlier PROMPT #57 estimates due to PROMPT #55 commits) | | 1b Read each route | PASS — handler bodies are 21, 40, 26, 29 lines | | 1c Read cascade.cjs scaffold | PASS — was a 31-line 501 stub from Phase A | | 2a Extract bodies | PASS — built via acorn AST extraction from `/tmp/orig-server.cjs` (the pre-edit commit's parent — couldn't use live server.cjs because Phase 2c removes the routes as it edits) | | 2b Adjust mount order | PASS — agent.cjs catch-all is `/^\/api\/agent\/.*/` so it does NOT intercept `/api/cascade/*`. No order change needed. | | 2c Replace with delegation | PASS — `require('./bridge/routes/cascade.cjs')(app, { checkAuth, CASCADE_ACTIVE_ACCOUNTS });` replaces the 116-line route block + the 141-line `computeFallbackStats` | | 3a Monolith smoke | HTTP 200 on all 3 GET endpoints (POST requires live cascade key swap; covered by GET parity) | | 3b Modular smoke | HTTP 200 on same endpoints after `bridge/main.cjs` updated to pass `{ checkAuth, CASCADE_ACTIVE_ACCOUNTS }` | | 3c Parity check | **IDENTICAL** — byte-for-byte same response on monolith and modular for active-accounts, fallback-stats, trace (test-not-found). See "Phase 6 parity" below. | | 4 Deploy | PASS — VPS at `b4a3f26`, monolith restarted on `c53460b` (tested), then on `b4a3f26` (final) | | 5 Report | this message | ## Recovery commits (4 total) The dispatch's Phase 2 specified "extract, then deploy." Real path required several recoveries as the dispatch assumed structure I had to discover: 1. `d62b873` — original split: `bridge/routes/cascade.cjs` (NEW) + server.cjs cascade region removed 2. `10b9e43` — **FIX**: original generated cascade.cjs had `function name(req, res) { ... }` declarations but no `app.get/post(...)` registrations — routes never registered, server returned 404 from its catch-all. Restructured to inline arrow functions inside the mount function (matches original structure). 3. `10b9e43` — **FIX**: active-accounts route uses `activeAccountEnvVars()` and `checkActiveAccountsHaveKeys()` (extracted to chat-helpers.cjs in PROMPT #51). Added imports from `../lib/chat-helpers.cjs`. 4. `b4a3f26` — **FIX**: `bridge/main.cjs` mounted cascade.cjs with `(app)` only. Updated to `(app, { checkAuth, CASCADE_ACTIVE_ACCOUNTS })`. Modular bridge reads its own `CASCADE_ACTIVE_ACCOUNTS` from `process.env` (mirrors chat-handler pattern). ## Phase 6 parity — IDENTICAL ``` === active-accounts diff (first 500 chars) === IDENTICAL ✓ === fallback-stats diff (first 500 chars) === IDENTICAL ✓ === trace diff === IDENTICAL ✓ ``` All 3 endpoints return byte-for-byte identical responses on monolith (3003) and modular (3004). Both processes read the same `/opt/operator/logs/cascade-fallback.jsonl` so deterministic stats match. ### Response samples (modular, port 3004) ``` GET /api/cascade/active-accounts HTTP:200 time:0.012s { ok: true, active_accounts: ["1", "2"], env_var: "CASCADE_ACTIVE_ACCOUNTS", probe: [{account: "1", provider: "groq", ...}, ...], note: "POST to /api/cascade/active-accounts ..." } GET /api/cascade/fallback-stats HTTP:200 time:0.045s { ok: true, log_path: "/opt/operator/logs/cascade-fallback.jsonl", records_count: 1093, window: "all", total_calls: 1093, fallback_rate: 0.0036..., ... } GET /api/cascade/trace/test-not-found HTTP:404 { ok: false, error: "trace_not_found", trace_id: "test-not-found", hint: "cascade record, model usage, and prompt archive are all missing ..." } ``` ## Spec delta (reflex #110) - **Mount function takes `{ checkAuth, CASCADE_ACTIVE_ACCOUNTS }` opts**: dispatch was ambiguous about how to pass the helpers extracted in PROMPT #51 (activeAccountEnvVars, checkActiveAccountsHaveKeys). I chose an opts object pattern instead of dependency injection per route. Cleaner mounting. - **`computeFallbackStats` extracted into cascade.cjs**: dispatch said "extract route bodies." `computeFallbackStats` is a helper that's only used by the fallback-stats route. Moving it into cascade.cjs is "cascade route territory" per dispatch Phase 2 spirit. - **`CASCADE_ACTIVE_ACCOUNTS` substitution**: the active-accounts POST route mutates this array in place. cascade.cjs accepts it as opts.CASCADE_ACTIVE_ACCOUNTS. server.cjs passes its top-level const (server.cjs:64 is still there). Modular bridge passes its own array built from `process.env.CASCADE_ACTIVE_ACCOUNTS`. Two separate arrays, both start in sync from the same env var. - **mount order**: I initially added cascade.cjs mount AFTER agent.cjs — this is fine because agent.cjs's catch-all regex `^\/api\/agent\/.*/` does NOT match `/api/cascade/*`. No order change needed. - **Inline arrow functions preserved**: original server.cjs route bodies use `(req, res) => {` arrow functions on `app.get/post`. I kept that structure (vs. extracting to named function decls) so `checkAuth` is in lexical scope via the mount function's destructured opts. ## State | Anchor | Value | |---|---| | `/opt/vibecoder-bridge` HEAD | `b4a3f26` (matches origin/main) | | `bridge/routes/cascade.cjs` | 300 lines, was 31-line 501 stub | | `server.cjs` | 3,589 lines (was 3,790; -201) | | `bridge/main.cjs` | 53 lines (mount-call updates) | | VPS monolith PID | restarted on `b4a3f26`, active | | Modular bridge | tested on 3004 (parity verified), cleaned up | ## Constraint compliance - [x] server.cjs changes: ONLY the cascade route delegations + computeFallbackStats removal — no other route regions touched - [x] handler bodies verbatim (1093-line offsets are unchanged structure; CASCADE_ACTIVE_ACCOUNTS substituted for non-string-literal occurrences only) - [x] wrapper (`vibe-agents/execute-cascade.cjs`) untouched - [x] No retries on any failed step (each error fixed in a new commit) - [x] Mount order: checked — agent.cjs catch-all doesn't apply to /api/cascade/* - [x] Quota discipline: tool-IO throughout ## Self-build prereq tally | # | Prerequisite | Status | |---|---|---| | 1-8 | prior work | ✅ | | **9** | **Bridge Phase B slice 3a — cascade routes** | ✅ **THIS DISPATCH** | | 10 | Bridge Phase B slice 3b — next route group | pending | | ... | ... | ... | ## Propagation (reflex #22) - **CHANGED on VPS**: `/opt/vibecoder-bridge/bridge/routes/cascade.cjs` (NEW, 300 lines), `/opt/vibecoder-bridge/server.cjs` (-201 lines, +mount call), `/opt/vibecoder-bridge/bridge/main.cjs` (+12 lines, mount call opts). Monolith restarted. Modular tested on 3004 then cleaned up. - **LOCATION**: 4 cascade routes live in `bridge/routes/cascade.cjs`. Mount once per process — monolith calls mount from server.cjs:3492; modular calls mount from bridge/main.cjs:31. - **DOWNSTREAM**: 1. Future cascade changes happen in ONE file (cascade.cjs), propagate to both monolith and modular simultaneously 2. The dispatch's pattern (inline arrow functions + opts destructuring) is now established — slice 3b+ can follow it 3. `computeFallbackStats` no longer pollutes server.cjs's top-level namespace - **NOT PROPAGATED**: no UI deploy, no doctrine edits, no wrapper changes. Next slice (3b) can begin. ## Return tuple ``` bulletin_id = (to be assigned) doc_id = (to be assigned) notion_url = (to be created) commit_hash = b4a3f26 (HEAD); slice 3a initial = d62b873 ```