db: voice_avatar_sessions
Merged
CH
6d ago+4593+4593+0123456789012345678901234567890123456789-0-0-0123456789
PR #3 of the LiveAvatar video-call rollout: the store records each LiveAvatar session a call showed, and the seconds it ran.
What
- Table
voice_avatar_sessionsinpackages/database/src/schemas/voice.ts, with the columns id, voice_session_id (FK, cascade), user_id (FK, cascade), liveavatar_session_id (unique), started_at, ended_at, seconds and end_reason. It has a partial index over the open rows of a voice session. - Migration
20261013203349_voice_avatar_sessions, generated bypnpm db:generateand hardened to be idempotent. It uses theIF NOT EXISTSform and the repo'sDO $$ ⦠duplicate_objectFK idiom. AVATAR_END_REASONinvoice-vocabulary.ts. It holds the six pinned reasons:user,media_lost,max_duration,no_credits,unavailableanderror.- Model
@sidecar/database/models/voice-avatar-sessions.
Final signatures (for #8 and #9)
Every function is an Effect<_, SqlError | Schema.SchemaError, SqlClient>.
openAvatarSession({ userId, voiceSessionId, liveAvatarSessionId, startedAt: Date }): Effect<Option<string>>
// The new row's id. None when the account does not own that voice session, or when that LiveAvatar session is already recorded.
closeAvatarSession({ liveAvatarSessionId, endedAt: Date, reason: AvatarEndReason }): Effect<Option<number>>
// Idempotent. The first close writes ended_at, end_reason and the seconds (rounded up, never below 0).
// A later close changes nothing and returns the first close's seconds. None when no row names that session.
openAvatarSessionsOf({ userId, voiceSessionId }): Effect<readonly { liveAvatarSessionId: string; startedAt: Date }[]>
// The open rows on the account's voice session, oldest first.
The orchestrator approved two departures from the pinned plan:
openAvatarSessionsOftakes{userId, voiceSessionId}, so the read is scoped to the account.closeAvatarSessionreturns anOption.
The #9 sweep closes orphans with media_lost.
Callers
None yet. #8 (web: avatar frames in the voice service) and #9 (web: the sweep stops orphaned avatars) will consume this.
Tests
packages/database/src/models/__tests__/voice-avatar-sessions.test.ts runs on getTestDB() and covers four cases:
- closing a session twice records its seconds once;
- closing a session that no row names returns none;
- one account cannot open or read avatar sessions on another account's call;
- a LiveAvatar session is recorded only once.
apps/web/tests/storage-schema.test.ts now lists the new table among the declared tables.
./scripts/check.sh exits 0.
š¤ Generated with Claude Code