Preserve private audio originals and confirm written answers - #2861
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b719cb8c9f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await WriteSourceRowsAsync(writer, "objects", _sourceStorage.ObjectsAsync(userId, cancellationToken), cancellationToken); | ||
| await WriteSourceRowsAsync(writer, "references", _sourceStorage.ReferencesAsync(userId, cancellationToken), cancellationToken); | ||
| await WriteSourceRowsAsync(writer, "chunks", _sourceStorage.ChunksAsync(userId, cancellationToken), cancellationToken); |
There was a problem hiding this comment.
Export source arrays from one database snapshot
If an audio upload commits after objects finishes but before references or chunks, these independent queries emit references and bytes for a BlobId that is absent from the exported object list; concurrent deletion can produce the inverse. The buffered path performs the same sequential reads, so either account-export format can produce an archival package that cannot be reconstructed or hash-verified. Read the entire source graph from one database snapshot or pin a fixed set of owned IDs.
Useful? React with 👍 / 👎.
| public Task<Result<ThinkingAudioDto>> ConfirmAsync(Guid userId, Guid id, ThinkingAudioConfirmDto dto, CancellationToken ct) => TransactionAsync(async () => | ||
| { | ||
| var answer = await OwnedAsync(userId, id, ct); | ||
| if (answer.ConfirmedMemoryId.HasValue) return await MapAsync(answer, ct); |
There was a problem hiding this comment.
Reject mismatched confirmation replays
When two tabs hold an unconfirmed receipt and one confirms first, every later confirmation gets a successful response here before its expected revisions, representation, deck revision, or status are checked. A stale tab can therefore request a different choice and report it as confirmed even though the committed memory is unchanged—and the returned DTO does not expose the committed status. Treat only an exact replay as idempotent and return 409 for a mismatched request.
Useful? React with 👍 / 👎.
| var saved = await writtenAnswers.AnswerAsync(userId, answer.BoardId.Value, answer.CardId, answer.LayerId, | ||
| new(dto.ExpectedDeckRevision, previousText.Text, dto.Status), ct); | ||
| if (!saved.IsSuccess) throw new DomainException(saved.ErrorCode, saved.ErrorMessage); |
There was a problem hiding this comment.
Reject confirmation when a separate text answer exists
When the user already has a normal text answer with the same text and status, ThinkingAnswerService.AnswerAsync returns that existing memory as a success, so this path marks the audio receipt confirmed and creates a Verified representation linked to a memory produced by the separate text-answer flow. A direct API caller can thus bypass the UI's answerAlreadyKept guard and contradict the documented rule that an existing answer must be corrected in Memory. Detect the pre-existing answer and return a conflict instead of adopting it.
Useful? React with 👍 / 👎.
| if (!live) return | ||
| if (playbackUrl.value) URL.revokeObjectURL(playbackUrl.value) | ||
| playbackUrl.value = URL.createObjectURL(blob) |
There was a problem hiding this comment.
Discard stale audio downloads when the answer changes
If an original-audio download is in flight while the shared question is edited and saved, the revision-triggered reload can replace or clear saved before the old download completes. Because completion is guarded only by component liveness, the old blob is then installed as playbackUrl; after a replacement answer is uploaded, its player and download link serve the previous question's recording. Capture and verify the answer identity or generation before accepting the blob, and clear the URL on every saved-answer identity transition.
Useful? React with 👍 / 👎.
|
Bounded independent Luna review of 1b836c9..b719cb8: no direct HIGH/CRITICAL correctness, privacy or data-loss finding. Storage and full audio integration have each received one scoped pass; no additional review loop is requested. Owner checks, transaction rollback, stable upload retries, representation lineage, export and erasure were traced; supplied local proving evidence is documented above. MEDIUM follow-through: the local-draft sentence should distinguish the in-component "Reload saved recording" action from a browser hard reload, which cannot preserve an unuploaded File. This wording will be narrowed in the next original-library slice under #2808. The existing quota-before-hashing duplicate-at-capacity policy edge remains tracked, not a privacy or loss defect. Physical microphone/device, external STT and restore import remain unverified. |
Private audio originals and written confirmation
Saving a recording from a Thinking Deck question now retains the original privately without pretending the audio was transcribed or the question answered. The owner can replay/download it, save separate written versions, and explicitly confirm one into private memory. Retrying an uncertain upload compares its stable upload ID and original bytes; stale question/recording revisions reject confirmation without losing drafts or original evidence.
This continues #2808 and is stacked on #2857. Existing Classic, Studio, Companion and Unified experiences share the same flow. No shared card change, automatic processing job, remote transcription or telemetry is introduced.
Verification
Actual microphone hardware, physical mobile browsers, external transcription/provider quality and restore import are unverified. Account JSON is archival. Raw untranscribed user input has narrowly documented caption-lint exceptions; the UI retains written alternatives and never fabricates timed captions. No working database was migrated. Human decisions in OUTSTANDING_TASKS.md remain unchanged. The wider #2808 work remains open.