Core — Recording Limits
max_duration_seconds enforcement, RecordingDurationWarningEvent and auto-stop, session lock preventing a second active recording per user, and the recording_event_history diagnostics table.
Core — Recording Limits
Owner: Core Engineer Domain: core Complexity: M Prerequisite: Milestone 05 merged
When this slice is complete, a per-start max_duration_seconds can only lower the server's RECORDING_MAX_DURATION_SECONDS ceiling, never raise it; Core warns the client at roughly 80% of the limit via duration_warning.v1 and auto-stops at the limit with stopped.v1 { reason: "duration_limit" }, continuing the recording through the normal finalization pipeline. A second recording start for the same user — a different meeting, or the same meeting from another tab — is refused session_conflict while the original session continues unaffected, and GET /meetings?has_active_recording=true lists every live session. Every lifecycle, health, and stop-reason signal is captured in a newest-first diagnostics history, exposed only to service auth.
Required Capabilities
-
StartRecordingCommand.max_duration_secondsoverrides the server ceiling downward only; the effective limit is echoed onstarted.v1. - Core emits
com.wordloop.recording.duration_warning.v1 { meeting_id, remaining_seconds, auto_stop_at }at roughly 80% of the limit. - Core auto-stops the recording at the limit with
stopped.v1 { reason: "duration_limit", last_received_sequence }and no error event, then proceeds through the normalpost_processing→completedpipeline. - A second recording start for the same user — a different meeting, or the same meeting under a different
client_recording_id— is refused withrecording.error.v1 { code: "session_conflict", severity: "error" }; the original session is unaffected. -
GET /meetings?has_active_recording=truelists every meeting with an active or stopping recording for the user; a stopped meeting leaves the list. -
GET /meetings/{id}/recording/events(service auth only — bearer and impersonated-user auth get403) returns a newest-first diagnostics history includingstarted,status_changed,health,duration_warning, andstoppedentries, each withid,created_at, and apayloadobject. -
GET /meetings/{id}/recording/eventsfor an unknown meeting answers404.
Dependencies
- Milestone 05 (
live-audio-durability-recovery) must be merged — therecordingsrow and status-transition machinery this slice extends with limits, the session lock, and diagnostics history.
Domain Notes
max_duration_seconds is a ceiling that individual starts may lower but never raise — the test compose sets RECORDING_MAX_DURATION_SECONDS=30 and starts pass 12, never the reverse. The session lock is per-user, not per-meeting: a second tab is refused even when it targets a different meeting than the one already recording.
Test Cases
Test cases map to tests/bets/meeting-recording/test_milestone_11_recording_hardening.py. Run via ./dev test bet meeting-recording.
| Test | Location | Assertion |
|---|---|---|
test_long_session_reaches_target_or_clear_limit_state | test_milestone_11 | started.v1 echoes the lowered max_duration_seconds; duration_warning.v1 arrives with 1 <= remaining_seconds <= limit, auto_stop_at == started_at + limit (±1s), at roughly 50–100% of the limit's elapsed time. |
test_background_tab_behavior_is_visible | test_milestone_11 | After a resume command with last_client_sequence, resumed.v1 is received and GET /meetings/{id}/recording shows status: "active" with last_received_sequence matching what the client reported. |
test_no_audio_input_warns_or_auto_stops | test_milestone_11 | Core relays a recording.health.v1 { code: "no_audio_detected", status: "degraded", recoverable: true } event; degraded_reasons on GET /recording includes no_audio_detected; the recording status stays active (warn, not stop); the diagnostics history records a health event with that code. |
test_recording_auto_stops_at_hard_limits | test_milestone_11 | The recording auto-stops with reason: "duration_limit" at the configured limit (±15s tolerance), the warning precedes the stop, next_status is composing_audio or awaiting_gap_upload, and the meeting is no longer listed under has_active_recording=true. |
test_multi_tab_guard_blocks_conflicting_recording_control | test_milestone_11 | A second tab starting the same meeting (or a different one) gets session_conflict with severity: "error"; the original tab keeps recording and its frames continue to be accepted; has_active_recording=true lists the meeting as active. |
test_recording_diagnostics_capture_health_and_stop_reasons | test_milestone_11 | GET /recording/events refuses bearer and impersonated-user auth with 403; the newest-first history includes started, health, duration_warning, and stopped(duration_limit); status transitions include stopping → draining_ml → composing_audio/awaiting_gap_upload → ... → post_processing; an unknown meeting answers 404. |
Completion Checklist
- Code merged and deployed
- Bet progress tests pass (
./dev test bet meeting-recording) - Permanent service tests implemented per testing strategy
- Code review completed
- API review completed (
GET /meetings/{id}/recording/eventsdiagnostics endpoint) - Testing review completed
- Schema review completed (
recording_event_historytable) - System documentation updated (as applicable)
App — Recording Hardening UI
MediaRecorder/OPFS capability gate before recording starts, multi-tab guard, no-audio warning banner, auto-stop countdown overlay, and the diagnostics panel.
ML — Recording Health Signals
no_audio_detected health signal and unusable-audio detection emitted as a RecordingHealthEvent domain signal.