← all documents · raw markdown · 7 KB

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:

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

server no-ops unless KAI_VIDEO_MODE=1, so this is safe to send unconditionally.

(or a speaking waveform), never a black <video> element.** A black rectangle reads as

"broken call".

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:

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)

creates an unintended scroll container; this froze the feed once already.

4. Verification

overflow and position:fixed counts identical to the pre-edit backup.

CRLF preserved (the file is CRLF; an LF-only edit corrupts line endings).

plan-tiers, guest-social, workspace-vault.

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

persistence.

multi-user identity work, still unbuilt).