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
+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) |