Skip to content

Let the Persistent Mind call the user when nothing on screen can reach them - #5375

Merged
atomantic merged 3 commits into
mainfrom
next/issue-5309
Aug 29, 2026
Merged

Let the Persistent Mind call the user when nothing on screen can reach them#5375
atomantic merged 3 commits into
mainfrom
next/issue-5309

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

  • New default-off grant voice.call-user lets the Persistent Mind place a FaceTime Audio call to the single handle already configured in Settings > Voice. The model's callRequest carries a reason and an opening line but no recipient, so a confused turn cannot dial anyone else. Capabilities schema 3 → 4 (every prior wire version still accepted on input); an install upgrading with the mind already running gains nothing until the user opts in.
  • The gate runs after inference (server/services/persistentMindCallCapability.js) and refuses on: grant off, FaceTime feature off, voice disabled, no saved identity, a browser tab that could speak instead, voice quiet hours, a call already up, or the budget — at most 3 calls per rolling 24 h, at least 30 min apart.
  • Caps live in durable mind state (callHistory, schema 4 → 5, migration 313) because an in-memory cap would hand back a fresh allowance on every restart. The budget is charged only to a call that actually went out.
  • Every decision is on the trajectory as mind.call.requested + mind.call.placed / mind.call.suppressed; the dialed handle never is. A suppressed request is appended to the turn's reply so the mind cannot claim it called when the phone never rang.
  • Continuity: a placed call speaks its opening line the moment the far end picks up, runs each turn with the mind's persona plus a bounded briefing of its trajectory (new optional systemContext on runTurn), and hands the outcome back as a Persistent Mind message on hangup — including when nobody answered.
  • Critical-notification escalation (Settings > Voice, off by default) shares the same gate and the same budget but is authorized by facetime.escalateCritical, not the mind's grant: a critical notification still unread after escalateAfterMinutes (default 10) with no voice tab available asks to ring the user.
  • UI: the grant appears in Mind > Tools from the catalog; the escalation toggle, delay, and the shared caps are documented in Settings > Voice. Docs in docs/features/cos-enhancement.md and docs/features/voice.md.

Part of #5306.

Closes #5309

Test plan

  • server/services/persistentMindCallCapability.test.js (new, 14 cases): each gate in isolation, the placed path, the fourth call in 24 h as rate-capped, the 30-minute gap as too-soon, a failed dial not spending the budget, escalation calling without the mind grant but under the same caps, no handle on the trajectory, and every returned reason being a declared one.
  • server/lib/persistentMind.test.js: rate verdict across the rolling window, ageing out, a future timestamp failing closed, the ledger staying bounded and handle-free.
  • server/services/voice/proactiveTriggers.test.js: escalation fires only after the delay, only while unread, only for critical, only while enabled (re-read at fire time), one timer per notification, cleared on unwire.
  • server/sockets/voice.test.js: the opening line is spoken into a connected call exactly once across repeated state broadcasts, and not at all for a user-placed call.
  • server/services/voice/callSession.test.js: consume-once opening line, briefing scoped to a mind call, transcript handed back on hangup (and on an unanswered call), clean end when the handoff fails.
  • server/services/persistentMindAdapter.test.js: the request runs once on the terminal answer (not an intermediate tool round), and a suppression is surfaced in the reply.
  • scripts/migrations/313-persistent-mind-call-history.test.js: seeds an empty ledger, never resets an existing one, idempotent, tolerant of missing/invalid state.
  • Full server (35 527) and client (10 260) suites pass, plus client lint and build. API/socket catalogs regenerated.

…n reach them

The mind could only speak through the Mind tab or a browser voice tab, so a
user who had walked away was unreachable — and its capability boundary
explicitly forbade external messaging. This adds `voice.call-user` as a
separate, default-off grant that places a FaceTime Audio call to the single
handle already configured in Settings > Voice. The model's `callRequest`
carries a reason and an opening line but no recipient, so a confused turn
cannot dial anyone else.

The gate (server/services/persistentMindCallCapability.js) runs after
inference and refuses the call whenever the grant is off, the FaceTime feature
or voice is disabled, no identity is saved, a browser tab could speak the
message instead, the user's local time is inside voice quiet hours, a call is
already up, or the budget is spent — at most 3 calls per rolling 24 hours, at
least 30 minutes apart. Those counters live in durable Persistent Mind state
(schema 4 -> 5, migration 313) because an in-memory cap would hand back a
fresh allowance on every restart, and the budget is charged only to a call
that actually went out. Every decision, placed or suppressed, lands on the
trajectory as mind.call.*; the dialed handle never does. A suppressed request
is appended to the turn's reply, so the mind cannot claim it called when the
phone never rang.

A placed call speaks its opening line the moment the far end picks up, runs
with the mind's persona and a bounded briefing of its trajectory, and hands
the outcome back as a Persistent Mind message on hangup — including when
nobody answered, which is exactly when the mind would otherwise redial to say
the same thing.

Critical-notification escalation (Settings > Voice, off by default) shares the
gate and the budget but is authorized by facetime.escalateCritical rather than
the mind's grant: a critical notification still unread after
escalateAfterMinutes with no voice tab available asks to ring the user.

Capabilities schema 3 -> 4, accepting every prior wire version on input so an
older client bundle can still toggle the grants it knows about. An install
upgrading with the mind already running gains nothing until the user opts in.

Part of #5306.
# Conflicts:
#	docs/features/voice.md
#	server/lib/socketEventCatalog.generated.json
#	server/sockets/voice.js
#	server/sockets/voice.test.js
@atomantic
atomantic merged commit de4c67a into main Aug 29, 2026
7 checks passed
@atomantic
atomantic deleted the next/issue-5309 branch August 29, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[repo-study-facetime-bridge-mind-call-capability] FaceTime Audio phase 3: Persistent Mind voice.call-user capability + critical-notification escalation

1 participant