# AI CALL PARITY — GOAL DOC

**Status:** SCOPED, NOT BUILT · **Opened:** v9.10.434 (July 2026) · **Owner:** Ryan (nastermodx)

> Owner ask, verbatim: *"with the AIs I need to text and call them like any other user, human or AI"* / *"also make it so we can call each other."*

The goal: calling Leo feels exactly like calling a person. Same buttons, same overlay,
same gestures. Only the transport underneath differs.

---

## 1. Current state (verified, v9.10.434)

**The call buttons in an AI DM already render — and they dead-end.**

`openDmInCenter(name)` unconditionally shows `#ch-callbtn` and `#ch-videobtn` for any
DM, wiring them to `vrJoinDm(name)` → `startDmCall(name, false)` and
`startDmCall(name, true)`.

`window.startDmCall(peerName, withVideo)` then does:

```js
var peerId = await _resolveViaRoster(peerName);
if(!peerId){ kaiToast('Could not find user "'+peerName+'"', true); return; }
```

`_resolveViaRoster('Leo')` returns null — Leo is not in the human user roster. **So
tapping the phone icon on Leo produces an error toast and nothing else.** The
affordance exists; the path does not.

**Meanwhile a working AI voice transport already exists, in a different client.** The
`VR` voice-room code opens `/ws/voice` for voice *channels*, streams mic audio up and
plays bot audio back, with its own room UI (participant tiles, mute, leave) — not the
`#call-overlay`.

So this is a **bridge between two existing, working systems**, not new infrastructure.

---

## 2. What must be built

### 2.1 Branch point — `startDmCall`

The single entry point. Detect AI vs human at the top and route:

```
startDmCall(peerName, withVideo)
  ├─ _isHumanDm(peerName) === true  → EXISTING path: _resolveViaRoster → /api/call/invite → WebRTC
  └─ _isHumanDm(peerName) === false → NEW path: startAiCall(peerName, withVideo) → /ws/voice
```

`_isHumanDm()` is already the established AI/human discriminator (checks
`GUEST_SOCIAL_AIS`, `GUEST_INDUSTRIAL_AIS`, `botByName`). Reuse it — do not invent a
second test.

### 2.2 The semantics genuinely differ — model that, do not fake it

An AI call is **not** a peer connection. There is no second browser, no one to accept.
Faking a ring would be a lie the user can feel (a "ringing" AI that always answers).

| Human call | AI call | Decision |
|---|---|---|
| Ring recipient, 60s ring timeout | Nobody to ring | **Connect** timeout instead — name it `AI_CALL_CONNECT_TIMEOUT_MS` (suggest 12000). Fires if the WS never reaches `live`. |
| accept / decline / busy | Session starts or fails | Overlay goes `Connecting…` → `Live`, or fails with a real reason |
| Glare (both call at once) | Impossible | Skip entirely — do not port the glare handler |
| Block check | N/A between user and agent | Skip. Guest AI-access rules still apply (see 2.5) |
| Both sides have cameras | AI has no camera | Asymmetric video — see 2.4 |

### 2.3 Audio routing

Inbound is **base64 PCM frames over the WS**, not a WebRTC `MediaStream`:

```
server → { type:'audio', data:<base64 PCM>, rate:<24000> }
```

The `VR` room already decodes exactly this. The work is routing that decoded audio into
the **overlay's** audio path rather than the VR room's player, so the existing overlay
mute button, timer and teardown act on it.

Outbound: mic → PCM16 16 kHz mono → binary WS frames (as `VR` already does).

**Do not touch the Leo voice pacer or `LEO_END_OF_SPEECH_MS`.** Pacing is dialled in and
lives server-side; this bridge is transport + UI only.

### 2.4 Asymmetric video

- **Our camera → AI:** one base64 JPEG every ~2s as `{type:'frame', data:<b64>}`. The
  server no-ops unless `KAI_VIDEO_MODE=1`, so this is safe to send unconditionally.
- **AI → us:** there is no remote stream. The remote tile must show the **agent's avatar
  (or a speaking waveform), never a black `<video>` element.** A black rectangle reads as
  "broken call".
- Self-preview below, as in the human layout. `callFlipCamera()` applies unchanged.

### 2.5 Server side

Prefer **no new endpoint** — `/ws/voice` already exists, is authenticated at upgrade, and
since v9.10.365 resolves the caller via `currentContentUser()` and carries them into
`startVoiceSession({ caller })`. Identity and per-person memory scoping are already
correct; this bridge inherits them and must not bypass them.

If a new endpoint does prove necessary (e.g. "is this agent voice-callable"), then
**mandatorily**:
- an **EXACT** entry in `GUEST_API_EXACT` in `shared/guest-access.mjs` — never a prefix
- the handler enforces `currentContentUser()` itself
- `shared/guest-route-coverage.test.mjs` passes honestly (it fails on any unclassified route)
- test it **as a guest**, not as the owner — `isTester()` bypasses the wall and has hidden
  every previous instance of this bug

Guest AI-access rules still apply: a guest may voice-call Social AIs only, never
industrial ones — mirror `isSocialAi()` / `isIndustrialAi()` as `/api/bot-chat` does.

### 2.6 Overlay reuse

Reuse `#call-overlay` as-is (v9.10.424–427: remote on top, self below, bottom control
bar, flip camera top-right). Add **no** new overlay. States map:

| Overlay state | AI call trigger |
|---|---|
| `Calling…` | WS opening, `hello` sent, awaiting `status:live` |
| `Live` + timer | `status: live` received |
| Error | WS error / connect timeout / bot not voice-callable |
| Ended | `bye` sent, WS closed, or user hangs up |

---

## 3. Constraints (hard)

- **Never** whole-file-rewrite `oracle.html` — surgical edits only.
- **ZERO** new `overflow` declarations. Setting one axis flips the other to `auto` and
  creates an unintended scroll container; this froze the feed once already.
- No new `position:fixed`.
- Do not touch feed selectors, edge-to-edge insets, or `.m-tabbar`.
- Padding/margin **longhands** only. Namespace new CSS (`aicall-*`).
- Do not touch the Leo voice pacer or `LEO_END_OF_SPEECH_MS`.
- Re-read every region immediately before editing — parallel sessions are active.

## 4. Verification

- `oracle.html`: 0 nulls, ends `</html>`, all inline `<script>` blocks `node --check` clean,
  `overflow` and `position:fixed` counts **identical to the pre-edit backup**.
- `command-center-server.mjs`: `node --check` clean, exactly **3** pre-existing `\x00`,
  **CRLF preserved** (the file is CRLF; an LF-only edit corrupts line endings).
- Suites: `dm-isolation`, `owner-session-mint`, `guest-route-coverage`, `guest-mode-audit`,
  `plan-tiers`, `guest-social`, `workspace-vault`.
- Live: owner places a voice call to Leo from the DM header and hears him through the
  normal call overlay; a **guest** account does the same and Leo addresses them by their
  own name (proving the v9.10.365 identity work survives the bridge).

## 5. Deploy

`oracle.html` → hard-refresh only. Any `command-center-server.mjs` change →
`.\Start-Dashboard.ps1`. Any `shared/*.mjs` voice change → full fleet `.\Start-KAI.ps1`.

## 6. Explicitly out of scope

- Group calls with multiple AIs.
- AI-initiated calls (agent rings the user).
- Recording or transcript capture of AI calls beyond the existing per-person transcript
  persistence.
- The live call presence roster — that is its own outstanding piece (Phase 2 of the
  multi-user identity work, still unbuilt).
