|
46 | 46 | should_attach_to_existing_in_progress, |
47 | 47 | ) |
48 | 48 | from utils.transcribe_store import calendar_db, conversations_db, redis_db |
49 | | -from config.occasion_conversations import OCCASION_MAX_SPAN_SECONDS |
| 49 | +from config.occasion_conversations import OCCASION_MAX_SPAN_SECONDS, occasion_size_exceeded |
50 | 50 | from utils.conversations.lifecycle_clocks import speech_clock |
51 | 51 | from utils.episodes import boundaries, boundary_runtime |
52 | 52 | from routers.listen.occasion import occasion_attach_target |
@@ -161,12 +161,16 @@ async def _resume_continuation(self, pointer: dict[str, str]) -> bool: |
161 | 161 | ) |
162 | 162 | if existing: |
163 | 163 | existing = await self._continuity_row(existing) |
164 | | - if not existing or not resumable_continuation( |
165 | | - existing, |
166 | | - source=normalize_listen_source(self.host.request.source), |
167 | | - device_id=self.host.client_device_context.client_device_id, |
168 | | - now=self.clock(), |
169 | | - timeout=self.host.conversation_creation_timeout, |
| 164 | + if ( |
| 165 | + not existing |
| 166 | + or self._conversation_size_exceeded(existing) |
| 167 | + or not resumable_continuation( |
| 168 | + existing, |
| 169 | + source=normalize_listen_source(self.host.request.source), |
| 170 | + device_id=self.host.client_device_context.client_device_id, |
| 171 | + now=self.clock(), |
| 172 | + timeout=self.host.conversation_creation_timeout, |
| 173 | + ) |
170 | 174 | ): |
171 | 175 | return False |
172 | 176 | self.host.recording_session_id = pointer['recording_session_id'] |
@@ -533,14 +537,18 @@ async def create_new_in_progress_conversation(self, *, rollover: bool = False) - |
533 | 537 | in_progress_status=ConversationStatus.in_progress, |
534 | 538 | ) |
535 | 539 | if action == RecordingSessionReconnectAction.resume_current and not existing.get('deleted'): |
536 | | - if not self.host.is_multi_channel and not resumable_continuation( |
537 | | - existing, |
538 | | - source=normalize_listen_source(request.source), |
539 | | - device_id=self.host.client_device_context.client_device_id, |
540 | | - now=self.clock(), |
541 | | - timeout=self.host.conversation_creation_timeout, |
| 540 | + size_exceeded = self._conversation_size_exceeded(existing) |
| 541 | + if not self.host.is_multi_channel and ( |
| 542 | + size_exceeded |
| 543 | + or not resumable_continuation( |
| 544 | + existing, |
| 545 | + source=normalize_listen_source(request.source), |
| 546 | + device_id=self.host.client_device_context.client_device_id, |
| 547 | + now=self.clock(), |
| 548 | + timeout=self.host.conversation_creation_timeout, |
| 549 | + ) |
542 | 550 | ): |
543 | | - if ( |
| 551 | + if size_exceeded or ( |
544 | 552 | existing.get('source') == normalize_listen_source(request.source) |
545 | 553 | and existing.get('client_device_id') == self.host.client_device_context.client_device_id |
546 | 554 | and not any( |
@@ -760,16 +768,20 @@ async def prepare(self) -> Optional[str]: |
760 | 768 | existing = await self._continuity_row(existing) |
761 | 769 | finished_at = speech_clock(self.host.request.uid, existing) or self.clock() |
762 | 770 | seconds = (self.clock() - finished_at).total_seconds() |
| 771 | + size_exceeded = self._conversation_size_exceeded(existing) |
763 | 772 | if ( |
764 | | - decide_existing_conversation_action( |
| 773 | + size_exceeded |
| 774 | + or decide_existing_conversation_action( |
765 | 775 | seconds_since_last_segment=seconds, |
766 | 776 | conversation_creation_timeout=continuation_timeout(existing, self.host.conversation_creation_timeout), |
767 | 777 | ) |
768 | 778 | == ConversationLifecycleAction.process_and_create_new |
769 | 779 | ): |
770 | 780 | # This runs before STT initialization: an outage cannot indefinitely |
771 | 781 | # defer empty-generation cleanup. The transaction still lets content win. |
772 | | - if not (existing.get('transcript_segments') or existing.get('photos') or existing.get('has_content')): |
| 782 | + if size_exceeded or not ( |
| 783 | + existing.get('transcript_segments') or existing.get('photos') or existing.get('has_content') |
| 784 | + ): |
773 | 785 | await self._finalize_isolated(self.process_conversation, existing['id'], stage='prepare_rollover') |
774 | 786 | await self.create_new_in_progress_conversation() |
775 | 787 | return existing['id'] |
@@ -922,9 +934,17 @@ async def recover_stale_in_progress(self) -> None: |
922 | 934 | continue |
923 | 935 | await self._finalize_isolated(self.process_conversation, conversation['id'], stage='stale_in_progress') |
924 | 936 |
|
| 937 | + def _conversation_size_exceeded(self, conversation: dict[str, Any]) -> bool: |
| 938 | + cid = conversation.get('id') or getattr(getattr(self.host, 'state', None), 'current_conversation_id', None) |
| 939 | + uid = getattr(getattr(self.host, 'request', None), 'uid', None) |
| 940 | + doc_path = f'users/{uid}/conversations/{cid}' if uid and cid else None |
| 941 | + return occasion_size_exceeded(conversation, document_path=doc_path) |
| 942 | + |
925 | 943 | def _occasion_span_exceeded(self, conversation: dict[str, Any]) -> bool: |
926 | 944 | if not getattr(self.host, 'occasion_mode', False): |
927 | 945 | return False |
| 946 | + if self._conversation_size_exceeded(conversation): |
| 947 | + return True |
928 | 948 | started = persisted_started_seconds(conversation.get('started_at')) |
929 | 949 | return started is not None and self.clock().timestamp() - started >= OCCASION_MAX_SPAN_SECONDS |
930 | 950 |
|
@@ -973,7 +993,7 @@ async def lifecycle_loop(self) -> None: |
973 | 993 | if ( |
974 | 994 | boundary_action != 'hold' |
975 | 995 | and action == ConversationLifecycleAction.continue_current |
976 | | - and self._occasion_span_exceeded(conversation) |
| 996 | + and (self._occasion_span_exceeded(conversation) or self._conversation_size_exceeded(conversation)) |
977 | 997 | ): |
978 | 998 | # One occasion is capped like a smart-merged chain: all-day capture still rolls over. |
979 | 999 | logger.info('event=occasion_span_rollover conversation=%s', conversation_id) |
|
0 commit comments