# Validated emergency findings — Fable 5 implementation handoff **Date:** 2026-08-12 **Scope:** `emergency-fix/`, `emergency-fix/e2e-failures/`, Angular client, targeted Electron/auth bridge, signaling server, and relevant Playwright tests. **Method:** Read-only code and test audit. No product code was changed and the E2E suite was not rerun for this report. ## Executive verdict The emergency pack is directionally correct, but it mixes active defects, already-shipped mitigations, hypotheses, and documentation debt. The current app is **not yet demonstrated to be Discord-reliable** despite a green `65/65` Playwright run. The highest-confidence failure chain is: 1. Restored sessions do not ensure a provision secret. 2. A foreign signal credential cannot be created. 3. The user is sent to authorize login or never identifies/joins. 4. Presence is missing, so WebRTC discovery and WebSocket chat fallback fail. 5. When peers do connect, negotiation still compares home ids against foreign actor ids. 6. A data-channel failure tears down the entire peer connection, including media. 7. Reconnect attempts can be exhausted while signaling is offline, then stop silently. Independent P0/P1 defects also exist in direct-message identity, call delivery, and message synchronization: - inbound DMs trust an actor-id conversation id and can fork a second local thread; - an outbound call enters local voice state before delivery is known, and delivery failure is ignored; - a timed-out inventory cycle is marked “clean” without proving convergence, delaying the next poll for 15 minutes. ## Acceptance target This emergency is not complete until two users with different home signal servers can: - join the same foreign-hosted community without another login prompt; - see each other online and in the correct voice channel; - exchange live and catch-up text messages in both directions; - place and answer a private call with bidirectional audio; - keep one DM thread for the same human across actor aliases; - transfer a file and verify received bytes; - use camera and screen share while voice remains healthy; - recover from a signal-server restart and a data-channel failure, or receive a visible retry action within one minute. ## Finding classification - **Confirmed:** direct current-code mechanism. - **Partial:** some mechanism exists, but the report overstates or misattributes it. - **Mitigated:** code already contains the reported fix; retain as a regression test. - **Unproven:** plausible but needs reproduction evidence. ## Confirmed findings ### F1 — Restored sessions can lack a provision secret **Severity:** P0 **User symptom:** A logged-in user opens a foreign room or invite and is shown `/login?mode=authorize`, or remains invisible on that signal server. `loadCurrentUserSuccess` migrates the home credential and immediately attempts foreign provisioning: - [`users.effects.ts`](../toju-app/src/app/store/users/users.effects.ts), lines 199–216 The restore path does **not** call `ensureHomeProvisionSecret`. That call currently exists only when storage is prepared from a fresh login response: - [`users.effects.ts`](../toju-app/src/app/store/users/users.effects.ts), lines 288–306 Provisioning exits when the secret is absent: - [`signal-server-auth.service.ts`](../toju-app/src/app/domains/authentication/application/services/signal-server-auth.service.ts), lines 154–168 The web store uses `sessionStorage`, so a new tab/session can lose the secret even while other persisted identity data remains. Electron storage is durable, but old installs or cleared user data can still have no secret. **Required fix:** ensure or deliberately recover the home provision secret before any restore-time or join-time foreign provision. Do not silently swallow restore provisioning errors. **Important design caveat:** generating a new secret cannot authenticate foreign accounts created with a lost old secret. The implementation must define recovery for that case instead of looping register/login forever. ### F2 — Missing foreign credentials block presence and chat fallback **Severity:** P0 **User symptom:** The room opens locally, but other users do not see the user; chat is sender-only; voice is empty. The room connection refuses to proceed without a credential. The signaling transport skips identify if credentials cannot be resolved. The server rejects all non-keepalive, non-identify messages from unauthenticated connections. Live WebSocket channel chat is also membership-gated: - [`server/src/websocket/handler.ts`](../server/src/websocket/handler.ts), lines 539–552 Therefore auth/presence must be repaired before treating chat or voice as isolated transport bugs. **Required fix:** prove the complete sequence `credential -> identify(actor id) -> join_server -> mutual roster`, including reconnect. ### F3 — WebRTC initiator and glare logic use the wrong identity space **Severity:** P0 **User symptom:** Cross-home peers connect intermittently, both offer, both wait, or voice remains one-way/connecting. Per-signal credentials exist through `getIdentifyCredentialsForSignalUrl`, but the generic getter prefers the home credential: - [`signaling-transport-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-transport-handler.ts), lines 35–71 The peer manager and incoming signaling handler receive the generic home-biased getter: - [`realtime-session.service.ts`](../toju-app/src/app/infrastructure/realtime/realtime-session.service.ts), lines 198–208 and 232–234 Offer collision handling compares that id with the remote signal actor id: - [`negotiation.ts`](../toju-app/src/app/infrastructure/realtime/peer-connection-manager/connection/negotiation.ts), lines 106–126 Initiator election performs the same lexical comparison: - [`signaling-message-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-message-handler.ts), lines 528–535 **Required fix:** carry connection/signal scope into initiator election, polite-peer handling, reconnect, and P2P voice-state payloads. Both peers must compare actor ids from the same signal URL. ### F4 — Reconnect budget is consumed while signaling is unavailable **Severity:** P0 **User symptom:** A signal outage lasts about one minute; signaling returns, but the peer mesh does not retry and no error is shown. Each timer tick increments `reconnectAttempts` before checking signaling connectivity. At the limit, the timer and tracker are deleted: - [`peer-recovery.ts`](../toju-app/src/app/infrastructure/realtime/peer-connection-manager/recovery/peer-recovery.ts), lines 282–321 This means the client can spend all 12 attempts doing no actual reconnection work. **Required fix:** do not consume an attempt until an offer/reconnect is actually attempted. On exhaustion, publish an explicit failed state with a user-visible Retry action. A later presence or signaling recovery event must be able to re-arm repair. ### F5 — A closed data channel tears down media **Severity:** P0 **User symptom:** Voice, camera, or screen share drops when chat/file control transport fails. The active recovery path calls `removePeer`, then creates a new peer transport: - [`peer-recovery.ts`](../toju-app/src/app/infrastructure/realtime/peer-connection-manager/recovery/peer-recovery.ts), lines 208–235 The realtime README claims the initiator creates a replacement data channel on the existing `RTCPeerConnection`: - [`realtime/README.md`](../toju-app/src/app/infrastructure/realtime/README.md), line 263 That claim is false for the current recovery path. `replaceDataChannel` exists but is not the implemented repair. **Required decision:** - **Recommended emergency step:** keep full rebuild initially, but force resync, preserve/reapply media state, expose progress/failure, and fix the docs. - **Follow-up reliability step:** implement a true soft data-channel replacement on a healthy peer connection, with full rebuild as fallback. ### F6 — Direct messages can fork by home id versus actor id **Severity:** P0 **User symptom:** Two threads represent the same person; a reply lands in the thread the sender is not viewing. The pair id is a literal sorted pair of supplied ids: - [`direct-message.logic.ts`](../toju-app/src/app/domains/direct-message/domain/logic/direct-message.logic.ts), lines 17–21 Inbound handling trusts `payload.message.conversationId` before deriving a local id and performs no alias merge: - [`direct-message.service.ts`](../toju-app/src/app/domains/direct-message/application/services/direct-message.service.ts), lines 511–541 The lesson symbols `resolveDirectConversationId` and `mergeAliasDirectConversations` do not exist in the product tree. **Required fix:** define a canonical local human identity, remap inbound actor ids, merge duplicate conversations transactionally, and preserve messages/unread/status ordering. ### F7 — Outbound private calls can fail silently after local join **Severity:** P0 **User symptom:** Caller sees “In Voice”; callee never rings. `startCall` joins the call locally before sending the ring: - [`direct-call.service.ts`](../toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts), lines 220–235 `sendCallEvent` does not await or inspect delivery: - [`direct-call.service.ts`](../toju-app/src/app/domains/direct-call/application/services/direct-call.service.ts), lines 759–773 Candidate expansion in `PeerDeliveryService` helps when a matching NgRx user alias exists, but there is no complete credential/presence-based routable-id selection and no unreachable UX. **Required fix:** resolve the connected actor id before committing the caller to the session, return a delivery result, and transition to `recipientUnreachable`/Retry when no route accepts the ring. ### F8 — Message synchronization can falsely become “clean” **Severity:** P1, potentially P0 for long missing-history windows **User symptom:** Live chat works, but old messages remain missing for up to 15 minutes. Periodic sync sends inventory requests and dispatches `startSync`: - [`messages-sync.effects.ts`](../toju-app/src/app/store/messages/messages-sync.effects.ts), lines 166–213 Five seconds later, if the reducer still says syncing, the effect unconditionally sets `lastSyncClean = true` and dispatches `syncComplete`: - [`messages-sync.effects.ts`](../toju-app/src/app/store/messages/messages-sync.effects.ts), lines 215–232 No evidence of an empty inventory result is required. The next interval can therefore change from 10 seconds to 15 minutes after timeout rather than convergence. **Required fix:** model an inventory round explicitly. Mark clean only when all expected peers have replied and no missing ids remain. Timeouts and newly received messages must remain dirty. ### F9 — Live chat fallback and history repair are separate **Severity:** P1 **User symptom:** New text arrives while older text, edits, files, and emoji remain absent. The WebSocket `chat_message` path handles live channel messages for joined members. Inventory and sync batches are data-channel-only. `message-revision` has no equivalent WebSocket fallback. `onPeerConnected` kicks inventory: - [`messages-sync.effects.ts`](../toju-app/src/app/store/messages/messages-sync.effects.ts), lines 64–94 That signal can occur at peer-connection state `connected` before the data channel is open; a second data-channel-open signal usually retries, but there is no explicit durable “control channel reopened and inventory completed” state. **Required fix:** trigger and track reconciliation from data-channel open, not only generic peer connected. Queue failed inventory sends and retry on channel open. ### F10 — STUN-only defaults cannot guarantee Discord-like reachability **Severity:** P1 product limitation **User symptom:** Voice works on friendly networks but fails on symmetric NAT, enterprise Wi-Fi, carrier networks, or restrictive firewalls. TURN can be configured by the user, but no managed/default TURN service is supplied. Localhost E2E cannot validate NAT traversal. **Required product decision:** operate a credentialed TURN service with expiry/abuse controls, or explicitly reject the requirement that voice “always works.” Documentation alone does not satisfy the stated target. ## Claims that should not be treated as active defects ### Identify cache fallback — mitigated `getIdentifyCredentialsForSignalUrl` falls back to the credential store when the per-URL cache is empty: - [`signaling-transport-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-transport-handler.ts), lines 47–71 Keep this as a regression invariant. Add a focused spec; do not rewrite this path without a failing reproduction. ### Join-before-identify race — substantially mitigated Client reconnect sends identify before rejoin, and the server serializes message handling per connection. The remaining P0 case is missing credentials, not evidence that server serialization is currently broken. ### Shared `clientInstanceId` across tabs — fixed Current code uses `sessionStorage` and clears the legacy local-storage value. Retain the existing regression test. ### Multi-signal `user_left` teardown — mitigated Current client/server paths carry server membership information and preserve peers still shared elsewhere. This needs multi-signal integration coverage, but the pack should not call it a confirmed current failure. ### Voice allow-list alias miss — partial The voice playback service already collects `id`, `oderId`, and `peerId`. More likely active failures are stale/missing `voiceState`, a peer key absent from all three aliases, or the broader home/actor negotiation mismatch. Instrument before changing routing filters. ### “No retry after abandon” — overstated Presence fallback timers can create a later offer. The confirmed defect is that the explicit reconnect tracker is silently discarded and signaling-down ticks consume its budget. ### Attachment announce/message race — fixed, retain The incoming message handler re-queues auto-download after binding message to room, with unit coverage. Add reorder/large-file E2E rather than reimplementing the fix. ## E2E baseline assessment The archived run is internally consistent: - 65 passed - 0 failed - 0 flaky - 0 skipped - about 13.4 minutes Sources: - [`e2e-failures/README.md`](e2e-failures/README.md) - [`e2e-failures/00-summary.md`](e2e-failures/00-summary.md) - `e2e-failures/parsed-report.json` - `e2e-failures/full-run.log` ### What it proves - Scripted flows passed once in Chromium with retries disabled. - The Angular development server, ephemeral localhost signaling servers, fake mic/camera, and synthetic screen capture can support the covered scenarios. - Strong existing scenarios include two-user voice, eight-user local multi-signal voice, synthetic data-channel recovery, direct-call answer/audio on a same-route setup, chat sync, and several attachment regressions. ### What it does not prove The Playwright configuration runs only Chromium against `ng serve`: - [`e2e/playwright.config.ts`](../e2e/playwright.config.ts), lines 3–39 It does not run Electron, a packaged production build, real desktop safeStorage/SQLite behavior, real screen capture, real TURN/NAT, sleep/wake, or a production signal deployment. The cross-signal auth test adds the second endpoint manually before checking provision: - [`multi-signal-server-auth.spec.ts`](../e2e/tests/auth/multi-signal-server-auth.spec.ts), lines 20–70 It does not cover restore with a missing secret or joining an unknown foreign invite. The cross-signal call test proves only that the callee sees a ring: - [`dm-header-call-ring.spec.ts`](../e2e/tests/voice/dm-header-call-ring.spec.ts), lines 92–184 It does not answer the call, verify audio, exercise people-card home-id routing, or prove one DM thread. The offline DM test proves only local `QUEUED` state: - [`dm-flow.spec.ts`](../e2e/tests/chat/dm-flow.spec.ts), lines 15–36 It never reconnects or proves recipient delivery. The mesh helper permits fewer than the calculated expected connections: - [`signal-manager.ts`](../e2e/helpers/signal-manager.ts), lines 28–50 This can let a partial dual-signal mesh pass. Debug inspection also depends on Angular's dev-only `window.ng`. The data-channel test proves that audio eventually resumes after a synthetic close: - [`data-channel-recovery.spec.ts`](../e2e/tests/voice/data-channel-recovery.spec.ts), lines 27–54 It does not prove uninterrupted audio, which is consistent with the current full peer teardown. ## Revised implementation order Do not tune codecs, ICE timers, or TURN before identity and membership are correct. ### Packet 0 — Reproduction and observability **Goal:** produce one deterministic two-signal matrix and enough correlated data to prove each later fix. Log in debug builds: - home user id; - credential actor id per normalized signal URL; - room id/source URL; - peer-map key and peer signal URL; - initiator/polite decision inputs; - call `targetUserId` and delivery outcome; - DM incoming and resolved conversation ids; - data-channel generation and reconnect attempt reason. Do not log tokens, passwords, provision secrets, SDP bodies, or message contents. ### Packet 1 — Restore-safe silent foreign auth **Primary files:** - `toju-app/src/app/store/users/users.effects.ts` - `toju-app/src/app/domains/authentication/application/services/signal-server-auth.service.ts` - `signal-server-authorize.service.ts` - provision-secret stores; targeted Electron bridge only if the persistence contract changes **Tests:** - restored home session + absent secret; - restored Electron session; - web new-tab behavior; - old foreign account whose previous secret is lost; - offline/unknown endpoint never opens authorize; - foreign `auth_error` does not expire the valid home session. ### Packet 2 — Identify/join/presence integrity **Primary files:** - `room-signaling-connection.ts` - `signaling-transport-handler.ts` - `signaling.manager.ts` - targeted `server/src/websocket/handler.ts` only if a failing test proves a server change is needed **Tests:** - credential-store fallback on a fresh socket; - identify is first authenticated message before join; - signal-server restart restores both users to the roster; - leaving one signal scope does not remove a peer shared through another. ### Packet 3 — Cross-signal identity for calls and DMs **Primary files:** - `domains/direct-message/**` - `domains/direct-call/**` - `peer-delivery.service.ts` - authentication credential/alias readers **Tests:** - candidate collection from home id, actor id, peer id, and known presence routes; - unreachable call never leaves caller silently in voice; - merge two alias conversations without message/status loss; - cross-signal DM reply remains in one thread; - cross-signal call rings, answers, and carries bidirectional audio. ### Packet 4 — Per-signal WebRTC identity **Primary files:** - `realtime-session.service.ts` - `signaling-message-handler.ts` - `connection/negotiation.ts` - `recovery/peer-recovery.ts` - voice-state data-channel payload construction **Tests:** - home-id/actor-id matrix elects exactly one initiator; - polite-peer collision decision uses the same scoped ids; - same-home and cross-home voice are bidirectional; - stale voice state cannot leave a connected peer permanently muted without recovery. ### Packet 5 — Honest data-channel and reconnect recovery **Primary files:** - `recovery/peer-recovery.ts` - `messaging/data-channel.ts` - `peer-connection.manager.ts` - user-visible connectivity state/components **Tests:** - signaling-down ticks do not consume reconnect attempts; - max real attempts exposes Retry; - Retry re-arms recovery; - data-channel reopen forces inventory; - media state is reapplied after full rebuild; - if soft replacement is implemented, audio remains continuously flowing. ### Packet 6 — Message convergence and file integrity **Primary files:** - `store/messages/messages-sync.effects.ts` - message sync reducers/rules/handlers - attachment transfer only where a failing test identifies a gap **Tests:** - timeout remains dirty; - only complete, empty inventory rounds select the 15-minute cadence; - late join catches up; - edit/delete converge after DC recovery; - offline DM reconnects and delivers; - large-file download SHA-256 and size match the source; - attachment announce/message order is stressed in both channel and DM paths. ### Packet 7 — Production reliability proof Add separate suites rather than weakening existing fast PR coverage: - Electron smoke: login restore, foreign provision, text, file, voice; - signal restart during active voice and DM; - TURN-backed relay-only test; - sleep/wake or network-interface-change test; - 30-minute scheduled voice/screen-share soak; - exact mesh assertions where topology guarantees the count. ## Documentation corrections required with fixes 1. Replace the false soft-data-channel paragraph in `realtime/README.md` unless soft replacement ships. 2. Relabel the LESSONS outbound-call and DM-canonicalization entries as unfinished specifications until real symbols and tests exist. 3. Correct authentication README endpoints from `/api/auth/*` to `/api/users/*`. 4. Correct chat README inventory cap from 1,000 to 20,000. 5. Mark `shared-kernel/signaling-contracts.ts` non-authoritative or align it with live wire names. 6. Correct `e2e/CONTEXT.md` if it claims Electron coverage; the current suite is browser + `ng serve`. ## Decisions required from the product owner before implementation 1. **Lost provision secret recovery** - Recommended: automatically create a secret when none has ever existed; when an existing foreign account rejects the new secret, show a targeted per-server recovery action instead of a generic login loop. 2. **Web secret durability** - Recommended emergency default: keep session-scoped storage for security, but make the new-tab limitation explicit and recover without misclassifying home logout. 3. **Data-channel strategy** - Recommended: ship visible full-rebuild recovery + forced resync first, then soft replacement as a measured follow-up. 4. **TURN** - Recommended for the stated Discord-quality goal: managed credentialed TURN, not user-only configuration. 5. **History availability** - The signal server stores no chat history. Decide whether “offline users always recover history” may depend on another online client, or whether an encrypted durable mailbox/history service is required. 6. **Presence semantics** - Define online/idle/DND/offline and stale timeout behavior. Current emergency tests mostly prove connectivity, not Discord-like status semantics. ## Definition of done for Fable 5 For every packet: 1. Start from a behavior-level failing test or deterministic reproduction. 2. Change only the packet's owned surfaces. 3. Record actor ids and signal scope in test diagnostics without secrets. 4. Run focused unit/integration tests, then the relevant Playwright subset. 5. Run lint/build for touched packages. 6. Update lying docs in the same change. 7. Do not mark complete from unit-green alone; attach the user-visible proof. The green 65-test archive is a useful baseline, not release certification. Release confidence requires the two-signal cross-home matrix, real restart recovery, Electron coverage, and TURN/NAT validation described above.