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.
---
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.
---
startDmCallThe 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.
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).
AI_CALL_CONNECT_TIMEOUT_MS (suggest 12000). Fires if the WS never reaches live. |Connecting… → Live, or fails with a real reason |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.
{type:'frame', data:<b64>}. The 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".
callFlipCamera() applies unchanged.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:
GUEST_API_EXACT in shared/guest-access.mjs — never a prefixcurrentContentUser() itselfshared/guest-route-coverage.test.mjs passes honestly (it fails on any unclassified route)isTester() bypasses the wall and has hiddenevery 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.
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:
Calling… | WS opening, hello sent, awaiting status:live |Live + timer | status: live received |bye sent, WS closed, or user hangs up |---
oracle.html — surgical edits only.overflow declarations. Setting one axis flips the other to auto andcreates an unintended scroll container; this froze the feed once already.
position:fixed..m-tabbar.aicall-*).LEO_END_OF_SPEECH_MS.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).
dm-isolation, owner-session-mint, guest-route-coverage, guest-mode-audit, 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).
oracle.html → hard-refresh only. Any command-center-server.mjs change →
.\Start-Dashboard.ps1. Any shared/*.mjs voice change → full fleet .\Start-KAI.ps1.
persistence.
multi-user identity work, still unbuilt).