db: voice_avatar_sessions

Merged
CH
6d ago
+4593-0

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_sessions in packages/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 by pnpm db:generate and hardened to be idempotent. It uses the IF NOT EXISTS form and the repo's DO $$ … duplicate_object FK idiom.
  • AVATAR_END_REASON in voice-vocabulary.ts. It holds the six pinned reasons: user, media_lost, max_duration, no_credits, unavailable and error.
  • 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:

  • openAvatarSessionsOf takes {userId, voiceSessionId}, so the read is scoped to the account.
  • closeAvatarSession returns an Option.

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


Open workspace in Conductor