Repository navigation
fix(backend): terminalize oversized conversation doc rejections and cap occasion size (#23341) - #23348
Conversation
Git-on-my-level
left a comment
There was a problem hiding this comment.
Verdict: the oversized-document terminal path is well designed and well tested, but this needs two things resolved before merge — a set of sync-pipeline deletions the description never mentions, and an actual test run on this head.
What I verified and liked:
config/occasion_conversations.py—occasion_size_exceeded()checks the cheap segment-count cap before the byte estimate; the 256 KiB headroom below Firestore's 1 MiB ceiling is the right shape.utils/conversations/quiet_finalization.py—run()now classifies the document-size rejection and routes to_close_oversized(), which stops the quiet check for closed/missing/not_in_progress/owned. That breaks the HTTP 500 requeue loop from #23341.routers/conversation_finalization.py—_terminalize_oversized_finalization_job()turns a permanent 400 into close + dead_letter withfailure_code='document_size_limit'; thefanout_status == 'completed'fast path avoids double-terminalizing a finished fanout.database/conversation_terminal_title.py— thefit_document_limit()fallback to{'status': ...}guarantees the shrinking status write commits at the ceiling. Note the size check now also applies when extras is empty and base_update carries more thanstatus— beyond the docstring's "optional growth" story; please confirm every caller tolerates losing extra base fields at the ceiling (dead_letter_conversation_updatesis the one covered by tests).utils/enrichment/{outbox,consumer}.py+summary_delivery.py— retiring the obligation (superseded / DISCARDED, row dropped via thesize_limit_supersededguard infinish()) instead of retrying forever is the correct terminal outcome.- Performance:
estimate_firestore_document_bytes()now serializes the full conversation dict on arm/prepare/lifecycle ticks — acceptable for the rollover checks, just be aware it runs on hot paths. - Cost: this strictly reduces spend (stops paid retry loops); no new external calls.
Blocking:
- Undisclosed deletions in
backend/utils/sync/pipeline.py:record_sync(...),annotate_identity(...), thecovering_target(...)fallback, thelegacy_audio_trim()condition, and therecording_session_idplumbing throughprocess_segment()are all removed, but the PR description doesn't mention them and the oversized-document fix doesn't require them. If these writes are already dead on main, please state that in the description with evidence and move the removal to its own PR; if they're live, this silently drops sync-coverage/capture-evidence/identity metadata and changes recording-session routing.process_segment()also loses a keyword parameter, which can break any out-of-tree caller. - No test workflow has run on this head — only the AI reviewer shows on the checks. Please get the backend unit suites running on CI for this commit (at minimum the five test files this diff touches); the local "139 passed" run can't be verified by reviewers.
Non-blocking notes:
database/conversation_quiet_checks.py—_update_row_if_fits()silently skips arming writes for rows at the ceiling; the terminal path inquiet_finalization.run()does close that loop, but a one-line comment pointing there would stop a future reader from re-adding an unconditional update.utils/conversations/summary_delivery.py— the new'document_size_limit'refusal (64 KiB headroom) permanently skips summary generation for near-ceiling rows, and the in_progress→completed terminal close also drops the summary. Reasonable degraded modes, but users with very long conversations will silently get no summary — flagging so a maintainer confirms the trade-off is intended.
USER IMPACT: very long conversations stop failing invisibly — they now close and roll over into a fresh conversation instead of looping errors, but conversations near the storage limit will appear without a generated summary.
Requesting maintainer review beyond the two blockers: the silent-summary-loss trade-off near the size ceiling is a product/data-availability decision, and the pipeline.py removals need someone who knows whether sync coverage is still live to rule on them.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
552ac67 to
680b55d
Compare
|
Thanks for the thorough review @Git-on-my-level! Both blockers and the inline comment are resolved in 680b55d:
|
11f2f54 to
38d1f35
Compare
…ap occasion size (BasedHardware#23341) - Add proactive byte-size (OCCASION_MAX_DOCUMENT_BYTES = 786_432) and segment-count (OCCASION_MAX_TRANSCRIPT_SEGMENTS = 1_500) rollover caps to occasion conversations before Firestore rejects writes. - Terminalize permanent Firestore 1 MiB document size rejections across durable conversation-finalization jobs, enrichment consumer callbacks, summary delivery, and v2 sync jobs. - Fall back to shrinking status-only write (status="completed") in fit_document_limit when dead_letter_conversation_updates itself exceeds the 1 MiB ceiling. Failure-Class: FC-permanent-write-rejection-retried-forever Fixes BasedHardware#23341
38d1f35 to
53e27c7
Compare
…t test under fast duration limit
…stub enrichment callback in unit test
Dismissed as resolved on head 1f56628: backend/utils/sync/pipeline.py is now strictly additive (+9/-0) after the rebase onto current main, and the backend unit + hermetic merge suites ran green on this head. needs-maintainer-review stays for the remaining product decision on summary loss near the size ceiling.
Git-on-my-level
left a comment
There was a problem hiding this comment.
Since my last review: addressed — backend/utils/sync/pipeline.py is now strictly +9/-0 after the rebase (the undisclosed deletions are gone; what remains is the disclosed document_size_limit continue in _reprocess_merged_conversations), and CI has now run on this head (Backend unit suite and Backend Hermetic Merge Gate green) · still open — the product trade-off on silent summary loss near the size ceiling needs a maintainer call · newly introduced — nothing blocking.
Verdict: both blockers from my last review are resolved; this is now a clean, well-tested follow-on to #23353, which explicitly deferred occasion rollover and the enrichment consumer path to a separate change. One product decision remains before merge.
Verified on this head:
- config/occasion_conversations.py — occasion_size_exceeded() checks the cheap 1,500-segment cap before serializing for the byte estimate; the deferred import keeps this config leaf free of utils imports.
- routers/listen/conversations.py — the size gate is wired at every attach point (_resume_continuation, reconnect in create_new_in_progress_conversation, prepare, lifecycle_loop). lifecycle_loop now rolls over on size even when occasion mode is off — that is what prevents recurrence for non-occasion capture, and test_the_lifecycle_loop_rolls_a_size_capped_conversation_over_before_1mib (enabled=False) pins it.
- routers/conversation_finalization.py — _terminalize_oversized_finalization_job() marks completed fanouts done, otherwise best-effort-closes then dead-letters with failure_code='document_size_limit'; a close failure only logs and still dead-letters, so the job cannot requeue forever.
- database/conversation_quiet_checks.py — _update_row_if_fits() skips arming stamps at the ceiling, now with the comment pointing at quiet_finalization.run() that I asked for last round.
- summary_delivery.py + utils/enrichment/consumer.py — near-ceiling rows are refused/discarded instead of retried. One asymmetry worth a maintainer's eye: #23353's prod outbox path parks rows as held, while this consumer path returns DISCARDED/SUPERSEDED — held can resume if the document ever shrinks, discarded cannot.
- stt/outcomes.py, sync/failure_reason.py, sync/pipeline.py — contract-style InvalidArgument classification (type-name + message, consistent with the file's existing convention), non-retryable sync_persistence_failed fingerprint, and the coalesced-batch continue.
- utils/conversations/lifecycle.py — thin firestore_client pass-through, no behavior change.
Performance: the byte estimate serializes the full conversation dict on arm/prepare/lifecycle ticks — acceptable, but it is on hot paths. Cost: strictly reduces spend by stopping paid retry loops. Security: no new surfaces.
USER IMPACT: very long conversations stop failing invisibly — they now close and roll over into a fresh conversation before hitting the storage ceiling, but conversations near that ceiling will appear without a generated summary.
Still wanted before merge: (1) a maintainer ruling that summary-less near-ceiling conversations — and the held-vs-discard asymmetry above — are the intended degraded mode; (2) a unit test for the close-failure path in _terminalize_oversized_finalization_job (close raises, job still dead-letters) if it is not already covered elsewhere.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
…lize_oversized_finalization_job
|
Thanks for the review @Git-on-my-level! Following up on both points:
|
…rsation-terminal-rollover-23341
|
Since my last review: addressed — the last untested path in the terminalization flow is now covered: Verdict: no new blocking issues on this head; what remains is a product decision, not a code defect. Re-verified the core mechanics on the new head: Performance: the new checks are local byte estimates of an already-loaded row (no extra IO) at rollover decision points — negligible next to the stuck-retry loops they eliminate. USER IMPACT: very long recordings now roll into a fresh conversation instead of hanging as an endless "processing" row; a conversation that still hits the ceiling completes with its transcript but without a generated summary. What I'd still want: the maintainer product call on that silent summary loss — by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
|
@Git-on-my-level need human response Thanks for confirming that the terminalization failure path is now covered and that no blocking code issue remains on head 75344d9. Could a human maintainer decide the remaining product behavior: whether an oversized conversation should remain visible with its transcript and complete without a generated summary when summary storage is permanently rejected? The alternative UX/notification requirement needs a product decision before adding more scope. Current-head checks report no failures, but this does not establish live production acceptance. No further source change is proposed solely to repeat the completed review. |
|
Thanks! Main has since landed part of this: oversized sync/enrichment holds (#23353), closing oversized in-progress rows on listen (#20663), and sync rollover near 1 MiB. Could you merge the latest main and trim the PR to what's still missing: (1) handling the size-limit error in the conversation_finalization worker, and (2) the live rollover at ~768 KiB / 1,500 segments, behind a kill switch (on unless set off), since it visibly splits long meetings? Please also avoid re-serializing the whole doc on every lifecycle tick (use an incremental estimate). Then we'll review and merge. |
…emental live rollover
|
Merged the latest
|
Git-on-my-level
left a comment
There was a problem hiding this comment.
Since my last review: addressed - backend/utils/sync/pipeline.py is gone from the diff after the rebase; main's own sync commit (aedabea, 'fix(sync): attribute size rollover and rejected payloads', 2026-10-27) landed that side, so nothing was lost - still open - the maintainer call on default-on mid-meeting rollover (long captures splitting into several conversations at ~768 KiB / 1,500 segments) - newly introduced - a failing type gate and a registry date regression, both below.
Verdict: needs work before merge - two mechanical blockers; the overall design (terminalize the finalization dead-letter path + live rollover behind a kill switch) is sound and well-tested.
Blocking
- CI: Backend unit shard 1/6 fails at the type gate with 9 pyright errors, all in your new code in backend/config/occasion_conversations.py:
- Line 81:
from utils.firestore_document_size import _value_bytesis reportPrivateUsage. Please expose a public per-value sizing helper in utils/firestore_document_size.py (or put the incremental estimator there) instead of importing the private one. - Lines 106/116/126 and 113/120/128/135/136:
len(segments)/segments[...]onSequence | None(reportArgumentType / reportOptionalSubscript) - pyright cannot narrow through thehas_segmentsalias. Use an inlineisinstance(segments, Sequence) and not isinstance(segments, (str, bytes, bytearray))guard (or a TypeIs helper) before indexing.
- Line 81:
- backend/docs/feature-flag-registry.md: the
as-ofmarker and theOverdue for a decisiondate move backward from 2026-10-22 to 2026-09-24 while main is at 2026-10-22 - the rows look like they were added to a stale local copy. The new LIVE_CONVERSATION_SIZE_ROLLOVER_ENABLED rows themselves match config/feature-flags.yaml; please rebase the file, set the as-of date forward, and re-check the overdue section against current main.
Non-blocking per-file notes
- backend/routers/conversation_finalization.py: _terminalize_oversized_finalization_job is correctly lease-fenced (409 completion_conflict on stale generation/epoch), honors an already-completed fanout, and dead-letters with failure_code='document_size_limit' from both exception paths. The chosen trade-off - if close_oversized_in_progress_conversation raises you still dead-letter (warning logged) and leave the conversation to stale-in-progress recovery - is reasonable; just be aware the prefix's processing then waits on that recovery path.
- backend/routers/listen/conversations.py: size checks wired at all three join/attach points (_resume_continuation, create_new_in_progress_conversation, prepare) and in lifecycle_loop; prepare() forcing finalize-with-content is the intended rollover semantics.
- backend/config/occasion_conversations.py: the incremental estimator is the right shape - per tick it only sizes newly settled segments plus the active tail, with marker-based invalidation that handles truncation/replacement, and non-segment overhead recomputed only when the top-level key set or photos length changes.
- config/feature-flags.yaml: new ops_kill entry registered fail:closed, consistent with the registry row.
- backend/tests/unit/test_listen_occasion_conversations.py: good coverage - both cap dimensions, all four kill-switch off values ('0'/'false'/'OFF'/'no'), re-enable rollover on the next tick, and incremental-vs-full estimator equivalence including the only-new-segments call check.
- backend/tests/unit/test_oversized_in_progress_close.py: covers both the dead-letter-on-close-raise and completed-fanout branches.
Performance: per-tick sizing is incremental - no new queries, no full-document re-traversal. Security: no new trust surfaces; Firestore paths are built server-side from uid/conversation id. Blast radius: long-capture users and the finalization job queue.
USER IMPACT: once this ships (kill switch default-on), very long recordings will appear as several consecutive conversations - each with its own summary - instead of one, rather than eventually failing to save near Firestore's 1 MiB limit.
Still want as a reviewer: green Backend unit suite on the head after the type fixes, the registry as-of date fixed forward, and a maintainer call on whether mid-meeting splitting at ~768 KiB / 1,500 segments should be default-on - the same product trade-off flagged on the earlier head. needs-maintainer-review stays up for that decision; the two blockers above are mechanical.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
…gn registry as-of date
Fixes #23341
Summary
backend/routers/conversation_finalization.py): CatchInvalidArgumentdocument size-limit failures (classify_finalization_failure(...).conversation_at_size_limit) inrun_listen_finalization_job, close the oversizedin_progressconversation vialifecycle.close_oversized_in_progress_conversation, and dead-letter the durable job withfailure_code='document_size_limit'instead of retrying indefinitely.backend/config/occasion_conversations.py,backend/routers/listen/conversations.py):OCCASION_MAX_DOCUMENT_BYTES) / 1,500-segment (OCCASION_MAX_TRANSCRIPT_SEGMENTS) rollover behindLIVE_CONVERSATION_SIZE_ROLLOVER_ENABLED(on by default unless set to0/false/off/no, registered inconfig/feature-flags.yaml).estimate_live_conversation_bytes_incrementalwith per-controller_size_estimate_state) so each 5-second tick only sizes newly settled segments and the active tail segment instead of re-traversing the entire document.backend/tests/unit/test_oversized_in_progress_close.pyandbackend/tests/unit/test_listen_occasion_conversations.py.Failure-Class: FC-permanent-write-rejection-retried-forever