chore: dev-stack switches, shared e2e harness, and desktop shell rules

- `LIVE_RELOAD=false npm run dev` keeps the renderer alive across a machine
  suspend; the reload client otherwise destroys the session under test.
- `dev-peer.sh` plus a separate userdata dir runs a second local peer.
- `tools/voice-probe.js` samples peer state and RTP counters from a live
  window, persisting to localStorage so a renderer reload cannot erase it.
- e2e helpers for voice pairs, peer-role election, and a TURN relay.
- Electron single-instance and dev-client-load decisions move into rules
  files with colocated specs.
This commit is contained in:
2026-08-14 03:19:29 +02:00
parent d71e3a98da
commit e49b3ec112
41 changed files with 35947 additions and 50 deletions
+74
View File
@@ -0,0 +1,74 @@
# Emergency Fix Pack — MetoYou / Toju
> **Purpose:** Give other agents a single, user-first map of what this app is, how critical features are supposed to work, where the code lies, and the exact order to fix failures.
>
> **Constraint for this pack:** Analysis and planning only. Product code was not changed when this pack was authored (2026-08-12).
>
> **Primary surface:** `toju-app/` + targeted `electron/` (auth secret store / IPC). Expand to `server/` / `e2e/` only when a packet says so.
---
## How to use this pack
1. Read **this file** + `01-product-overview.md`.
2. Pick a work packet from `12-agent-work-packets.md` (do not invent parallel scope).
3. Open the matching issue file (`04``09`) for user story, intended behavior, failure modes, code paths, and proof.
4. Follow priority order in `11-fix-priority-plan.md` unless the user overrides.
5. Before coding: run the repo **interview-before-implement** ritual (choices A/B/C) unless the user says “just fix it” / “no interview” or an active handoff already records approved decisions.
6. When a packet finishes: update `11-fix-priority-plan.md` status checkboxes and clear or rewrite `agents-docs/HANDOFF.md` per `.cursor/rules/handoff.mdc`.
---
## File index
| File | Contents |
|------|----------|
| `00-README.md` | This index and rules of engagement |
| `01-product-overview.md` | What MetoYou/Toju is (user + architecture) |
| `02-user-journeys.md` | End-to-end journeys: login → rooms → chat → voice → multi-server |
| `03-architecture-map.md` | Domains, transports, identity model, key paths |
| `04-auth-login-bugs.md` | Login / authorize / silent provision failures |
| `05-signaling-multi-server.md` | Signal servers, WS drops, room affinity, presence |
| `06-voice-webrtc-bugs.md` | Voice, camera, screen-share, peer negotiation |
| `07-data-channel-drops.md` | Control-plane DC failures and recovery gaps |
| `08-messaging-visibility.md` | “Messages not seen” / sync / fallback |
| `09-identity-cross-signal.md` | Home vs foreign actor ids (calls, DMs, voice routing) |
| `10-code-lies-doc-debt.md` | Docs/lessons that disagree with the tree |
| `11-fix-priority-plan.md` | Ordered waves, acceptance, proof |
| `12-agent-work-packets.md` | Copy-paste packets for new chats |
| `13-validated-findings-fable-5-handoff.md` | Code-validated findings, corrected priorities, E2E limits, and implementation handoff |
| `e2e-failures/` | Latest full Playwright e2e run baseline (errors + slow tests) |
---
## Product in one paragraph (user view)
MetoYou (product client **Toju**) is a **desktop-first P2P chat app**: you log in once to a **signal server**, join **chat-servers** (communities with text + voice channels), talk in text, join voice, share screen/camera, and DM / call people. Media and most chat go **peer-to-peer over WebRTC**. The signal server only authenticates you, tracks who is in which room, and relays WebRTC offers / narrow chat & DM fallbacks. You may use **many signal servers**; the app is supposed to create a linked account on each new host **silently** after the first home login.
---
## Critical bug themes (user language)
1. **“It keeps asking me to log in”** even though Im already signed in (especially when joining something on another signal server).
2. **“Voice doesnt work / one-way / connecting forever”** — often after reconnect or when people registered on different signal servers.
3. **“Connection / data channel drops”** — chat, attachments, emoji, and sometimes voice die; recovery is silent or tears everything down.
4. **“I sent a message but they dont see it”** (or only some devices see it) — presence missing, P2P sync dead, or wrong identity.
5. **“Call rings nobody / Im In Voice alone”** — cross-signal identity mismatch on outbound or inbound paths.
6. **Different signal servers** make all of the above worse because **home user id ≠ foreign provisioned actor id**, and signaling is **not federated**.
---
## Related existing research (do not redo)
- Active research story: `agents-docs/user-stories/silent-cross-signal-server-auth.md`
- Feature contracts: `agents-docs/features/{authentication,signaling,voice-webrtc,messaging,server-directory}.md`
- Realtime deep dive: `toju-app/src/app/infrastructure/realtime/README.md`
- Lessons index: `agents-docs/LESSONS-INDEX.md` — tags `[auth] [realtime] [direct-call] [direct-message] [identity] [signaling]`
---
## Non-goals of this pack
- Implementing fixes (separate chats / packets).
- Rewriting the whole monorepo.
- Treating unit-green alone as “done” — each packet lists user-visible proof.
+118
View File
@@ -0,0 +1,118 @@
# 01 — Product overview (user + system)
## What the user thinks this app is
**Toju / MetoYou** is a Discord-like desktop chat app with:
- **Accounts** on a signal (signaling) server
- **Chat-servers** (communities) with text channels and voice channels
- **Live text chat** in those channels
- **Voice / camera / screen share** in voice channels
- **Direct messages** and **private calls**
- **Friends**, profile cards, custom emoji, GIFs, file attachments
- **Multiple signal servers** in the network settings (home + others)
The marketing promise implied by the product design:
> Log in once. Join communities anywhere on the network. Chat and voice “just work.” Switching signal hosts should not feel like logging into a second product.
---
## What the system actually is
| Layer | Role |
|-------|------|
| **Angular client** (`toju-app/`) | All UX, NgRx state, domain logic, WebRTC + WebSocket clients |
| **Electron shell** (`electron/`) | Desktop window, SQLite persistence, IPC (`window.api`), provision-secret safeStorage, screen capture helpers |
| **Signaling server** (`server/`) | Auth tokens, public server directory REST, WebSocket identify/join/presence, RTC offer/answer/ICE relay, narrow chat/DM/voice_state fallbacks, multi-device `account_sync`. **Does not store chat history.** |
| **Web / Capacitor** | Same Angular app; weaker secret storage (sessionStorage); mobile voice/UI constraints |
### Transport split (critical mental model)
| Transport | What the user experiences | What it actually carries |
|-----------|---------------------------|--------------------------|
| **WebSocket to signal server** | “Im online / in this server / someone joined voice” | Identity, room membership, presence, SDP/ICE relay, `chat_message` + DM fallbacks, `voice_state`, `account_sync` |
| **WebRTC media** | Hearing / seeing people | Mic, camera, screen tracks (never through the signal server) |
| **WebRTC ordered data channel** | Messages syncing, files, emoji, many “live” features | Chat events, inventory sync, attachments, avatar/emoji chunks, voice/screen control, plugin bus |
If the **socket** is wrong or unauthenticated → user is invisible; no relay; chat fallback may fail.
If the **data channel** is dead → live text may still limp via `chat_message`, but **history sync, attachments, emoji, many controls** fail.
If **media** is routed wrong or peer never connects → voice UI lies (“In Voice”) with silence.
---
## Core vocabulary (user ↔ engineering)
| User says | Engineering term |
|-----------|------------------|
| “My account / login” | Home session on `homeSignalServerUrl` + local profile in SQLite/IndexedDB |
| “Another network / signal host” | Foreign `ServerEndpoint` URL; needs per-URL credential |
| “A server / community” | Saved **Room** (chat-server) with `sourceUrl` / `sourceId` = which signal hosts it |
| “Text channel / voice channel” | Channels inside a room; text vs voice types |
| “Im in voice” | Local `VoiceSession` + `voice_state` with `isConnected` + mic ownership via `clientInstanceId` |
| “DM / call someone” | Direct-message conversation + optional `direct-call` session |
| “It logged me out” | Often `/login` or `/login?mode=authorize` — may be **foreign authorize**, not true home logout |
---
## Multi-signal identity (the footgun)
1. User registers on **Signal A** → home user id `H`.
2. User later needs **Signal B** → client is supposed to **auto-register/login** with a local **provision secret**, creating actor id `A_B` on B (username may be suffixed).
3. Peers on Signal B see the user as `A_B`, not `H`.
4. WebRTC peer map keys, `targetUserId` on relay, DM conversation ids, and call participant lists may mix `H` and `A_B`.
**Invariant the product claims:** one human, one local profile, many per-server actor credentials — UX never asks for password again except last-resort authorize / real home expiry.
**Invariant signaling claims:** non-federated — peers in the same room must share the **same signal endpoint** to discover each other. Cross-room “same person” is a client-side identity problem.
---
## Feature inventory (what “done product” includes)
Must work for a release-quality emergency fix:
1. Home register / login / logout / session restore
2. Silent foreign provision + Network settings badges
3. Discover / create / join / leave chat-servers (public, password, invite, moderated)
4. Text channels: send, edit, delete, react, typing, unread, sync after late join
5. Voice channels: join/leave, mute/deafen, speaking indicators, multi-device takeover, move between channels
6. Camera + screen share in voice / calls
7. DMs + friends + delivery states
8. Direct / group calls (ring, answer, decline, DND)
9. Attachments over data channel
10. Multi-device `account_sync` for saved rooms / chat batches / friends / avatar / emoji
11. Endpoint health, version compatibility, room signal affinity + fallback
Secondary (do not block P0 voice/auth/chat): plugins, game activity, themes, KLIPY, link previews, experimental media.
---
## Platforms
| Platform | Notes for bugs |
|----------|----------------|
| Electron desktop | Primary; provision secret in safeStorage; SQLite; best screen share |
| Browser | sessionStorage provision secret dies with tab; IndexedDB |
| Capacitor mobile | Auth routing, mic permissions, no reliable screen share; title bar hidden |
---
## Trust boundaries (short)
- REST mutations and WS `identify` require bearer/session token **per signal URL**.
- Actor user ids in request bodies are ignored server-side; token wins.
- Message bodies are **not E2E encrypted** beyond DTLS on WebRTC and TLS on WS.
- Signaling server is **not** the source of truth for chat history.
---
## Where truth lives (for agents)
| Kind of truth | Prefer |
|---------------|--------|
| Wire WebSocket types | `agents-docs/features/signaling.md` + `server/src/websocket/handler.ts`**not** `shared-kernel/signaling-contracts.ts` |
| Auth multi-server | `agents-docs/features/authentication.md` + `emergency-fix/04-*.md` |
| Voice/WebRTC plumbing | `toju-app/.../realtime/README.md` — verify against code (see `10-code-lies`) |
| Domain UX | `toju-app/src/app/domains/<name>/README.md` |
| Known past bugs | `agents-docs/LESSONS.md` — verify symbols still exist (some lessons describe **desired** fixes as if shipped) |
+256
View File
@@ -0,0 +1,256 @@
# 02 — User journeys (how it is supposed to feel)
Each journey: **steps the user takes**, **what should happen**, **failure symptoms**, **where to dig**.
---
## J1 — First-time home login
**User steps**
1. Open app → login/register.
2. Pick (or accept default) signal server.
3. Register or log in with username/password.
4. Land on dashboard / saved servers.
**Supposed to happen**
- Session token stored for that URL (`metoyou.authTokens` + credential store).
- Local user profile scoped in DB; NgRx `currentUser` set with `homeSignalServerUrl`.
- **Provision secret** created and persisted (Electron safeStorage / web sessionStorage).
- Signing public key registered on **home** server when possible.
- WebSocket connects, `identify` with home token + `clientInstanceId`, ready for joins.
**Broken looks like**
- Stuck on login; bounce back after “success”.
- Dashboard with no token → later `SESSION_EXPIRED`.
- Later foreign joins always open authorize (missing secret).
**Code**
- `domains/authentication/` (`AuthenticationService`, login/register UI)
- `store/users/users.effects.ts` (`authenticateUser`, `prepareAuthenticatedUserStorage`, `ensureHomeProvisionSecret`)
- Feature: `agents-docs/features/authentication.md`
---
## J2 — Restart app still logged in
**User steps**
1. Quit Electron fully; reopen.
2. Expect same user without typing password.
**Supposed to happen**
- Load user from local DB + valid home token (credential store or legacy token fallback).
- Identify on home (and later foreign) sockets.
- Opportunistic `ensureProvisioned` for active endpoints **without** login UI.
**Broken looks like**
- Flash of dashboard then `/login`.
- Profile restored but chat/presence dead (“alone”).
- Foreign rooms immediately open `/login?mode=authorize`.
**Code**
- `loadCurrentUser$`, `hasValidPersistedSession`, `migrateHomeCredential`
- Lessons: identify legacy token fallback; persisted user still needs token
---
## J3 — Join a community on the *same* signal server
**User steps**
1. Discover or invite → Join.
2. Open a text channel; send “hello”.
3. Join a voice channel; talk.
**Supposed to happen**
- REST join with bearer; then WS `join_server`.
- Receive `server_users` / `user_joined`; peer mesh forms.
- Text: local add + DC `chat-message` + WS `chat_message` fallback.
- Voice: `voice_state` broadcast; WebRTC offer/answer; same-channel audio routing.
**Broken looks like**
- Joined in UI but not in others member lists (identify/join race).
- Messages only on sender device.
- Voice tile appears, no audio.
---
## J4 — Join a community on a *different* signal server (critical)
**User steps**
1. Already logged into home Signal A.
2. Open invite / browser card whose `sourceUrl` is Signal B.
3. Join and chat/voice.
**Supposed to happen**
1. `ensureCredentialForServerUrl(B)` → silent provision (or reuse credential).
2. REST + WS use **actor id on B**, not home id.
3. No authorize login page.
4. Peers on B see actor display name (may show `#prefix` disambiguation).
5. Voice/chat use Bs WebSocket for that rooms affinity.
**Broken looks like**
- Redirect to `/login?mode=authorize&serverId=…` while user bar still shows logged in.
- Join “succeeds” locally but invisible on B.
- Can see members but WebRTC never connects (initiator/identity mismatch).
- DMs/calls to that person later miss rings or fork conversations.
**Code**
- `SignalServerAuthorizeService`, `SignalServerProvisionerService`, `room-signaling-connection.ts`
- Story: `agents-docs/user-stories/silent-cross-signal-server-auth.md`
- Pack: `04-auth-login-bugs.md`, `09-identity-cross-signal.md`
---
## J5 — Send a text message (server channel)
**User steps**
1. In text channel, type and send.
2. Peer in same room should see it live; late joiner should catch up after connecting.
**Supposed to happen**
- Optimistic local message with stable id (attachments bind to that id).
- Broadcast on data channel; also relay `chat_message` on signaling for peers without DC.
- Edits/deletes primarily P2P (+ `account_sync` to sibling devices).
- On peer connect: inventory ↔ sync-batch (up to 20k recent msgs, chunks of 200).
**Broken looks like**
- Sender sees it; others dont (no presence / no join / DC+fallback both fail).
- Others see live but not history (DC inventory never ran).
- Multi-device: one device has history, another empty (`account_sync` / identify).
- Attachments “Waiting for image…” forever (announce vs message ordering).
**Pack:** `08-messaging-visibility.md`
---
## J6 — Join voice in a channel
**User steps**
1. Click a voice channel.
2. Grant mic if prompted.
3. Hear others; they hear you; speaking indicators; optional camera/screen.
**Supposed to happen**
- Leave any previous voice/call first (exclusive).
- Publish `voice_state` with channel/server ids + `clientInstanceId`.
- Only one device owns mic (others passive; Join = takeover).
- Peer connections already for chat mesh; mic tracks attached only to same-channel peers.
- Playback only for peers in same voice channel.
**Broken looks like**
- “Connecting” forever.
- One-way audio.
- UI shows peers in channel but silent.
- After network blip: forever dead until full app restart.
- Works same-home, fails cross-home (initiator uses home id vs peer actor id).
**Pack:** `06-voice-webrtc-bugs.md`
---
## J7 — Call someone from DM / people card
**User steps**
1. Open DM or people card → Call.
2. Callee hears ring / sees modal (unless DND).
3. Answer → private call UI with optional chat panel.
**Supposed to happen**
- `direct-call` event delivered via PeerDelivery (DC then signaling).
- `targetUserId` = callees **currently connected signal identity**.
- Callee admission checks **all local aliases** (home + every provisioned actor id).
- Caller never sits “In Voice” if ring could not be delivered.
**Broken looks like**
- Caller In Voice; callee silent (outbound route null / wrong id).
- Callee never notified (inbound alias filter — partially fixed).
- Cross-signal: two DM threads; replies land in the “wrong” empty one.
**Pack:** `09-identity-cross-signal.md`
---
## J8 — Network blip / signal server restart
**User steps**
1. In voice + chat; WiFi blips or signal process restarts.
2. Continue without manual reconnect.
**Supposed to happen**
- WS reconnect with backoff; health probe forces fresh socket on instance change.
- `reIdentifyAndRejoin` then room resync.
- Peer disconnect grace 10s; then reconnect loop (~12 × 5s).
- DC close triggers repair; chat fallback covers live text meanwhile.
- Voice presence clears on dead voice-active disconnect server-side.
**Broken looks like**
- Zombie “online” with no events.
- Give-up after ~60s with **no error UI**.
- Docs promise soft DC replace; code tears down full peer (audio drop).
- Identify skipped → alone forever until manual leave/rejoin.
**Pack:** `05-signaling-multi-server.md`, `07-data-channel-drops.md`
---
## J9 — Two devices, same account
**User steps**
1. Desktop in voice; phone/browser also logged in.
2. Second device shows “in voice on another device”; can Takeover.
3. Chat/history appears on both.
**Supposed to happen**
- Distinct `clientInstanceId` per tab (sessionStorage).
- Broadcasts reach sibling connections; `account_sync` for owned state.
- Voice exclusive; takeover yields mic on old owner.
**Broken looks like**
- Tabs evict each other (shared clientInstanceId in localStorage — lesson says use sessionStorage).
- Second device never gets chat batches.
- Both think they own voice / neither transmits.
---
## J10 — Logout
**User steps**
1. Title-bar Logout (desktop) or Settings → Logout (mobile).
**Supposed to happen**
- Disconnect sockets; clear current user id; reset rooms/users/messages; `/login`.
**Broken looks like**
- Stale credentials for foreign URLs linger and confuse next account (verify credential clear scope when fixing auth).
+154
View File
@@ -0,0 +1,154 @@
# 03 — Architecture map (for agents)
## Bounded contexts (product client)
See `toju-app/src/app/domains/README.md`. Emergency-relevant domains:
| Domain | Owns |
|--------|------|
| `authentication` | Login/register HTTP, provision secret, per-URL credentials, authorize navigation |
| `server-directory` | Endpoints, health, discovery, invites, room metadata affinity |
| `chat` | Message rules, sync rules, chat UI |
| `direct-message` | DMs, friends, offline queue, PeerDelivery usage |
| `direct-call` | Private call sessions / rings |
| `voice-session` | Join/leave bookkeeping, floating controls, settings storage |
| `voice-connection` | Facade over realtime for mic/camera/playback/VAD |
| `screen-share` | Picker / quality |
| `attachment` | Chunked P2P files |
| `access-control` | Permissions / bans |
**Infrastructure (not a domain):** `infrastructure/realtime/` (WebRTC + signaling), `infrastructure/persistence/`, `infrastructure/mobile/`.
**Global NgRx:** `store/users`, `store/rooms`, `store/messages` — orchestration across domains.
---
## Identity & credentials
```
Home profile (NgRx User)
id / username / displayName / homeSignalServerUrl
├─ AuthTokenStore (legacy per-URL token) metoyou.authTokens
├─ SignalServerCredentialStore metoyou.signalServerCredentials
│ { serverUrl → userId, token, provisioned? }
└─ ProvisionSecretStore (per home user id)
Electron safeStorage | web sessionStorage
```
**Actor resolution for a room:** `SignalServerAuthService.resolveActorUserIdForServer(sourceUrl, homeOderId)` — foreign URL must use provisioned `userId`.
**Identify on socket:** `SignalingTransportHandler.getIdentifyCredentialsForSignalUrl(url)` must resolve token+actor for that URL (store fallback).
**Danger:** `getIdentifyCredentials()` returns **home** credential — used for some localOderId / polite-peer paths → cross-signal initiator bugs (`06`, `09`).
---
## Room ↔ signal affinity
```
Room { id, channels[], sourceUrl, sourceId, ... }
RoomSignalingConnection
ensureCredentialForServerUrl(sourceUrl)
identify(actor for sourceUrl)
join_server / view_server on that WS
ServerSignalingCoordinator
maps serverId ↔ signalUrl ↔ peers
```
Fallback: try other online compatible endpoints on outage; **do not** treat Cloudflare 521/522 as “client incompatible”. Non-federated: peers must converge on same endpoint for that room.
---
## Realtime composition root
`WebRTCService` (`realtime-session.service.ts`) wires:
- `SignalingTransportHandler` → many `SignalingManager` (one WS per URL)
- `PeerConnectionManager` → negotiation, DC, recovery
- `MediaManager` / noise / screen share
- `WebRtcStateController` (signals)
Inbound WS → `SignalingMessageHandler` → users/rooms/voice/chat effects.
---
## Message send path (server channel)
```
Composer → MessagesActions.sendMessage({ id? })
→ local DB + NgRx
→ DC broadcast chat-message (+ message-revision)
→ WS chat_message fallback (room members)
→ account_sync to sibling devices
```
Receive gates: room must be current or saved; channel scoping for text.
---
## Voice join path
```
UI join channel
→ VoiceSessionFacade startSession
→ leave previous exclusive target
→ enableVoice / heartbeat
→ voice_state on WS (+ DC control)
→ MediaManager.syncVoiceRouting (same channel only)
→ VoicePlaybackService for remote same-channel peers
```
Peer PC may already exist from presence mesh; join mainly attaches tracks + announces state.
---
## Call / DM delivery path
```
DirectCallService / DirectMessageService
→ PeerDeliveryService
1) data channel if open
2) signaling forward (targetUserId)
3) offline queue (DM) / silent fail (call if ignored)
```
Inbound call admission: `direct-call-participant-identity.rules.ts` (aliases — implemented).
Outbound routable id pick: **documented in LESSONS as fixed; symbols not in tree** — see `10-code-lies`.
---
## Recovery constants (cheat sheet)
| Constant | Value |
|----------|-------|
| WS reconnect backoff | 1s → 30s |
| Connect timeout | 5s |
| Keepalive interval / ack timeout | 25s / 10s |
| Health probe | 5s |
| Peer disconnect grace | 10s |
| Peer reconnect | 5s × 12 then silent abandon |
| DC recovery grace (closing) | 2.5s |
| Non-initiator give-up | 5s |
| Offer-sent grace | 20s |
| Inventory / full sync limit | 20_000 |
| Sync batch | 200 |
| Sync poll | 10s fast / 15 min slow |
Source: `realtime.constants.ts`, `message-sync.rules.ts`.
---
## Server responsibilities (only when needed)
Default agent scope excludes `server/` unless packet expands. Know this:
- Serializes WS handlers per connection (identify-before-join).
- Relays RTC only when peers share membership (DM types exempt).
- No message persistence.
- `voiceActive` routes offers to owning connection.
Canonical envelopes: `agents-docs/features/signaling.md`.
+119
View File
@@ -0,0 +1,119 @@
# 04 — Auth & login bugs
> **User theme:** “Im logged in but it keeps asking me to log in / authorize.”
> **Severity:** P0 — blocks multi-server chat, voice, presence.
> **Existing research:** `agents-docs/user-stories/silent-cross-signal-server-auth.md` (do not redo; implement after interview).
---
## How auth is supposed to work (user view)
1. Register or log in **once** on a home signal server.
2. Stay signed in across app restarts (desktop).
3. When touching another signal server (join, invite, create room, activate endpoint): the app **silently** creates or reuses an account there.
4. You only see a login form again if:
- true home session expired / missing token, or
- last-resort: username collision exhaustion / user clicked Sign in in Network settings.
5. Offline / dead endpoints never bounce you to authorize login.
Settings → Network may show `Authorized` / `Needs sign-in` as diagnostics — not as the default path for normal joins.
---
## How auth is supposed to work (system)
| Concept | Role |
|---------|------|
| Home credential | Token + user id for `homeSignalServerUrl` |
| Foreign credential | Separate user id + token per URL in `SignalServerCredentialStore` |
| Provision secret | Password used only for auto register/login on foreign hosts |
| `ensureProvisioned` | Register-or-login with secret; suffix username on collision |
| `ensureCredentialForServerUrl` | Gate before foreign room connect; navigate authorize only for `collision` or `no-provision-secret` when endpoint online |
| `authorize` mode | Manual login that upserts foreign credential **without** resetting home profile |
| `auth_required` vs `auth_error` | Race vs rejected token — must not falsely expire home on foreign races |
Authorize nav rule: `shouldNavigateToAuthorizeSignalServer` in `signal-server-authorize.rules.ts`.
---
## Failure modes (code-backed)
### A — Missing provision secret → authorize UI (primary)
**Symptom:** Logged-in user opens foreign room/invite → `/login?mode=authorize`.
**Mechanism**
1. `ensureProvisioned``{ kind: 'skipped', reason: 'no-provision-secret' }`.
2. `ensureCredentialForServerUrl` navigates to authorize.
3. Authorize mode does not auto-leave when `currentUser` exists → stuck prompt.
**Why secret missing**
- Created only in `prepareAuthenticatedUserStorage` when `homeSignalServerUrl` + `loginResponse` present.
- Session restore calls `ensureProvisioned` but **does not** ensure secret exists first.
- Web: sessionStorage secret dies with tab.
- Old installs / wiped Electron `userData/provision-secrets/`.
**Files**
- `signal-server-auth.service.ts`, `signal-server-authorize.service.ts`
- `provision-secret-store.service.ts`, `electron/api/provision-secret-store.ts`
- `users.effects.ts`
### B — Username collision exhaustion
All register candidates 409 + login 401 → `collision` → authorize. Rare but real on crowded foreign servers.
### C — False home session expiry
`signalServerAuthFailed$` may `SESSION_EXPIRED` → full `/login` when home classification / retry budget wrong. Distinguish `auth_required` (re-identify) vs `auth_error` (clear credential; foreign re-provision vs home expire).
### D — Credential missing → unauthenticated join → invisible user
Without token, socket never identifies; `join_server` dropped; user alone; later gates open authorize. Cascades into “messages not seen” and “voice empty”.
### E — Opportunistic provision swallows errors
`ensureProvisioned(...).catch(() => undefined)` on health/startup → failure deferred until mid-join authorize popup.
### F — Others?
---
## Doc / README lies in this area
| Claim | Reality |
|-------|---------|
| Auth domain README sequence: `POST /api/auth/login` | Real paths: `/api/users/login`, `/api/users/register` (`AuthenticationService`) |
| Feature doc: offline must not open authorize | Code path exists; still fails open on missing secret when “online” |
---
## Fix directions (interview choices — not approved)
From user story (recommend **A**):
| Option | Idea |
|--------|------|
| **A** | On restore / before foreign provision, always `ensureHomeProvisionSecret`; keep authorize only for collision / manual |
| **B** | Never navigate on `no-provision-secret`; toast + Network badge |
| **C** | Durable web secret (not sessionStorage) |
| **D** | Stronger unique usernames before collision UI |
---
## Proof of done
1. Two live signal servers: home register on A → join room on B → **no** `/login` navigation.
2. Full Electron restart → foreign rejoin still silent.
3. Offline foreign URL → no authorize navigation.
4. Focused tests: missing secret on restore → secret created → provision → `Router.navigate(['/login'])` never called.
5. Foreign `auth_error` with valid home → re-provision, not `SESSION_EXPIRED`.
---
## Agent scope
- Default: `toju-app/domains/authentication`, `store/users`, `store/rooms/room-signaling-connection.ts`, server-directory call sites.
- Electron: provision-secret store/IPC only if persistence fix needs it.
- Ask before deep `server/` auth changes.
+112
View File
@@ -0,0 +1,112 @@
# 05 — Signaling & multi-signal-server connection
> **User theme:** “Im connected but nobody sees me / rooms flicker / different signal servers break everything.”
> **Severity:** P0 for presence and as root cause of voice + chat visibility.
---
## How signaling is supposed to work (user view)
- When you open a community, you appear in the member list for everyone in that community.
- Leaving / going offline shows you as offline (without duplicate leave spam for multi-device).
- Switching communities updates who you see without wiping presence for other saved servers.
- If the signal host restarts or WiFi blips, you come back automatically.
- Communities hosted on different signal hosts still work as long as your client is authorized on that host (see `04`).
- Users should be able to see/sync chats/files/profile images/server states info change/plugin states and events/talk without losing connectivity or only some users are seen and other aren't.
---
## How signaling is supposed to work (system)
1. **One WebSocket per signal URL** (`SignalingManager`).
2. **Identify first** with that URLs token + actor `oderId` + `clientInstanceId`.
3. Then `join_server` / `view_server` for rooms on that URL.
4. Server serializes handlers per connection so join cannot race mid-identify.
5. Client `reIdentifyAndRejoin` on reconnect; rooms effects resync as safety net.
6. Room traffic prefers room `sourceUrl`; temporary fallback to other compatible endpoints on outage.
7. **Non-federated:** Signal A does not share peer registry with Signal B. Same human on two hosts = two actor ids (client glue).
Canonical catalog: `agents-docs/features/signaling.md`.
Client map: `infrastructure/realtime/README.md` + `signaling/`.
---
## Failure modes
### 1 — Identify / join race or skipped identify
**Symptom:** Local UI shows room open; others dont see you; you miss `user_joined` / `chat_message`; “alone”.
**Causes**
- Join sent before identify (fixed server-side serialization + client reIdentify — regressions still dangerous).
- `getIdentifyCredentialsForSignalUrl` returns null (no credential) → no identify.
- Identify cache empty on fresh socket without store fallback (lesson: must fall back to credential store).
### 2 — Wrong socket / wrong affinity
**Symptom:** Presence or RTC relay never reaches peers in the room; works only when everyone shares the same endpoint URL alias.
**Causes**
- Room `sourceUrl` stale vs directory `serverInstanceId` canonicalization.
- Fallback broadcast regressions (raw room messages must not spam every manager when route unknown — README describes current intended behavior).
- Cold start reconnect before health probes collapse aliases.
### 3 — Multi-signal leave tears down wrong peers
**Symptom:** Leaving a room on signal-sweden drops a peer still shared via signal.toju.app.
**Intended:** `user_left` subtracts only that clusters shared servers; preserve routes while P2P still live.
### 4 — Half-open / zombie sockets
**Symptom:** UI “connected”; no events after server process restart.
**Mitigations in tree:** keepalive ack timeout after first ack; `/api/health` probe; `serverInstanceId` change forces new WS. Regressions: skipping first heartbeat tick, treating 521/522 as incompatible.
### 5 — Auth failure cascade
Foreign `auth_required` / `auth_error` without silent re-provision → authorize UI or invisible membership (`04`).
### 6 — Multi-device eviction loop
Shared `clientInstanceId` in localStorage across tabs → server evicts sibling on identify. Must stay in **sessionStorage**.
---
## Different signal servers — user-visible contract
| Situation | Expected |
|-----------|----------|
| Two users, same room, same signal URL | Discover each other; RTC relay OK |
| Two users, “same” community mirrored on different signals | **Not supported as one mesh** — they are different rooms unless directory/affinity converges them |
| One user, rooms on A and B | Two sockets; two actor ids; local profile one |
| DM/call across people who met on foreign rooms | Must address **connected** actor id (see `09`) |
---
## Key files
- `signaling.manager.ts`, `signaling-transport-handler.ts`, `server-signaling-coordinator.ts`
- `signaling-message-handler.ts`, `server-membership-signaling-handler.ts`
- `room-signaling-connection.ts`, `rooms.effects.ts`
- `server-directory` health / canonical endpoint rules
- Server: `server/src/websocket/handler.ts` (out of default scope — ask)
---
## Proof of done (signaling)
1. Two clients join same room: both appear in member lists within seconds of identify.
2. Kill signal process; both recover presence without manual leave/rejoin.
3. User with rooms on two URLs: leave on URL A does not remove voice peer still shared only via URL B.
4. Fresh connect: first outbound after open is identify; join never accepted unauthenticated (server log / test).
5. Alias URLs with same `serverInstanceId` collapse before room reconnect.
---
## Fix order notes
Usually fix **auth provision (`04`)** before deep signaling surgery — many “connection” bugs are missing identify. Then harden identify credential resolution and affinity. Instrument `connectionScope`, actor id, and signal URL on every join in debug builds.
+130
View File
@@ -0,0 +1,130 @@
# 06 — Voice & WebRTC bugs
> **User theme:** “Voice is broken / one-way / connecting forever / works until reconnect.”
> **Severity:** P0.
---
## How voice is supposed to work (user view)
1. Click a voice channel (or answer a call).
2. Mic turns on (unless muted); you appear in the channel roster for others.
3. You hear everyone in **that** channel; they hear you.
4. Mute / deafen / camera / screen share behave Discord-like.
5. Navigate away → floating controls; voice continues.
6. Joining another voice target auto-leaves the previous one.
7. Second device shows youre in voice elsewhere; Join takes over.
8. After a short network blip, voice returns without restarting the app.
Voice is **not** supposed to depend on the data channel for audio itself — but DC carries control/state; broken DC recovery currently **rebuilds the whole peer**, which drops media too (`07`).
The users should always be able to hear each other and see each other in joined calls and voice channels. (Not 1 out of 6 cant be heard for 3 users!)
---
## How voice is supposed to work (system)
| Piece | Responsibility |
|-------|----------------|
| `voice-session` | Session metadata, floating UI, settings, exclusivity, takeover rules |
| `voice-connection` | Facade, VAD, per-peer playback gain |
| `MediaManager` | getUserMedia, RNNoise, gain, same-channel track routing |
| `PeerConnectionManager` | RTCPeerConnection, negotiation, ICE |
| Signaling | `offer`/`answer`/`ice_candidate` relay; `voice_state`; `voice_client_takeover` |
| ICE | STUN defaults; TURN optional via settings — **no bundled TURN** |
Initiator election: deterministic compare of local id vs peer `oderId` so only one side offers.
Audio routing: attach/detach mic based on matching `voiceState.roomId` + `serverId`. Playback similarly scoped.
---
## Failure modes
### 1 — Cross-signal initiator / politeness uses home id
**Symptom:** Peers never connect or glare forever when users have different home servers / foreign actor ids in the room.
**Mechanism**
- Presence peer ids = **per-server actor** `oderId`.
- `getLocalOderId` / polite-peer path often uses `getIdentifyCredentials()`**home** id (`signaling-transport-handler.ts`, `realtime-session.service.ts`, `negotiation.ts`).
- Election `localOderId < peerId` inconsistent across clients → dual offer, dual wait, or stuck non-initiator.
**Fix direction:** elect and politeness using **per-signal-url** identify credentials (`getIdentifyCredentialsForSignalUrl(peerSignalUrl)` / room source URL), not home-only.
### 2 — Voice allow-list misses peer map key → one-way / silence
**Symptom:** Connected peer, speaking UI maybe wrong, no audio out or in.
**Mechanism:** `syncOutgoingVoiceRouting` / playback only recognizes certain aliases (`id` / `oderId` / `peerId`). If `activePeerConnections` key is another alias → track detached.
### 3 — Silent give-up after reconnect budget
**Symptom:** After ~60s of failures, voice never returns; no error toast.
**Constants:** `PEER_RECONNECT_MAX_ATTEMPTS = 12`, interval 5s. Tracker deleted; no UI.
### 4 — Transient signaling drops offers during WS reconnect
Offers/ICE classified transient may be deferred/dropped while socket reconnecting → half-open peers; depends on fallback offer timers (`USER_JOINED_FALLBACK`, non-initiator give-up 5s).
### 5 — DC recovery tears down media
Closed control channel → `removePeer` + full reconnect (`peer-recovery.ts`). Docs claim soft `replaceDataChannel` preserving AV — **code does not do that** (`07`, `10`).
### 6 — Auth / presence missing
If identify/join failed (`04`/`05`), no RTC relay eligibility / no peer discovery → empty voice.
### 7 — Same-channel filter false negatives
Remote `voice_state` missing/stale channel ids → locally mute peer while UI still lists them in channel.
### 8 — No TURN by default
Symmetric NAT / strict firewalls fail ICE with STUN-only. User-configurable TURN exists; many installs never set it. Product decision needed: ship defaults vs document limitation.
### 9 — Multi-device ownership races
Takeover / heartbeat / `voiceActive` routing wrong → offers hit passive device; active device silent.
---
## Code vs docs
| Doc claim | Code |
|-----------|------|
| Soft DC renegotiation preserves media | Always full peer recreate on closed DC |
| Deterministic initiator from logical peer ids | Uses home identify credentials in several paths |
| TURN supported | Configurable only; not default |
---
## Key files
- `domains/voice-session/`, `domains/voice-connection/`
- `infrastructure/realtime/media/media.manager.ts`
- `peer-connection-manager/**`, `negotiation.ts`, `peer-recovery.ts`
- `signaling-message-handler.ts` (server_users / user_joined offers)
- `ice-server-settings.service.ts`, `realtime.constants.ts`
- Feature: `agents-docs/features/voice-webrtc.md`
---
## Proof of done
1. Two users same home signal, same voice channel: bidirectional audio < 5s after both join.
2. Two users **different home signals**, same foreign-hosted room (both provisioned): bidirectional audio.
3. Toggle mute/deafen; camera; screen share request path.
4. Kill WiFi 15s: recovers or shows actionable error (not silent forever).
5. Second client takeover: first stops transmitting; second owns mic.
6. Regression test for initiator election with mismatched home vs actor ids.
---
## Interview prompts (when implementing)
- Soft DC replace vs keep full rebuild but fix media reattach + UX error?
- Ship public TURN defaults or document “requires open NAT / user TURN”?
- Instrument-only first week vs behavior fix first?
+99
View File
@@ -0,0 +1,99 @@
# 07 — Data channel drops & recovery
> **User theme:** “Connection drops / chat and files die / voice dies after a blip.”
> **Severity:** P0/P1 — shared control plane for chat sync, attachments, emoji, screen control, and currently media (because recovery rebuilds the whole PC).
---
## How the data channel is supposed to work (user view)
- Once youre in a community with other people, messages, files, emoji, and many live updates “just sync.”
- Brief network glitches should self-heal.
- You should not need to restart the app to get chat syncing again.
- Voice should ideally survive control-plane blips (product docs claim this; code currently does not).
- Users should sync all but only load a portion into ram for viewing so the app doesn't crash of high ram usage.
---
## How the data channel is supposed to work (system)
- Single **ordered** RTCDataChannel per peer pair (label typically chat/control).
- Carries: chat events, inventory sync, attachments, avatar/emoji chunks, voice/screen control, pings, plugin bus, game activity, etc.
- Back-pressure: high 4MB / low 1MB watermarks.
- Ping every 5s for RTT.
- On failure, recovery should restore control **and** ensure inventory/resync runs when channel reopens.
### Documented recovery (README / voice-webrtc.md)
1. Non-fatal error on **open** channel → request voice-state snapshot on same channel.
2. **Closed** channel → initiator renegotiates **new DC on existing PC** (preserve AV); non-initiator waits then full rebuild if missing.
3. Closing-but-not-closed → short grace (2.5s).
4. `replaceDataChannel` adopts the new channel.
### Actual recovery (`peer-recovery.ts`)
1. Closed → `repairUnavailableDataChannel``removePeer` + `attemptPeerReconnect` / `schedulePeerReconnect` (**full PC teardown**).
2. Closing → wait `DATA_CHANNEL_RECOVERY_GRACE_MS` (2.5s) → same full recreate.
3. `replaceDataChannel` exists on the manager and is wired into handlers, but **recovery path never calls it**; specs assert it is **not** called in several cases.
4. After 12 reconnect attempts (~60s): abandon **silently**.
**This is a documented lie** — treat README paragraph as aspirational until code matches or docs are corrected in the same PR as a deliberate decision.
---
## Failure modes
### 1 — Full rebuild drops audio/video on every DC close
User hears a “drop” even when ICE media might have survived. Cascades into voice bug reports.
### 2 — Silent abandon
No toast, no “Reconnect” CTA, no automatic retry on later `user_joined`. Mesh looks permanently broken until navigation/restart.
### 3 — Live chat limp vs history dead
While DC down, `chat_message` WS fallback may still deliver **live** text. Inventory sync is **DC-only** → late joiners / catch-up fail until P2P returns (`08`).
### 4 — Large payloads kill shared channel
Custom emoji / attachment floods can stress or close the shared ordered channel (lessons). One feature outage becomes total control-plane outage.
### 5 — Attachment announce vs message ordering
`file-announce` on DC can beat `chat-message` on WS → auto-download gives up unless re-queued on message bind (lesson; verify still present when touching attachments).
### 6 — Replacement channel race
If soft-replace is reintroduced, must close old channel to release SCTP (voice-webrtc changelog). Current full rebuild avoids that class but at higher cost.
---
## Key files
- `peer-connection-manager/messaging/data-channel.ts`
- `peer-connection-manager/recovery/peer-recovery.ts` (+ specs)
- `peer-connection.manager.ts` (`replaceDataChannel`)
- `realtime.constants.ts`
- Consumers: chat sync effects, attachment transfer, custom emoji chunking, screen-share request
---
## Fix directions (interview)
| Option | Idea | Tradeoff |
|--------|------|----------|
| **A (align code to docs)** | Implement true soft DC replace on connected PC; full rebuild only if PC not connected | Harder; matches user expectation for voice survival |
| **B (align docs to code)** | Keep full rebuild; fix reattach + force inventory on reopen; **surface give-up UX** | Faster; still interrupts voice |
| **C** | Separate unreliable channel for bulk (files/emoji) vs reliable small control | Larger design |
Recommend starting with **instrumentation + Bs UX/resync**, then **A** if voice drop rate stays high.
---
## Proof of done
1. Force-close DC in debug: control messages resume; inventory runs; user sees progress or success.
2. If soft-replace chosen: audio continues through DC replace (automated or manual with metrics).
3. After max attempts: visible error + manual retry works.
4. Attachment + emoji transfer during recovery does not deadlock the mesh.
5. Update `realtime/README.md` + `voice-webrtc.md` in the same change set so they match behavior.
+110
View File
@@ -0,0 +1,110 @@
# 08 — Messaging visibility (“messages not seen”)
> **User theme:** “I sent a message but they dont see it / history missing / only some devices have chat.”
> **Severity:** P0.
---
## How messaging is supposed to work (user view)
### Server text channels
- Send in a text channel → everyone currently in that community sees it quickly.
- If someone was offline or just joined, they still get recent history after connecting to peers (or from their other logged-in device).
- Edits, deletes, reactions converge.
- Typing indicators are ephemeral.
- The **signal server does not keep a chat log** — history lives on clients.
### Direct messages
- 1:1 and group DMs deliver even without a shared community when signaling can reach the peer.
- Delivery ticks: queued → sent → delivered → acknowledged (monotonic).
- Offline: queue until peer/network returns.
### Multi-device
- Second device receives live + catch-up via `account_sync` when siblings are online.
---
## How messaging is supposed to work (system)
| Path | Role |
|------|------|
| P2P `chat-message` / revisions | Primary live + sync plane |
| WS `chat_message` | Narrow **live** fallback to room members |
| Inventory / sync-batch | Catch-up on DC (limit 20_000, chunk 200) |
| `account_sync` chat batches | Sibling devices |
| DM PeerDelivery | DC → signaling → offline queue |
Feature contract: `agents-docs/features/messaging.md`.
Domain: `domains/chat/`, `domains/direct-message/`, `store/messages/`.
---
## Failure modes
### 1 — Invisible membership → no live fallback
If identify/join failed (`04`/`05`), user is not in server membership → server never broadcasts `chat_message` to them; peers may not offer DC. **Classic “chats dont sync for multi-client users”** root cause (serialized identify).
### 2 — Live works, history doesnt
DC down: live WS fallback OK; inventory never runs → “they only see new messages after refresh if a peer happens to sync later” / empty history for late joiners.
### 3 — Sync poll too slow after “clean” cycle
Fast poll 10s while catching up; **15 min** when clean. A false “clean” leaves long windows without repair.
### 4 — Cross-signal identity forks DMs
Incoming DM `conversationId` carries foreign actor id → second empty thread; replies invisible on the thread the user is watching (`09`). Lessons claim canonicalize/merge — **symbols not in tree**.
### 5 — Recipient alias miss
DM/call ignored if local admission only checks home id. Inbound DM aliases largely fixed; verify call + any new surfaces.
### 6 — Attachment-only emptiness
Message text arrives; image stuck “Waiting…” — announce/bind race (lesson). Looks like “message incomplete / not really received.”
### 7 — NgRx prune confusion
Inactive rooms pruned to 100 messages in memory; DB still has more. User switching rooms may think history vanished until reload/sync — document vs bug.
### 8 — Doc lie on inventory cap
Chat domain README still says **1000**; code `INVENTORY_LIMIT = 20_000`.
---
## Investigation checklist (agents)
1. Did both users `identify` + `join_server` on the **same** signal URL as the room?
2. Is there an open DC between them? (`connectedPeers`, debug metrics)
3. Does live send emit both DC and `chat_message`?
4. On receive, is `roomId` in saved/current rooms?
5. For DM: conversation id aliases; delivery state machine stuck at QUEUED?
6. Multi-device: `account_sync_peer_online` fired; batches received?
---
## Proof of done
1. Two clients: send N messages with DC disabled (force) → live still appears via WS.
2. Re-enable DC → inventory brings missing history.
3. Third client late join → receives recent history from a peer with DB.
4. Cross-home users in foreign room: messages visible both ways without authorize prompt.
5. Cross-signal DM: single conversation thread; replies visible to both.
6. Multi-device: second device gets `chat-sync-batch` after identify.
7. Regression covering identify-before-join (presence + chat broadcast).
---
## Fix order
1. Auth + identify (`04`/`05`) — without presence, messaging “fixes” are theater.
2. Ensure fallback + DC resync on repair (`07`).
3. Identity canonicalize for DMs (`09`).
4. Attachment re-queue invariants.
5. Correct stale chat README inventory number.
+113
View File
@@ -0,0 +1,113 @@
# 09 — Cross-signal identity (calls, DMs, voice routing)
> **User theme:** “Calls dont ring / Im In Voice alone / DMs fork / voice fails only with people on other homes.”
> **Severity:** P0.
> **Coupling:** Depends on silent provision (`04`) creating foreign actor ids at all.
---
## How identity is supposed to work (user view)
- You have one profile on this device.
- People may see slightly different usernames on other signal hosts (suffix / `#prefix` tag) — still you.
- Calling or DMing someone you met in any community should reach **them**, not a ghost.
- You should never end up with two chat threads that are secretly the same person.
- Voice in a shared room should work even if you registered on different home signal servers.
---
## How identity is supposed to work (system)
| Id | Meaning |
|----|---------|
| Home user id | Local profile / NgRx `User.id` |
| Foreign actor id | Credential `userId` for that signal URL |
| Peer map key | Usually the `oderId` seen on that signals presence |
| `targetUserId` on WS relay | Must equal callees **connected** `oderId` on that server |
| DM `conversationId` | Should canonicalize to one thread per human pair on this device |
Self-admission for inbound events must accept **all** aliases: home id, entity id, peer id, every valid provisioned credential user id.
Outbound delivery must pick a **routable** id (one the signaling server can map to an open connection), not merely the home id stored on a people card.
---
## What is implemented vs claimed
| Capability | Status (2026-08-12 tree) |
|------------|--------------------------|
| Inbound direct-call alias admission + normalize | **Present**`direct-call-participant-identity.rules.ts` |
| DM self-id alias sets | **Present**`direct-message-identity.rules.ts` (narrower than lessons conversation merge) |
| Outbound `collectRecipientDeliveryCandidateIds` / `pickRoutableRecipientId` | **Absent** — lesson describes as if shipped |
| `DirectCallService.resolveRoutableRecipientId` / `recipientUnreachable` UX | **Absent** |
| `resolveDirectConversationId` / `mergeAliasDirectConversations` | **Absent** |
| Room join uses `resolveActorUserIdForServer` | **Present**`room-signaling-connection.ts` |
| Initiator election uses per-signal actor id | **Weak / home-biased** — see `06` |
**Treat LESSONS entries for outbound call routing and DM canonicalize as specifications of unfinished work**, not as completed history.
---
## Failure modes
### 1 — Outbound call: silent “In Voice”
Caller joins call session; `PeerDeliveryService` cannot resolve signaling peer id (home id ≠ connected actor id); ring never sent or wrong `targetUserId`; delivery result ignored.
### 2 — Inbound call: dropped ring (historical)
Fixed for admission aliases; still verify e2e `dm-header-call-ring` across secondary signal registration. Do not assume outbound is fixed because inbound is.
### 3 — DM thread fork
Incoming messages keyed by foreign actor conversation id → UI shows empty home-id thread; replies land elsewhere.
### 4 — Voice peer election / routing alias miss
Home vs actor mismatch → no PC or one-way audio (`06`).
### 5 — People search / friends store home ids only
Cards display home identity; without alias expansion at send time, every cross-signal action is fragile.
---
## Key files
- `domains/direct-call/` (+ `direct-call-participant-identity.rules.ts`)
- `domains/direct-message/` (+ `direct-message-identity.rules.ts`, `PeerDeliveryService`)
- `domains/authentication/` credential store + `resolveActorUserIdForServer`
- `infrastructure/realtime/signaling-transport-handler.ts` (home vs per-URL credentials)
- Lessons (aspirational): outbound routing + DM canonicalize sections in `LESSONS.md`
- E2E intent: `e2e/tests/voice/dm-header-call-ring.spec.ts` (scope expand if needed)
---
## Recommended implementation sequence
1. **Instrument** one cross-home repro: log home id, actor ids, peer map keys, `targetUserId`, conversation ids.
2. **Outbound delivery** — implement lesson APIs for real: collect aliases, pick routable, always attempt send, surface unreachable error (stop fake In Voice).
3. **DM canonicalize + merge** — single thread; remap inbound; merge duplicates on load.
4. **Voice initiator / routing** — per-signal local actor id (`06`).
5. Rewrite lesson examples if names differ so future agents dont hunt phantom files.
---
## Proof of done
1. User1 home A, User2 home B, meet in room on A: DM-header call rings Bs modal; B answers; both hear audio.
2. Same pair: DM replies appear in **one** thread on both clients.
3. People-card call when only shared presence is under actor id: still rings or shows **unreachable** (never silent In Voice).
4. Unit tests for alias collect/pick and conversation merge.
5. E2E cross-signal call + DM (expand scope with user approval for `e2e/`).
---
## Interview choices
| Option | Focus first |
|--------|-------------|
| **A (recommended)** | Outbound routable id + unreachable UX |
| **B** | DM canonicalize/merge first |
| **C** | Voice initiator per-signal id first |
| **D** | Full identity service refactor (large — avoid in emergency) |
+101
View File
@@ -0,0 +1,101 @@
# 10 — Code lies & documentation debt
> Agents must **verify symbols in the tree** before trusting lessons or READMEs. This file lists known mismatches found 2026-08-12.
---
## Critical lies (behavior-affecting)
### L1 — Soft data-channel replace (docs yes, code no)
**Claims:** `toju-app/.../realtime/README.md` (Data channel section), `agents-docs/features/voice-webrtc.md`.
**Reality:** `repairUnavailableDataChannel` always `removePeer` + reconnect. `replaceDataChannel` not used by recovery; specs expect it not called.
**Action:** Either implement soft replace or rewrite docs in the same PR as the recovery decision (`07`).
### L2 — LESSONS outbound call routing “fixed”
**Claims:** `LESSONS.md``peer-delivery-identity.rules.ts`, `collectRecipientDeliveryCandidateIds`, `pickRoutableRecipientId`, `resolveRoutableRecipientId`, `call.errors.recipientUnreachable`.
**Reality:** **No matches** in `toju-app/`. Phantom APIs.
**Action:** Implement (`09`) or rewrite lesson as “desired / unfinished” with Status.
### L3 — LESSONS DM conversation canonicalize “fixed”
**Claims:** `resolveDirectConversationId`, `mergeAliasDirectConversations`, `direct-message-conversation-identity.rules.ts`, etc.
**Reality:** **Absent.** Only narrower `direct-message-identity.rules.ts`.
**Action:** Same as L2.
### L4 — Home identify credentials used as “local peer id” for negotiation
**Claims:** Deterministic initiator from logical peer ids (implies per-room identity).
**Reality:** `getIdentifyCredentials()` returns **home** credential; used for polite peer / localOderId paths while presence uses actor ids.
**Action:** Fix in voice/identity packets (`06`/`09`); update README after.
---
## Medium lies (wrong paths / stale numbers)
### L5 — Authentication domain README API paths
**Claims:** `POST /api/auth/login`, `/api/auth/register`.
**Reality:** `/api/users/login`, `/api/users/register`.
### L6 — Chat domain README inventory cap
**Claims:** capped at **1 000** messages.
**Reality:** `INVENTORY_LIMIT = 20_000` (messaging feature doc correct).
### L7 — `shared-kernel/signaling-contracts.ts` as wire authority
**Claims (implicit):** types like `join` / `leave` / `chat` / `ice-candidate`.
**Reality:** Feature `signaling.md` correctly says **do not treat as authoritative**. Live types use `join_server`, `chat_message`, `ice_candidate`, etc.
**Action:** Mark file deprecated or align types; never generate client sends from it blindly.
---
## Soft / incomplete docs (not lies, but traps)
| Topic | Note |
|-------|------|
| Auth feature doc vs missing secret | Doc describes intended offline/authorize rules; primary bug is still open (user story). |
| TURN | Documented STUN-only defaults — accurate; users may think voice “should always work on restrictive NAT”. |
| Domain auth README | Oversimplified sequence diagram (no provision secret / multi-credential). |
| Handoff silent auth | Accurate research; not a code lie — unfinished work. |
---
## Verification commands for agents
```bash
# Phantom lesson APIs (should be empty until implemented)
rg -n 'pickRoutableRecipientId|resolveDirectConversationId|mergeAliasDirectConversations|peer-delivery-identity' toju-app
# DC recovery behavior
rg -n 'repairUnavailableDataChannel|replaceDataChannel' toju-app/src/app/infrastructure/realtime
# Inventory limit
rg -n 'INVENTORY_LIMIT' toju-app/src/app/domains/chat
# Auth paths
rg -n 'users/login|auth/login' toju-app/src/app/domains/authentication
```
---
## Policy for this emergency
1. Prefer **feature docs + code** over domain README diagrams when they conflict.
2. Prefer **code + specs** over LESSONS examples when symbols missing.
3. When fixing behavior, **fix the lying doc in the same PR**.
4. When a lesson was aspirational, relabel it explicitly so the next agent does not skip the work.
+153
View File
@@ -0,0 +1,153 @@
# 11 — Fix priority plan (systematic waves)
> Execute in order unless the user explicitly reprioritizes.
> Each wave lists **user outcome**, **work**, **depends on**, **proof**.
> Checkboxes are for agents to update when a wave is done.
---
## Guiding principles
1. **User-visible done** > unit-green alone.
2. Fix **identity + auth** before chasing random WebRTC knobs — many voice/chat bugs are “never identified / wrong id.”
3. **Interview before implement** on each wave (repo rule) unless opted out / handoff already approved.
4. Stay in `toju-app` + targeted `electron`; ask once before `server/` or full `e2e/`.
5. Correct lying docs in the same change (`10`).
---
## Wave 0 — Shared baseline (half day)
- [ ] **0.1** Read `00-README.md`, `01`, `03`, `10`.
- [ ] **0.2** Build a **cross-home repro matrix** (manual or scripted):
- Same home, same room
- Different homes, room on home A
- Different homes, room on foreign B (provision required)
- DM + call between different homes
- [ ] **0.3** Enable WebRTC/signaling debug logs; capture home id, actor ids, peer keys, signal URLs on failure.
- [ ] **0.4** Confirm whether provision secret exists after fresh login vs after restart (Electron + browser).
**Proof:** Written repro notes attached to the next handoff (short).
---
## Wave 1 — Silent multi-signal auth (P0)
**User outcome:** Never bounced to authorize login just for touching another signal server.
- [ ] **1.1** Interview + implement provision-secret ensure on restore (see `04`, user story options AD; recommend A).
- [ ] **1.2** Harden `auth_required` / `auth_error` home vs foreign handling (no false `SESSION_EXPIRED`).
- [ ] **1.3** Offline endpoints never navigate authorize (regression test).
- [ ] **1.4** Prove J4 + restart path (`02`).
**Depends on:** Wave 0.
**Pack:** `04`, story `agents-docs/user-stories/silent-cross-signal-server-auth.md`.
---
## Wave 2 — Presence identify/join integrity (P0)
**User outcome:** When you open a room, others see you; you receive live chat/voice roster.
- [ ] **2.1** Verify `getIdentifyCredentialsForSignalUrl` store fallback on every reconnect path.
- [ ] **2.2** Ensure room connect always identify → join with **actor** id for `sourceUrl`.
- [ ] **2.3** Alias canonicalization / cold-start health wait still correct.
- [ ] **2.4** Prove two-client presence + live `chat_message` after signal restart.
**Depends on:** Wave 1 (foreign rooms).
**Pack:** `05`, `08`.
---
## Wave 3 — Cross-signal outbound identity (P0)
**User outcome:** Calls ring the real person or show a clear error; DMs stay one thread.
- [ ] **3.1** Implement outbound routable recipient selection + unreachable UX (`09`, lesson L2).
- [ ] **3.2** Implement DM conversation canonicalize + merge (`09`, lesson L3).
- [ ] **3.3** Relabel/fix LESSONS so examples match shipped symbols.
- [ ] **3.4** Prove cross-home call + DM reply visibility.
**Depends on:** Wave 1. Can parallelize lightly with Wave 2 if staffing allows.
**Pack:** `09`.
---
## Wave 4 — Voice negotiation & routing (P0)
**User outcome:** Bidirectional voice in shared channels, including cross-home users; reconnect either works or errors visibly.
- [ ] **4.1** Per-signal-url local actor id for initiator election + polite peer (`06` failure 1).
- [ ] **4.2** Voice allow-list includes all peer-map aliases.
- [ ] **4.3** After reconnect budget: user-visible failure + retry action.
- [ ] **4.4** Decide TURN product stance (document vs defaults).
- [ ] **4.5** Prove same-home + cross-home voice; takeover; blip recovery.
**Depends on:** Waves 12; benefits from 3.
**Pack:** `06`.
---
## Wave 5 — Data channel recovery honesty (P0/P1)
**User outcome:** Control plane self-heals; chat history catch-up resumes; voice doesnt mysteriously die without explanation.
- [ ] **5.1** Interview: soft replace (A) vs full rebuild + UX/resync (B) (`07`).
- [ ] **5.2** Implement choice; force inventory/resync on DC reopen.
- [ ] **5.3** Align `realtime/README.md` + `voice-webrtc.md` with code (`10` L1).
- [ ] **5.4** Prove DC force-close recovery + no silent abandon.
**Depends on:** Wave 4 preferred (so voice metrics make sense).
**Pack:** `07`, `08`.
---
## Wave 6 — Messaging catch-up & attachments (P1)
**User outcome:** Late joiners get history; images/files dont stick on “Waiting…”.
- [ ] **6.1** Verify/fix inventory trigger on peer + DC repair.
- [ ] **6.2** Re-verify attachment announce/bind re-queue invariants.
- [ ] **6.3** Fix chat README inventory cap (`10` L6).
- [ ] **6.4** Prove late-join history + image sync under reorder stress.
**Depends on:** Waves 2 + 5.
**Pack:** `08`.
---
## Wave 7 — Doc & contract cleanup (P2, continuous)
- [ ] **7.1** Fix auth domain README paths (`10` L5).
- [ ] **7.2** Deprecate or fix `signaling-contracts.ts` warning banner.
- [ ] **7.3** Sync domain READMEs that oversimplify multi-credential auth.
- [ ] **7.4** Clear or refresh `agents-docs/HANDOFF.md` after each finished wave.
---
## Suggested staffing (multiple agents)
| Agent | Wave |
|-------|------|
| Auth agent | 1 |
| Realtime/signaling agent | 2, 5 |
| Identity/DM/call agent | 3 |
| Voice agent | 4 |
| Chat/attachments agent | 6 |
| Docs agent | 7 (or same PR as each fix) |
Do **not** start Wave 4 “TURN tuning” before Waves 13 — it wastes time on NAT while identity is wrong.
---
## Definition of “emergency over”
All of the following true on a two-signal-server manual matrix:
1. No spurious authorize/login while home session valid.
2. Presence mutual in shared rooms.
3. Bidirectional voice same-home and cross-home.
4. Live text + catch-up history.
5. Cross-home DM + call succeed or fail loudly.
6. DC/WS blip recovers or shows retry UI within ~1 minute.
7. Docs for recovery/identity match code (`10` critical lies cleared).
+129
View File
@@ -0,0 +1,129 @@
# 12 — Agent work packets (copy into new chats)
> Each packet is one new chat. Attach `@emergency-fix/00-README.md` plus the listed files.
> Say: **continue this packet; interview before implement; do not expand scope.**
---
## Packet A — Silent foreign auth
**Attach**
- `@emergency-fix/00-README.md`
- `@emergency-fix/04-auth-login-bugs.md`
- `@emergency-fix/11-fix-priority-plan.md` (Wave 1)
- `@agents-docs/user-stories/silent-cross-signal-server-auth.md`
**Goal:** Wave 1 — never show authorize login for normal foreign joins when home session is valid.
**Out of scope:** Voice negotiation, DM canonicalize, server password hashing changes.
**First message expectation:** Short interview (options AD from user story); wait.
---
## Packet B — Presence / identify-join
**Attach**
- `@emergency-fix/00-README.md`
- `@emergency-fix/05-signaling-multi-server.md`
- `@emergency-fix/08-messaging-visibility.md` (membership section)
- `@emergency-fix/11-fix-priority-plan.md` (Wave 2)
**Goal:** Mutual presence + live chat broadcast after connect/reconnect; foreign actor id on join.
**Depends:** Packet A ideally done (or mock credentials present).
**Out of scope:** Soft DC replace, TURN.
---
## Packet C — Outbound call + DM identity
**Attach**
- `@emergency-fix/00-README.md`
- `@emergency-fix/09-identity-cross-signal.md`
- `@emergency-fix/10-code-lies-doc-debt.md` (L2, L3)
- `@emergency-fix/11-fix-priority-plan.md` (Wave 3)
**Goal:** Routable outbound `targetUserId`; unreachable UX; single DM thread across aliases; fix phantom LESSON references when shipping.
**Out of scope:** Auth provision secret (Packet A); full voice ICE redesign.
**Note:** LESSONS claim these fixes already exist — **they do not**. Implement, dont search forever.
---
## Packet D — Voice initiator & routing
**Attach**
- `@emergency-fix/00-README.md`
- `@emergency-fix/06-voice-webrtc-bugs.md`
- `@emergency-fix/03-architecture-map.md`
- `@emergency-fix/11-fix-priority-plan.md` (Wave 4)
**Goal:** Cross-home bidirectional voice; per-signal actor initiator election; reconnect UX; TURN product decision.
**Depends:** A + B strongly; C helpful.
**Out of scope:** Rewriting entire PeerConnectionManager; server relay policy changes unless proven necessary (ask).
---
## Packet E — Data channel recovery
**Attach**
- `@emergency-fix/00-README.md`
- `@emergency-fix/07-data-channel-drops.md`
- `@emergency-fix/10-code-lies-doc-debt.md` (L1)
- `@emergency-fix/11-fix-priority-plan.md` (Wave 5)
**Goal:** Honest recovery (soft replace **or** documented full rebuild + UX + forced resync); docs match code.
**Interview required:** Option A vs B in `07`.
---
## Packet F — Messaging catch-up & attachments
**Attach**
- `@emergency-fix/00-README.md`
- `@emergency-fix/08-messaging-visibility.md`
- `@emergency-fix/11-fix-priority-plan.md` (Wave 6)
- Optional: `agents-docs/features/messaging.md`, `agents-docs/features/attachments.md`
**Goal:** Late-join history; attachment waiting stuck fixed under reorder; README inventory cap corrected.
**Depends:** B + E.
---
## Packet G — Doc debt sweep
**Attach**
- `@emergency-fix/10-code-lies-doc-debt.md`
- `@emergency-fix/11-fix-priority-plan.md` (Wave 7)
**Goal:** Clear remaining L5L7 and any lies left after AF; no behavior changes unless a one-line comment/doc only.
---
## Handoff one-liner templates
After finishing a packet, overwrite `agents-docs/HANDOFF.md` and ask the user to start a new chat with:
```text
Continue from @agents-docs/HANDOFF.md and @emergency-fix/11-fix-priority-plan.md — next unchecked wave only.
```
Or jump packets:
```text
Start @emergency-fix/12-agent-work-packets.md Packet C — interview before implement.
```
@@ -0,0 +1,485 @@
# 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 199216
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 288306
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 154168
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 539552
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 3571
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 198208 and 232234
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 106126
Initiator election performs the same lexical comparison:
- [`signaling-message-handler.ts`](../toju-app/src/app/infrastructure/realtime/signaling/signaling-message-handler.ts), lines 528535
**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 282321
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 208235
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 1721
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 511541
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 220235
`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 759773
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 166213
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 215232
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 6494
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 4771
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 339
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 2070
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 92184
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 1536
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 2850
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 2754
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.
+19
View File
@@ -0,0 +1,19 @@
# E2E summary
- Run: 2026-08-12T10:58:08Z → 2026-08-12T11:11:34Z UTC
- **65 passed / 0 failed / 0 flaky / 0 timeouts**
- Wall: ~13.4 min
## Errors
None.
## Soft signals (passed but slow)
- 128.2s — `voice/mixed-signal-config-voice.spec.ts` — 8 users with different signal configs can voice, mute, deafen, and chat concurrently
- 125.1s — `voice/multi-signal-eight-user-voice.spec.ts` — keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds
- 53.4s — `auth/user-session-data-isolation.spec.ts` — gives a new user a blank slate and restores only that user local data after account switches
- 33.7s — `chat/chat-message-features.spec.ts` — shows per-server channel lists on first saved-server click
- 30.3s — `chat/multi-client-chat-sync.spec.ts` — syncs messages between same-user devices and late-joining users after offline gaps
See `README.md` for full duration table.
+152
View File
@@ -0,0 +1,152 @@
# E2E Playwright run — emergency-fix baseline
- **Started (UTC):** 2026-08-12T10:58:08Z
- **Finished (UTC):** 2026-08-12T11:11:34Z
- **Wall duration:** 804.6s (~13.4 min)
- **Command:** `npm run test:e2e -- --reporter=list --reporter=json`
- **Result:** 65 passed, 0 failed, 0 flaky, 0 skipped
## Verdict
**No hard errors.** The suite completed green: 65/65 expected, 0 unexpected, 0 timeouts, 0 flakes.
There is nothing to triage as a failing assertion in this run. Slow cases below are the only signals worth watching if “timeouts shouldnt happen / app should be fast.”
## Errors / failures
_None in this run._
Stale note: `test-results/.last-run.json` and `test-results/html-report/` still show an old July failure; this run overrode reporters with list+json and did not refresh those artifacts.
## Slow tests (≥30s) — soft concern
| Duration | File | Test |
| --- | --- | --- |
| 128.2s | `voice/mixed-signal-config-voice.spec.ts` | 8 users with different signal configs can voice, mute, deafen, and chat concurrently |
| 125.1s | `voice/multi-signal-eight-user-voice.spec.ts` | keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds |
| 53.4s | `auth/user-session-data-isolation.spec.ts` | gives a new user a blank slate and restores only that user local data after account switches |
| 33.7s | `chat/chat-message-features.spec.ts` | shows per-server channel lists on first saved-server click |
| 30.3s | `chat/multi-client-chat-sync.spec.ts` | syncs messages between same-user devices and late-joining users after offline gaps |
These passed, but they are the closest thing to “timeout risk” in a green suite. Multi-user voice specs dominate wall time.
## Per-file totals
| File | Tests | Total | Max |
| --- | ---: | ---: | ---: |
| `voice/mixed-signal-config-voice.spec.ts` | 1 | 128.2s | 128.2s |
| `voice/multi-signal-eight-user-voice.spec.ts` | 1 | 125.1s | 125.1s |
| `chat/chat-message-features.spec.ts` | 9 | 84.2s | 33.7s |
| `auth/user-session-data-isolation.spec.ts` | 2 | 73.2s | 53.4s |
| `screen-share/screen-share.spec.ts` | 3 | 49.8s | 23.4s |
| `voice/direct-call.spec.ts` | 3 | 31.8s | 17.5s |
| `chat/multi-client-chat-sync.spec.ts` | 1 | 30.3s | 30.3s |
| `plugins/plugin-api-two-users.spec.ts` | 1 | 27.4s | 27.4s |
| `chat/notifications.spec.ts` | 2 | 25.6s | 20.9s |
| `voice/data-channel-recovery.spec.ts` | 2 | 22.1s | 12.1s |
| `chat/profile-avatar-sync.spec.ts` | 2 | 21.5s | 11.9s |
| `settings/connectivity-warning.spec.ts` | 1 | 21.3s | 21.3s |
| `voice/voice-full-journey.spec.ts` | 1 | 21.1s | 21.1s |
| `chat/server-icon-sync.spec.ts` | 1 | 14.2s | 14.2s |
| `chat/dm-flow.spec.ts` | 3 | 11.8s | 4.0s |
| `settings/stun-turn-fallback.spec.ts` | 1 | 11.5s | 11.5s |
| `auth/login-return-url.spec.ts` | 3 | 9.4s | 3.5s |
| `voice/dm-header-call-ring.spec.ts` | 2 | 9.1s | 4.8s |
| `auth/multi-device-session.spec.ts` | 1 | 8.6s | 8.6s |
| `chat/multi-device-attachment-sharing.spec.ts` | 2 | 8.6s | 4.3s |
| `voice/voice-mute-state-reset.spec.ts` | 1 | 6.9s | 6.9s |
| `servers/server-discovery-default.spec.ts` | 2 | 6.3s | 3.1s |
| `settings/ice-server-settings.spec.ts` | 2 | 6.2s | 3.9s |
| `chat/large-generic-file-transfer.spec.ts` | 1 | 5.7s | 5.7s |
| `plugins/plugin-manager-ui.spec.ts` | 1 | 4.6s | 4.6s |
| `chat/custom-emoji-user-binding.spec.ts` | 1 | 3.9s | 3.9s |
| `auth/multi-signal-server-auth.spec.ts` | 1 | 3.8s | 3.8s |
| `auth/offline-signal-server-no-login-loop.spec.ts` | 1 | 3.7s | 3.7s |
| `chat/local-attachment-persistence.spec.ts` | 1 | 2.8s | 2.8s |
| `chat/attachment-only-message-grouping.spec.ts` | 1 | 2.5s | 2.5s |
| `mobile/mobile-login-on-startup.spec.ts` | 2 | 2.4s | 1.2s |
| `chat/multi-image-gallery.spec.ts` | 1 | 2.3s | 2.3s |
| `plugins/plugin-support-api.spec.ts` | 1 | 1.9s | 1.9s |
| `mobile/mobile-settings-logout.spec.ts` | 1 | 1.7s | 1.7s |
| `mobile/android-app-icon.spec.ts` | 6 | 0.0s | 0.0s |
## All tests by duration
| Duration | Status | File | Test |
| --- | --- | --- | --- |
| 128.2s | passed | `voice/mixed-signal-config-voice.spec.ts` | 8 users with different signal configs can voice, mute, deafen, and chat concurrently |
| 125.1s | passed | `voice/multi-signal-eight-user-voice.spec.ts` | keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds |
| 53.4s | passed | `auth/user-session-data-isolation.spec.ts` | gives a new user a blank slate and restores only that user local data after account switches |
| 33.7s | passed | `chat/chat-message-features.spec.ts` | shows per-server channel lists on first saved-server click |
| 30.3s | passed | `chat/multi-client-chat-sync.spec.ts` | syncs messages between same-user devices and late-joining users after offline gaps |
| 27.4s | passed | `plugins/plugin-api-two-users.spec.ts` | runs chat, embed, soundboard, and profile APIs between two users |
| 23.4s | passed | `screen-share/screen-share.spec.ts` | screen share connection stays stable for 10+ seconds |
| 21.3s | passed | `settings/connectivity-warning.spec.ts` | shows warning icon when a peer loses all connections |
| 21.1s | passed | `voice/voice-full-journey.spec.ts` | two users register, create server, join voice, and stay connected 10+ seconds with audio |
| 20.9s | passed | `chat/notifications.spec.ts` | keeps unread badges visible when a muted channel suppresses desktop popups |
| 19.7s | passed | `auth/user-session-data-isolation.spec.ts` | preserves a user saved rooms and local history across app restarts |
| 19.6s | passed | `chat/chat-message-features.spec.ts` | syncs messages in a newly created text channel |
| 17.5s | passed | `voice/direct-call.spec.ts` | two users can ring, answer, chat, see self voice indicators, and exchange audio |
| 15.4s | passed | `screen-share/screen-share.spec.ts` | single user screen share: video and audio flow to receiver, voice audio continues |
| 14.2s | passed | `chat/server-icon-sync.spec.ts` | loads the chat-server image for online, late-joining, restarted, and discovery users |
| 12.1s | passed | `voice/data-channel-recovery.spec.ts` | heals a three-user voice mesh when one client loses every data channel |
| 11.9s | passed | `chat/profile-avatar-sync.spec.ts` | syncs display name and description changes for online and late-joining users and persists after restart |
| 11.5s | passed | `settings/stun-turn-fallback.spec.ts` | users with different ICE configs can voice chat together |
| 11.1s | passed | `screen-share/screen-share.spec.ts` | multiple users screen share simultaneously |
| 10.0s | passed | `voice/data-channel-recovery.spec.ts` | keeps two users hearing each other after a data-channel error and close |
| 9.6s | passed | `chat/profile-avatar-sync.spec.ts` | syncs avatar changes for online and late-joining users and persists after restart |
| 8.9s | passed | `voice/direct-call.spec.ts` | keeps private-call audio flowing after the data channel closes |
| 8.6s | passed | `auth/multi-device-session.spec.ts` | covers identity, chat sync, typing exclusion, and voice exclusivity |
| 6.9s | passed | `voice/voice-mute-state-reset.spec.ts` | clears stale mute state after abrupt disconnect and voice rejoin |
| 5.7s | passed | `chat/large-generic-file-transfer.spec.ts` | browser receiver can request and download a generic file above the auto-save cap |
| 5.5s | passed | `chat/chat-message-features.spec.ts` | edits and removes messages for both users |
| 5.3s | passed | `voice/direct-call.spec.ts` | missing and ended private calls do not leave stale call controls behind |
| 4.8s | passed | `voice/dm-header-call-ring.spec.ts` | callee homed on another signal server is notified when called via their provisioned actor id |
| 4.8s | passed | `chat/notifications.spec.ts` | shows desktop notifications and unread badges for inactive channels |
| 4.6s | passed | `chat/chat-message-features.spec.ts` | sends KLIPY GIF messages with mocked API responses |
| 4.6s | passed | `plugins/plugin-manager-ui.spec.ts` | installs, grants, activates, and logs an all-API test plugin |
| 4.4s | passed | `chat/chat-message-features.spec.ts` | syncs image and file attachments between users |
| 4.4s | passed | `chat/chat-message-features.spec.ts` | syncs multi-chunk image attachments byte-identical between users |
| 4.3s | passed | `chat/multi-device-attachment-sharing.spec.ts` | only the uploading device claims "Shared from your device"; the second same-user device can request it |
| 4.3s | passed | `chat/chat-message-features.spec.ts` | renders link embeds for shared links |
| 4.3s | passed | `chat/multi-device-attachment-sharing.spec.ts` | relays file-announce metadata to a sibling device that is already online during upload |
| 4.2s | passed | `voice/dm-header-call-ring.spec.ts` | callee is notified when the caller starts the call from the DM chat header |
| 4.1s | passed | `chat/chat-message-features.spec.ts` | shows typing indicators to other users |
| 4.0s | passed | `chat/dm-flow.spec.ts` | delivers a live DM to the recipient conversation |
| 3.9s | passed | `chat/custom-emoji-user-binding.spec.ts` | a second user on the same client does not inherit the first user library |
| 3.9s | passed | `chat/dm-flow.spec.ts` | opens a DM from a user card and queues messages while offline |
| 3.9s | passed | `settings/ice-server-settings.spec.ts` | allows adding, removing, and reordering ICE servers |
| 3.8s | passed | `chat/dm-flow.spec.ts` | shows friend and message actions on the search people list |
| 3.8s | passed | `auth/multi-signal-server-auth.spec.ts` | auto-provisions a foreign signal server when a new endpoint is added |
| 3.7s | passed | `auth/offline-signal-server-no-login-loop.spec.ts` | does not redirect to authorize login after a foreign server goes offline |
| 3.6s | passed | `chat/chat-message-features.spec.ts` | shows local room history on first saved-server click |
| 3.5s | passed | `auth/login-return-url.spec.ts` | unwraps nested login returnUrl chains after successful login |
| 3.1s | passed | `servers/server-discovery-default.spec.ts` | a fresh account sees public servers in Popular Servers without searching |
| 3.1s | passed | `servers/server-discovery-default.spec.ts` | discovery falls back to the public listing when featured/trending routes 404 |
| 3.1s | passed | `auth/login-return-url.spec.ts` | redirects unauthenticated /servers visits to login and returns there after login |
| 2.8s | passed | `auth/login-return-url.spec.ts` | lets a returning user log back in after an expired session redirect |
| 2.8s | passed | `chat/local-attachment-persistence.spec.ts` | remembers sent image and file across a page reload with no peer connected |
| 2.5s | passed | `chat/attachment-only-message-grouping.spec.ts` | each caption-less attachment keeps its own message bubble and preview |
| 2.3s | passed | `settings/ice-server-settings.spec.ts` | validates TURN entries require credentials |
| 2.3s | passed | `chat/multi-image-gallery.spec.ts` | groups three images in one message bubble with a visible grid |
| 1.9s | passed | `plugins/plugin-support-api.spec.ts` | covers plugin requirement, event, data, and websocket APIs with the fixture plugin |
| 1.7s | passed | `mobile/mobile-settings-logout.spec.ts` | exposes logout in the settings menu on mobile viewports |
| 1.2s | passed | `mobile/mobile-login-on-startup.spec.ts` | greets a signed-out mobile visitor on /dashboard with the login screen |
| 1.2s | passed | `mobile/mobile-login-on-startup.spec.ts` | greets a signed-out mobile visitor on the app root with the login screen |
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | renders the brand mark (white cat on a purple disc) in the launcher bitmap |
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | renders the splash art as the brand mark centred on a purple field |
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | insets the adaptive foreground so launcher masks do not clip the cat face |
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | replaces every stock Capacitor placeholder with the brand asset |
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | ships a launcher icon and splash for every required density |
| 0.0s | passed | `mobile/android-app-icon.spec.ts` | uses the brand purple as the adaptive-icon background |
## Files in this folder
| Path | Purpose |
| --- | --- |
| `README.md` | This summary |
| `00-summary.md` | Short status for handoff/skimming |
| `slow-tests.md` | Slow-test detail only |
| `parsed-report.json` | Machine-readable extract |
| `full-run.log` | Raw Playwright JSON report (+ npm banner) |
| `run-started.txt` / `run-finished.txt` | UTC timestamps |
File diff suppressed because one or more lines are too long
@@ -0,0 +1,516 @@
{
"stats": {
"startTime": "2026-08-12T10:58:09.830Z",
"duration": 804598.4550000001,
"expected": 65,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"failures": [],
"timeouts": [],
"slow": [
{
"title": "8 users with different signal configs can voice, mute, deafen, and chat concurrently",
"file": "voice/mixed-signal-config-voice.spec.ts",
"status": "passed",
"duration": 128215,
"error": null,
"errors": [],
"timeoutHint": false
},
{
"title": "keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds",
"file": "voice/multi-signal-eight-user-voice.spec.ts",
"status": "passed",
"duration": 125136,
"error": null,
"errors": [],
"timeoutHint": false
},
{
"title": "gives a new user a blank slate and restores only that user local data after account switches",
"file": "auth/user-session-data-isolation.spec.ts",
"status": "passed",
"duration": 53448,
"error": null,
"errors": [],
"timeoutHint": false
},
{
"title": "shows per-server channel lists on first saved-server click",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 33665,
"error": null,
"errors": [],
"timeoutHint": false
},
{
"title": "syncs messages between same-user devices and late-joining users after offline gaps",
"file": "chat/multi-client-chat-sync.spec.ts",
"status": "passed",
"duration": 30290,
"error": null,
"errors": [],
"timeoutHint": false
}
],
"all": [
{
"title": "unwraps nested login returnUrl chains after successful login",
"file": "auth/login-return-url.spec.ts",
"status": "passed",
"duration": 3506,
"timeoutHint": false
},
{
"title": "redirects unauthenticated /servers visits to login and returns there after login",
"file": "auth/login-return-url.spec.ts",
"status": "passed",
"duration": 3088,
"timeoutHint": false
},
{
"title": "lets a returning user log back in after an expired session redirect",
"file": "auth/login-return-url.spec.ts",
"status": "passed",
"duration": 2811,
"timeoutHint": false
},
{
"title": "covers identity, chat sync, typing exclusion, and voice exclusivity",
"file": "auth/multi-device-session.spec.ts",
"status": "passed",
"duration": 8640,
"timeoutHint": false
},
{
"title": "auto-provisions a foreign signal server when a new endpoint is added",
"file": "auth/multi-signal-server-auth.spec.ts",
"status": "passed",
"duration": 3836,
"timeoutHint": false
},
{
"title": "does not redirect to authorize login after a foreign server goes offline",
"file": "auth/offline-signal-server-no-login-loop.spec.ts",
"status": "passed",
"duration": 3727,
"timeoutHint": false
},
{
"title": "preserves a user saved rooms and local history across app restarts",
"file": "auth/user-session-data-isolation.spec.ts",
"status": "passed",
"duration": 19725,
"timeoutHint": false
},
{
"title": "gives a new user a blank slate and restores only that user local data after account switches",
"file": "auth/user-session-data-isolation.spec.ts",
"status": "passed",
"duration": 53448,
"timeoutHint": false
},
{
"title": "each caption-less attachment keeps its own message bubble and preview",
"file": "chat/attachment-only-message-grouping.spec.ts",
"status": "passed",
"duration": 2507,
"timeoutHint": false
},
{
"title": "shows per-server channel lists on first saved-server click",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 33665,
"timeoutHint": false
},
{
"title": "shows local room history on first saved-server click",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 3598,
"timeoutHint": false
},
{
"title": "syncs messages in a newly created text channel",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 19613,
"timeoutHint": false
},
{
"title": "shows typing indicators to other users",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 4090,
"timeoutHint": false
},
{
"title": "edits and removes messages for both users",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 5534,
"timeoutHint": false
},
{
"title": "syncs image and file attachments between users",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 4442,
"timeoutHint": false
},
{
"title": "syncs multi-chunk image attachments byte-identical between users",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 4369,
"timeoutHint": false
},
{
"title": "renders link embeds for shared links",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 4303,
"timeoutHint": false
},
{
"title": "sends KLIPY GIF messages with mocked API responses",
"file": "chat/chat-message-features.spec.ts",
"status": "passed",
"duration": 4610,
"timeoutHint": false
},
{
"title": "a second user on the same client does not inherit the first user library",
"file": "chat/custom-emoji-user-binding.spec.ts",
"status": "passed",
"duration": 3929,
"timeoutHint": false
},
{
"title": "opens a DM from a user card and queues messages while offline",
"file": "chat/dm-flow.spec.ts",
"status": "passed",
"duration": 3882,
"timeoutHint": false
},
{
"title": "delivers a live DM to the recipient conversation",
"file": "chat/dm-flow.spec.ts",
"status": "passed",
"duration": 4024,
"timeoutHint": false
},
{
"title": "shows friend and message actions on the search people list",
"file": "chat/dm-flow.spec.ts",
"status": "passed",
"duration": 3849,
"timeoutHint": false
},
{
"title": "browser receiver can request and download a generic file above the auto-save cap",
"file": "chat/large-generic-file-transfer.spec.ts",
"status": "passed",
"duration": 5690,
"timeoutHint": false
},
{
"title": "remembers sent image and file across a page reload with no peer connected",
"file": "chat/local-attachment-persistence.spec.ts",
"status": "passed",
"duration": 2793,
"timeoutHint": false
},
{
"title": "syncs messages between same-user devices and late-joining users after offline gaps",
"file": "chat/multi-client-chat-sync.spec.ts",
"status": "passed",
"duration": 30290,
"timeoutHint": false
},
{
"title": "only the uploading device claims \"Shared from your device\"; the second same-user device can request it",
"file": "chat/multi-device-attachment-sharing.spec.ts",
"status": "passed",
"duration": 4333,
"timeoutHint": false
},
{
"title": "relays file-announce metadata to a sibling device that is already online during upload",
"file": "chat/multi-device-attachment-sharing.spec.ts",
"status": "passed",
"duration": 4266,
"timeoutHint": false
},
{
"title": "groups three images in one message bubble with a visible grid",
"file": "chat/multi-image-gallery.spec.ts",
"status": "passed",
"duration": 2294,
"timeoutHint": false
},
{
"title": "shows desktop notifications and unread badges for inactive channels",
"file": "chat/notifications.spec.ts",
"status": "passed",
"duration": 4758,
"timeoutHint": false
},
{
"title": "keeps unread badges visible when a muted channel suppresses desktop popups",
"file": "chat/notifications.spec.ts",
"status": "passed",
"duration": 20875,
"timeoutHint": false
},
{
"title": "syncs avatar changes for online and late-joining users and persists after restart",
"file": "chat/profile-avatar-sync.spec.ts",
"status": "passed",
"duration": 9605,
"timeoutHint": false
},
{
"title": "syncs display name and description changes for online and late-joining users and persists after restart",
"file": "chat/profile-avatar-sync.spec.ts",
"status": "passed",
"duration": 11864,
"timeoutHint": false
},
{
"title": "loads the chat-server image for online, late-joining, restarted, and discovery users",
"file": "chat/server-icon-sync.spec.ts",
"status": "passed",
"duration": 14204,
"timeoutHint": false
},
{
"title": "ships a launcher icon and splash for every required density",
"file": "mobile/android-app-icon.spec.ts",
"status": "passed",
"duration": 1,
"timeoutHint": false
},
{
"title": "replaces every stock Capacitor placeholder with the brand asset",
"file": "mobile/android-app-icon.spec.ts",
"status": "passed",
"duration": 2,
"timeoutHint": false
},
{
"title": "uses the brand purple as the adaptive-icon background",
"file": "mobile/android-app-icon.spec.ts",
"status": "passed",
"duration": 1,
"timeoutHint": false
},
{
"title": "renders the brand mark (white cat on a purple disc) in the launcher bitmap",
"file": "mobile/android-app-icon.spec.ts",
"status": "passed",
"duration": 13,
"timeoutHint": false
},
{
"title": "renders the splash art as the brand mark centred on a purple field",
"file": "mobile/android-app-icon.spec.ts",
"status": "passed",
"duration": 7,
"timeoutHint": false
},
{
"title": "insets the adaptive foreground so launcher masks do not clip the cat face",
"file": "mobile/android-app-icon.spec.ts",
"status": "passed",
"duration": 3,
"timeoutHint": false
},
{
"title": "greets a signed-out mobile visitor on /dashboard with the login screen",
"file": "mobile/mobile-login-on-startup.spec.ts",
"status": "passed",
"duration": 1217,
"timeoutHint": false
},
{
"title": "greets a signed-out mobile visitor on the app root with the login screen",
"file": "mobile/mobile-login-on-startup.spec.ts",
"status": "passed",
"duration": 1181,
"timeoutHint": false
},
{
"title": "exposes logout in the settings menu on mobile viewports",
"file": "mobile/mobile-settings-logout.spec.ts",
"status": "passed",
"duration": 1704,
"timeoutHint": false
},
{
"title": "runs chat, embed, soundboard, and profile APIs between two users",
"file": "plugins/plugin-api-two-users.spec.ts",
"status": "passed",
"duration": 27382,
"timeoutHint": false
},
{
"title": "installs, grants, activates, and logs an all-API test plugin",
"file": "plugins/plugin-manager-ui.spec.ts",
"status": "passed",
"duration": 4564,
"timeoutHint": false
},
{
"title": "covers plugin requirement, event, data, and websocket APIs with the fixture plugin",
"file": "plugins/plugin-support-api.spec.ts",
"status": "passed",
"duration": 1913,
"timeoutHint": false
},
{
"title": "single user screen share: video and audio flow to receiver, voice audio continues",
"file": "screen-share/screen-share.spec.ts",
"status": "passed",
"duration": 15375,
"timeoutHint": false
},
{
"title": "multiple users screen share simultaneously",
"file": "screen-share/screen-share.spec.ts",
"status": "passed",
"duration": 11093,
"timeoutHint": false
},
{
"title": "screen share connection stays stable for 10+ seconds",
"file": "screen-share/screen-share.spec.ts",
"status": "passed",
"duration": 23371,
"timeoutHint": false
},
{
"title": "a fresh account sees public servers in Popular Servers without searching",
"file": "servers/server-discovery-default.spec.ts",
"status": "passed",
"duration": 3149,
"timeoutHint": false
},
{
"title": "discovery falls back to the public listing when featured/trending routes 404",
"file": "servers/server-discovery-default.spec.ts",
"status": "passed",
"duration": 3110,
"timeoutHint": false
},
{
"title": "shows warning icon when a peer loses all connections",
"file": "settings/connectivity-warning.spec.ts",
"status": "passed",
"duration": 21313,
"timeoutHint": false
},
{
"title": "allows adding, removing, and reordering ICE servers",
"file": "settings/ice-server-settings.spec.ts",
"status": "passed",
"duration": 3855,
"timeoutHint": false
},
{
"title": "validates TURN entries require credentials",
"file": "settings/ice-server-settings.spec.ts",
"status": "passed",
"duration": 2311,
"timeoutHint": false
},
{
"title": "users with different ICE configs can voice chat together",
"file": "settings/stun-turn-fallback.spec.ts",
"status": "passed",
"duration": 11549,
"timeoutHint": false
},
{
"title": "keeps two users hearing each other after a data-channel error and close",
"file": "voice/data-channel-recovery.spec.ts",
"status": "passed",
"duration": 10035,
"timeoutHint": false
},
{
"title": "heals a three-user voice mesh when one client loses every data channel",
"file": "voice/data-channel-recovery.spec.ts",
"status": "passed",
"duration": 12070,
"timeoutHint": false
},
{
"title": "two users can ring, answer, chat, see self voice indicators, and exchange audio",
"file": "voice/direct-call.spec.ts",
"status": "passed",
"duration": 17480,
"timeoutHint": false
},
{
"title": "keeps private-call audio flowing after the data channel closes",
"file": "voice/direct-call.spec.ts",
"status": "passed",
"duration": 8950,
"timeoutHint": false
},
{
"title": "missing and ended private calls do not leave stale call controls behind",
"file": "voice/direct-call.spec.ts",
"status": "passed",
"duration": 5345,
"timeoutHint": false
},
{
"title": "callee is notified when the caller starts the call from the DM chat header",
"file": "voice/dm-header-call-ring.spec.ts",
"status": "passed",
"duration": 4235,
"timeoutHint": false
},
{
"title": "callee homed on another signal server is notified when called via their provisioned actor id",
"file": "voice/dm-header-call-ring.spec.ts",
"status": "passed",
"duration": 4815,
"timeoutHint": false
},
{
"title": "8 users with different signal configs can voice, mute, deafen, and chat concurrently",
"file": "voice/mixed-signal-config-voice.spec.ts",
"status": "passed",
"duration": 128215,
"timeoutHint": false
},
{
"title": "keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds",
"file": "voice/multi-signal-eight-user-voice.spec.ts",
"status": "passed",
"duration": 125136,
"timeoutHint": false
},
{
"title": "two users register, create server, join voice, and stay connected 10+ seconds with audio",
"file": "voice/voice-full-journey.spec.ts",
"status": "passed",
"duration": 21107,
"timeoutHint": false
},
{
"title": "clears stale mute state after abrupt disconnect and voice rejoin",
"file": "voice/voice-mute-state-reset.spec.ts",
"status": "passed",
"duration": 6888,
"timeoutHint": false
}
]
}
@@ -0,0 +1 @@
2026-08-12T11:11:34Z
@@ -0,0 +1 @@
2026-08-12T10:58:08Z
+17
View File
@@ -0,0 +1,17 @@
# Slow E2E tests (≥30s)
All of these **passed**. Listed because the product ask is that timeouts should not happen and the app should stay fast.
| Duration | File | Test |
| --- | --- | --- |
| 128.2s | `voice/mixed-signal-config-voice.spec.ts` | 8 users with different signal configs can voice, mute, deafen, and chat concurrently |
| 125.1s | `voice/multi-signal-eight-user-voice.spec.ts` | keeps 8 users on 2 signal apis while voice, mute, and deafen stay consistent for 20+ seconds |
| 53.4s | `auth/user-session-data-isolation.spec.ts` | gives a new user a blank slate and restores only that user local data after account switches |
| 33.7s | `chat/chat-message-features.spec.ts` | shows per-server channel lists on first saved-server click |
| 30.3s | `chat/multi-client-chat-sync.spec.ts` | syncs messages between same-user devices and late-joining users after offline gaps |
## Notes
- `mixed-signal-config-voice` and `multi-signal-eight-user-voice` are intentionally heavy (8 users / multi-signal). ~2 minutes each is suite design cost more than a single-user app hang.
- `user-session-data-isolation` (~53s) and channel/chat sync (~3034s) are closer to “app feels slow” candidates if they regress upward.
- Playwright config `timeout` is 90s per test; none of the failures/timeouts hit that ceiling in this run.