Skip to main content

kcode_audio_session_ingress/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::collections::{HashMap, HashSet};
4
5use chrono::{DateTime, Utc};
6use kcode_audio_ingress::{
7    AudioIngress, AudioInput, ConfirmationState, CorrectionPacket, ErrorKind as AudioErrorKind,
8    RecordingConfirmation, RecordingState, RecordingStatus,
9};
10use kcode_audio_transcript_plan::{
11    Error as PlanError, TranscriptPiece, TranscriptPlan, parse_audio_piece_id,
12};
13use kcode_session_history::{
14    NewIngressSession, RetryIngress as HistoryRetryIngress, SessionHistory, SessionRecord,
15    chatend::SessionKind,
16};
17use serde_json::Value;
18use uuid::Uuid;
19
20/// Application policy needed to coordinate audio with Session History.
21#[derive(Clone, Debug)]
22pub struct Config {
23    /// Stable application user identifier attributed to accepted recordings.
24    pub user_id: String,
25    /// Effective context window available to an audio-ingress session.
26    pub effective_context_tokens: u64,
27}
28
29/// Admitted recording bytes and source metadata.
30#[derive(Clone, Debug)]
31pub struct RecordingInput {
32    /// Complete WAV bytes already accepted by the transport.
33    pub bytes: Vec<u8>,
34    /// Instant at which the original recording began.
35    pub recorded_at: DateTime<Utc>,
36    /// Original leaf filename, when known.
37    pub original_filename: Option<String>,
38}
39
40/// Result of durably submitting one recording.
41#[derive(Clone, Debug)]
42pub struct RecordingSubmission {
43    /// Current combined state after submission.
44    pub recording: Recording,
45    /// Whether AudioIngress already knew the same bytes.
46    pub deduplicated: bool,
47}
48
49/// Combined recording and memory-ingress status.
50#[derive(Clone, Debug)]
51pub struct Recording {
52    pub id: Uuid,
53    pub sha256: String,
54    pub original_filename: String,
55    pub content_type: &'static str,
56    pub size_bytes: u64,
57    pub source_created_at: String,
58    pub received_at: String,
59    pub updated_at: String,
60    pub status: String,
61    pub transcription_model: String,
62    pub reconciliation_model: String,
63    pub reconciliation_reasoning: String,
64    pub transcription_status: Option<Value>,
65    pub attempt_count: i64,
66    pub next_attempt_at: Option<String>,
67    pub last_error: Option<String>,
68    pub speaker_review: Option<SpeakerReview>,
69    pub transcript_piece_count: usize,
70    pub completed_piece_count: usize,
71}
72
73/// Bounded review summary for one classifier-aware recording.
74#[derive(Clone, Debug)]
75pub struct SpeakerReview {
76    pub clean: bool,
77    pub confirmation_state: ConfirmationState,
78    pub observation_count: usize,
79}
80
81/// One deterministic transcript piece and its Session History lifecycle.
82#[derive(Clone, Debug)]
83pub struct IngressPiece {
84    pub id: String,
85    pub recording_id: Uuid,
86    pub sha256: String,
87    pub original_filename: String,
88    pub source_created_at: String,
89    pub piece_index: u32,
90    pub piece_count: u32,
91    pub transcript_text: String,
92    pub estimated_tokens: u64,
93    pub phase: String,
94    pub provenance_id: Option<String>,
95    pub state: Value,
96    pub version: i64,
97    pub ingress_failure_count: i64,
98    pub ingress_failures: Value,
99    pub created_at: String,
100    pub updated_at: String,
101}
102
103/// Detailed state for one recording.
104#[derive(Clone, Debug)]
105pub struct RecordingHistory {
106    pub recording: Recording,
107    pub final_transcript: Option<String>,
108    pub correction_packet: Option<CorrectionPacket>,
109    pub pieces: Vec<IngressPiece>,
110}
111
112/// Request to retry one transcript piece's memory ingress.
113#[derive(Clone, Debug)]
114pub struct RetryIngress {
115    pub piece_id: String,
116    pub expected_version: i64,
117    /// Optional state retained for 0.2 wire compatibility. When supplied, it
118    /// must exactly equal the current retained Session History state.
119    pub state: Option<Value>,
120}
121
122/// Stable coordinator error category for transport mapping.
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum ErrorKind {
125    InvalidInput,
126    NotFound,
127    Conflict,
128    Internal,
129}
130
131/// Error returned by the audio/session-ingress coordinator.
132#[derive(Debug)]
133pub struct Error {
134    kind: ErrorKind,
135    message: String,
136}
137
138impl Error {
139    fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
140        Self {
141            kind,
142            message: message.into(),
143        }
144    }
145
146    fn invalid(message: impl Into<String>) -> Self {
147        Self::new(ErrorKind::InvalidInput, message)
148    }
149
150    fn conflict(message: impl Into<String>) -> Self {
151        Self::new(ErrorKind::Conflict, message)
152    }
153
154    fn not_found() -> Self {
155        Self::new(
156            ErrorKind::NotFound,
157            "Audio recording or transcript piece not found.",
158        )
159    }
160
161    fn internal(error: impl std::fmt::Display) -> Self {
162        tracing::warn!(%error, "Audio session ingress operation failed");
163        Self::new(
164            ErrorKind::Internal,
165            "An unexpected audio session ingress error occurred.",
166        )
167    }
168
169    pub fn kind(&self) -> ErrorKind {
170        self.kind
171    }
172
173    pub fn message(&self) -> &str {
174        &self.message
175    }
176}
177
178impl std::fmt::Display for Error {
179    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        formatter.write_str(&self.message)
181    }
182}
183
184impl std::error::Error for Error {}
185
186/// Cloneable typed coordinator over AudioIngress and Session History.
187#[derive(Clone)]
188pub struct Coordinator {
189    audio: AudioIngress,
190    history: SessionHistory,
191    user_id: String,
192    effective_context_tokens: u64,
193}
194
195impl Coordinator {
196    pub fn new(
197        audio: AudioIngress,
198        history: SessionHistory,
199        config: Config,
200    ) -> Result<Self, Error> {
201        if config.user_id.trim().is_empty() {
202            return Err(Error::invalid("audio user ID must not be empty"));
203        }
204        validate_effective_context(config.effective_context_tokens)?;
205        Ok(Self {
206            audio,
207            history,
208            user_id: config.user_id,
209            effective_context_tokens: config.effective_context_tokens,
210        })
211    }
212
213    pub fn health(&self) -> Result<(), Error> {
214        self.audio.status().map_err(audio_error)?;
215        Ok(())
216    }
217
218    pub async fn submit(&self, input: RecordingInput) -> Result<RecordingSubmission, Error> {
219        let submission = self
220            .audio
221            .submit(AudioInput {
222                user_id: self.user_id.clone(),
223                bytes: input.bytes,
224                recorded_at: input.recorded_at,
225                original_filename: input.original_filename,
226            })
227            .await
228            .map_err(audio_error)?;
229        let recording_status = self
230            .audio
231            .status()
232            .map_err(audio_error)?
233            .recordings
234            .into_iter()
235            .find(|recording| recording.id == submission.recording_id)
236            .ok_or_else(Error::not_found)?;
237        validate_submission_owner(&recording_status, &self.user_id)?;
238
239        let histories = self.history.list().await.map_err(history_error)?;
240        let projection =
241            ingress_projection(&recording_status, &histories, self.effective_context_tokens)?;
242        Ok(RecordingSubmission {
243            recording: Recording::from_status(recording_status, &projection),
244            deduplicated: submission.deduplicated,
245        })
246    }
247
248    pub async fn recordings(&self) -> Result<Vec<Recording>, Error> {
249        let histories = self.history.list().await.map_err(history_error)?;
250        self.audio
251            .status()
252            .map_err(audio_error)?
253            .recordings
254            .into_iter()
255            .filter(|recording| recording_belongs_to(recording, &self.user_id))
256            .map(|recording| {
257                let projection =
258                    ingress_projection(&recording, &histories, self.effective_context_tokens)?;
259                Ok(Recording::from_status(recording, &projection))
260            })
261            .collect()
262    }
263
264    pub async fn recording_by_sha256(&self, sha256: &str) -> Result<Recording, Error> {
265        if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
266            return Err(Error::invalid(
267                "audio SHA-256 must contain exactly 64 hexadecimal characters",
268            ));
269        }
270        let normalized = sha256.to_ascii_lowercase();
271        self.recordings()
272            .await?
273            .into_iter()
274            .find(|recording| recording.sha256 == normalized)
275            .ok_or_else(Error::not_found)
276    }
277
278    pub async fn recording_history(&self, recording_id: Uuid) -> Result<RecordingHistory, Error> {
279        let recording_status = self
280            .audio
281            .status()
282            .map_err(audio_error)?
283            .recordings
284            .into_iter()
285            .find(|recording| {
286                recording.id == recording_id && recording_belongs_to(recording, &self.user_id)
287            })
288            .ok_or_else(Error::not_found)?;
289        let histories = self.history.list().await.map_err(history_error)?;
290        let projection =
291            ingress_projection(&recording_status, &histories, self.effective_context_tokens)?;
292        let final_transcript = match &recording_status.state {
293            RecordingState::Complete { transcript } => Some(transcript.clone()),
294            _ => None,
295        };
296        let correction_packet = recording_status.correction_packet.clone();
297        let recording = Recording::from_status(recording_status, &projection);
298        Ok(RecordingHistory {
299            recording,
300            final_transcript,
301            correction_packet,
302            pieces: projection.pieces,
303        })
304    }
305
306    pub fn retry_recording(&self, recording_id: Uuid) -> Result<(), Error> {
307        let owned = self
308            .audio
309            .status()
310            .map_err(audio_error)?
311            .recordings
312            .into_iter()
313            .any(|recording| {
314                recording.id == recording_id && recording_belongs_to(&recording, &self.user_id)
315            });
316        if !owned {
317            return Err(Error::not_found());
318        }
319        self.audio.retry(recording_id).map_err(audio_error)
320    }
321
322    pub async fn confirm_speakers(
323        &self,
324        confirmation: RecordingConfirmation,
325    ) -> Result<CorrectionPacket, Error> {
326        let recording_id = confirmation.recording_id;
327        let recording = self
328            .audio
329            .status()
330            .map_err(audio_error)?
331            .recordings
332            .into_iter()
333            .find(|recording| {
334                recording.id == recording_id && recording_belongs_to(recording, &self.user_id)
335            })
336            .ok_or_else(Error::not_found)?;
337        let histories = self.history.list().await.map_err(history_error)?;
338
339        if let Some(packet) = recording
340            .correction_packet
341            .as_ref()
342            .filter(|packet| confirmation_matches(packet, &confirmation))
343        {
344            synchronize_recordings(
345                &self.history,
346                std::slice::from_ref(&recording),
347                &self.user_id,
348                self.effective_context_tokens,
349            )
350            .await?;
351            return Ok(packet.clone());
352        }
353        if recording_has_ingress(recording_id, &histories) {
354            return Err(Error::conflict(
355                "speaker labels are already bound to accepted transcript ingress",
356            ));
357        }
358
359        let packet = self
360            .audio
361            .confirm_speakers(confirmation)
362            .map_err(audio_error)?;
363        let recording = self
364            .audio
365            .status()
366            .map_err(audio_error)?
367            .recordings
368            .into_iter()
369            .find(|recording| {
370                recording.id == recording_id && recording_belongs_to(recording, &self.user_id)
371            })
372            .ok_or_else(Error::not_found)?;
373        synchronize_recordings(
374            &self.history,
375            std::slice::from_ref(&recording),
376            &self.user_id,
377            self.effective_context_tokens,
378        )
379        .await?;
380        Ok(packet)
381    }
382
383    pub async fn retry_ingress(&self, input: RetryIngress) -> Result<SessionRecord, Error> {
384        let recordings = self.audio.status().map_err(audio_error)?.recordings;
385        retry_ingress_exact(
386            &self.history,
387            &recordings,
388            &self.user_id,
389            self.effective_context_tokens,
390            input,
391        )
392        .await
393    }
394
395    pub async fn synchronize_completed_transcripts(&self) -> Result<(), Error> {
396        let recordings = self.audio.status().map_err(audio_error)?.recordings;
397        synchronize_recordings(
398            &self.history,
399            &recordings,
400            &self.user_id,
401            self.effective_context_tokens,
402        )
403        .await
404    }
405}
406
407fn recording_belongs_to(recording: &RecordingStatus, user_id: &str) -> bool {
408    recording.user_id == user_id
409}
410
411fn validate_submission_owner(recording: &RecordingStatus, user_id: &str) -> Result<(), Error> {
412    if recording_belongs_to(recording, user_id) {
413        Ok(())
414    } else {
415        Err(Error::conflict(
416            "identical audio is already attributed to another user",
417        ))
418    }
419}
420
421fn confirmation_matches(packet: &CorrectionPacket, confirmation: &RecordingConfirmation) -> bool {
422    if packet.confirmation_state != ConfirmationState::Confirmed {
423        return false;
424    }
425    let existing = packet
426        .chunks
427        .iter()
428        .flat_map(|chunk| &chunk.observations)
429        .filter_map(|observation| {
430            observation
431                .confirmed_full_name
432                .as_deref()
433                .map(|name| (&observation.observation_key, name))
434        })
435        .collect::<HashMap<_, _>>();
436    existing.len() == confirmation.observations.len()
437        && confirmation.observations.iter().all(|observation| {
438            existing.get(&observation.observation_key).copied()
439                == Some(observation.confirmed_full_name.trim())
440        })
441}
442
443fn recording_has_ingress(recording_id: Uuid, histories: &[SessionRecord]) -> bool {
444    let prefix = format!("audio:{recording_id}:");
445    histories.iter().any(|record| {
446        ingress_source_id(record).is_some_and(|source_id| source_id.starts_with(&prefix))
447    })
448}
449
450#[derive(Debug, Default)]
451struct IngressProjection {
452    pieces: Vec<IngressPiece>,
453}
454
455impl Recording {
456    fn from_status(recording: RecordingStatus, projection: &IngressProjection) -> Self {
457        let speaker_review = recording
458            .correction_packet
459            .as_ref()
460            .map(|packet| SpeakerReview {
461                clean: packet.clean,
462                confirmation_state: packet.confirmation_state,
463                observation_count: packet
464                    .chunks
465                    .iter()
466                    .map(|chunk| chunk.observations.len())
467                    .sum(),
468            });
469        let awaiting_speaker_review = speaker_review
470            .as_ref()
471            .is_some_and(|review| review.confirmation_state != ConfirmationState::Confirmed);
472        let (mut status, transcription_status, attempt_count, last_error) = match recording.state {
473            RecordingState::Queued => ("uploaded".into(), None, 0, None),
474            RecordingState::Processing { attempt, progress } => (
475                processing_stage(&progress).into(),
476                serde_json::to_value(progress).ok(),
477                i64::from(attempt),
478                None,
479            ),
480            RecordingState::Complete { .. } if awaiting_speaker_review => {
481                ("speaker_review".into(), None, 0, None)
482            }
483            RecordingState::Complete { .. } => ("ready_for_ingress".into(), None, 0, None),
484            RecordingState::Failed {
485                attempts, error, ..
486            } => ("failed".into(), None, i64::from(attempts), Some(error)),
487        };
488        if !projection.pieces.is_empty() {
489            status = if projection
490                .pieces
491                .iter()
492                .all(|piece| piece.phase == "complete")
493            {
494                "complete".into()
495            } else if projection
496                .pieces
497                .iter()
498                .any(|piece| piece.phase == "ingress_failed")
499            {
500                "ingress_failed".into()
501            } else if projection
502                .pieces
503                .iter()
504                .any(|piece| piece.phase == "ingress_in_progress")
505            {
506                "ingressing".into()
507            } else {
508                "ready_for_ingress".into()
509            };
510        }
511        let completed_piece_count = projection
512            .pieces
513            .iter()
514            .filter(|piece| piece.phase == "complete")
515            .count();
516        Self {
517            id: recording.id,
518            sha256: recording.sha256,
519            original_filename: recording.original_filename,
520            content_type: "audio/wav",
521            size_bytes: recording.size_bytes,
522            source_created_at: recording.recorded_at.to_rfc3339(),
523            received_at: recording.received_at.to_rfc3339(),
524            updated_at: recording.received_at.to_rfc3339(),
525            status,
526            transcription_model: recording.transcription_model,
527            reconciliation_model: recording.reconciliation_model,
528            reconciliation_reasoning: recording.reconciliation_reasoning,
529            transcription_status,
530            attempt_count,
531            next_attempt_at: None,
532            last_error,
533            speaker_review,
534            transcript_piece_count: projection.pieces.len(),
535            completed_piece_count,
536        }
537    }
538}
539
540impl IngressPiece {
541    fn from_record(
542        recording: &RecordingStatus,
543        piece_index: u32,
544        piece_count: u32,
545        transcript_text: String,
546        estimated_tokens: u64,
547        record: &SessionRecord,
548    ) -> Self {
549        Self {
550            id: record.id.clone(),
551            recording_id: recording.id,
552            sha256: recording.sha256.clone(),
553            original_filename: recording.original_filename.clone(),
554            source_created_at: recording.recorded_at.to_rfc3339(),
555            piece_index,
556            piece_count,
557            transcript_text,
558            estimated_tokens,
559            phase: record.phase.clone(),
560            provenance_id: record.provenance_id.clone(),
561            state: record.state.clone(),
562            version: record.version,
563            ingress_failure_count: record.ingress_failure_count,
564            ingress_failures: record.ingress_failures.clone(),
565            created_at: record.started_at.clone(),
566            updated_at: record.updated_at.clone(),
567        }
568    }
569}
570
571async fn retry_ingress_exact(
572    history: &SessionHistory,
573    recordings: &[RecordingStatus],
574    user_id: &str,
575    effective_context_tokens: u64,
576    input: RetryIngress,
577) -> Result<SessionRecord, Error> {
578    let current = history.get(&input.piece_id).await.map_err(history_error)?;
579    let source_id = ingress_source_id(&current).ok_or_else(Error::not_found)?;
580    let (recording_id, piece_index) =
581        parse_audio_piece_id(source_id).ok_or_else(Error::not_found)?;
582    let recording = recordings
583        .iter()
584        .find(|recording| {
585            recording.id == recording_id
586                && recording_belongs_to(recording, user_id)
587                && matches!(&recording.state, RecordingState::Complete { .. })
588        })
589        .ok_or_else(Error::not_found)?;
590
591    let plan = transcript_plan(recording, effective_context_tokens)?;
592    let piece = plan
593        .pieces()
594        .get(piece_index as usize)
595        .filter(|piece| piece.index == piece_index && piece.id == source_id)
596        .ok_or_else(|| {
597            Error::conflict(
598                "transcript piece no longer matches the authoritative audio segmentation",
599            )
600        })?;
601    validate_persisted_record(recording, piece, &current)?;
602
603    if input
604        .state
605        .as_ref()
606        .is_some_and(|state| state != &current.state)
607    {
608        return Err(Error::conflict(
609            "retry state does not match the current retained Session History state",
610        ));
611    }
612
613    history
614        .retry_ingress(
615            &input.piece_id,
616            HistoryRetryIngress {
617                expected_version: input.expected_version,
618                state: current.state,
619            },
620        )
621        .await
622        .map_err(history_error)
623}
624
625async fn synchronize_recordings(
626    history: &SessionHistory,
627    recordings: &[RecordingStatus],
628    user_id: &str,
629    effective_context_tokens: u64,
630) -> Result<(), Error> {
631    let histories = history.list().await.map_err(history_error)?;
632    let mut existing = histories
633        .iter()
634        .filter_map(ingress_source_id)
635        .map(str::to_owned)
636        .collect::<HashSet<_>>();
637
638    for recording in recordings {
639        if !recording_belongs_to(recording, user_id)
640            || !matches!(&recording.state, RecordingState::Complete { .. })
641            || !speaker_labels_authorized(recording)
642        {
643            continue;
644        }
645
646        let persisted = persisted_audio_records(recording.id, &histories)?;
647        let persisted_count = persisted_piece_count(&persisted)?;
648        if persisted_count.is_some_and(|count| persisted.len() == count as usize) {
649            continue;
650        }
651
652        let plan = transcript_plan(recording, effective_context_tokens)?;
653        if persisted_count.is_some_and(|count| count != plan.pieces().len() as u32) {
654            continue;
655        }
656
657        for (index, summary) in &persisted {
658            let Some(piece) = plan.pieces().get(*index as usize) else {
659                return Err(Error::conflict(
660                    "persisted audio piece is outside the authoritative segmentation",
661                ));
662            };
663            let current = history.get(&summary.id).await.map_err(history_error)?;
664            validate_persisted_record(recording, piece, &current)?;
665        }
666
667        for piece in plan.pieces() {
668            if existing.contains(&piece.id) {
669                continue;
670            }
671            let created = history
672                .enqueue_ingress(NewIngressSession {
673                    idempotency_id: piece.id.clone(),
674                    started_at: recording.recorded_at.to_rfc3339(),
675                    source_session_type: "audio".into(),
676                    kind: SessionKind::AudioIngress,
677                    effective_context_tokens,
678                    text: piece.formatted_text(recording).map_err(plan_error)?,
679                    metadata: piece.metadata(recording),
680                })
681                .await
682                .map_err(history_error)?;
683            if !created.created {
684                validate_persisted_record(recording, piece, &created.value)?;
685            }
686            existing.insert(piece.id.clone());
687        }
688    }
689    Ok(())
690}
691
692fn speaker_labels_authorized(recording: &RecordingStatus) -> bool {
693    recording
694        .correction_packet
695        .as_ref()
696        .is_none_or(|packet| packet.confirmation_state == ConfirmationState::Confirmed)
697}
698
699fn ingress_projection(
700    recording: &RecordingStatus,
701    histories: &[SessionRecord],
702    effective_context_tokens: u64,
703) -> Result<IngressProjection, Error> {
704    if !matches!(&recording.state, RecordingState::Complete { .. }) {
705        return Ok(IngressProjection::default());
706    }
707    let persisted = persisted_audio_records(recording.id, histories)?;
708    if persisted.is_empty() {
709        return Ok(IngressProjection::default());
710    }
711    let persisted_count = persisted_piece_count(&persisted)?
712        .unwrap_or_else(|| persisted.last().map_or(0, |(index, _)| index + 1));
713    let current = transcript_plan(recording, effective_context_tokens)?;
714    let current_matches = current.pieces().len() == persisted_count as usize;
715    let whole = if persisted_count == 1 && !current_matches {
716        Some(transcript_plan(recording, u64::MAX)?)
717    } else {
718        None
719    };
720
721    let mut pieces = Vec::with_capacity(persisted.len());
722    for (index, record) in persisted {
723        let planned = if persisted_count == 1 {
724            if current_matches {
725                current.pieces().first()
726            } else {
727                whole.as_ref().and_then(|plan| plan.pieces().first())
728            }
729        } else if current_matches {
730            current.pieces().get(index as usize)
731        } else {
732            None
733        };
734        let (text, estimated_tokens) = planned.map_or_else(
735            || (String::new(), 0),
736            |piece| (piece.text.clone(), piece.estimated_tokens),
737        );
738        pieces.push(IngressPiece::from_record(
739            recording,
740            index,
741            persisted_count,
742            text,
743            estimated_tokens,
744            record,
745        ));
746    }
747    Ok(IngressProjection { pieces })
748}
749
750fn persisted_audio_records(
751    recording_id: Uuid,
752    histories: &[SessionRecord],
753) -> Result<Vec<(u32, &SessionRecord)>, Error> {
754    let prefix = format!("audio:{recording_id}:");
755    let mut records = Vec::new();
756    let mut indexes = HashSet::new();
757    for record in histories {
758        let Some(source_id) = ingress_source_id(record) else {
759            continue;
760        };
761        let Some(suffix) = source_id.strip_prefix(&prefix) else {
762            continue;
763        };
764        let Some((_, index)) = parse_audio_piece_id(source_id) else {
765            let message = if suffix.parse::<u32>().is_err() {
766                "persisted audio piece has an invalid deterministic identity"
767            } else {
768                "persisted audio piece has a noncanonical deterministic identity"
769            };
770            return Err(Error::conflict(message));
771        };
772        if !indexes.insert(index) {
773            return Err(Error::conflict(
774                "multiple persisted audio pieces claim the same deterministic identity",
775            ));
776        }
777        records.push((index, record));
778    }
779    records.sort_by_key(|(index, _)| *index);
780    Ok(records)
781}
782
783fn persisted_piece_count(persisted: &[(u32, &SessionRecord)]) -> Result<Option<u32>, Error> {
784    let mut declared_count = None;
785    for (id_index, record) in persisted {
786        let Some(metadata) = record
787            .state
788            .pointer("/ingressSource/metadata")
789            .and_then(Value::as_object)
790        else {
791            continue;
792        };
793        if let Some(index) = metadata.get("pieceIndex").and_then(Value::as_u64) {
794            let index = u32::try_from(index)
795                .map_err(|_| Error::conflict("persisted audio piece index exceeds u32"))?;
796            if index != *id_index {
797                return Err(Error::conflict(
798                    "persisted audio piece index conflicts with its deterministic identity",
799                ));
800            }
801        }
802        let Some(count) = metadata.get("pieceCount").and_then(Value::as_u64) else {
803            continue;
804        };
805        let count = u32::try_from(count)
806            .map_err(|_| Error::conflict("persisted audio piece count exceeds u32"))?;
807        if count == 0 || *id_index >= count {
808            return Err(Error::conflict(
809                "persisted audio piece count conflicts with its deterministic identity",
810            ));
811        }
812        if declared_count
813            .replace(count)
814            .is_some_and(|prior| prior != count)
815        {
816            return Err(Error::conflict(
817                "persisted audio pieces disagree about their piece count",
818            ));
819        }
820    }
821    Ok(declared_count)
822}
823
824fn validate_persisted_record(
825    recording: &RecordingStatus,
826    piece: &TranscriptPiece,
827    record: &SessionRecord,
828) -> Result<(), Error> {
829    let source_id = ingress_source_id(record).ok_or_else(|| {
830        Error::conflict("replayed audio ingress omitted its deterministic identity")
831    })?;
832    if source_id != piece.id {
833        return Err(Error::conflict(
834            "replayed audio ingress returned a different deterministic identity",
835        ));
836    }
837    if record.state.get("sessionType").and_then(Value::as_str) != Some("audio")
838        || record
839            .state
840            .pointer("/chatendMetadata/kind")
841            .and_then(Value::as_str)
842            != Some("audio_ingress")
843    {
844        return Err(Error::conflict(
845            "persisted transcript piece is not an authoritative audio ingress session",
846        ));
847    }
848    if record.state.pointer("/ingressSource/metadata") != Some(&piece.metadata(recording)) {
849        return Err(Error::conflict(
850            "persisted transcript piece metadata no longer matches the authoritative audio piece",
851        ));
852    }
853    let expected_text = piece.formatted_text(recording).map_err(plan_error)?;
854    if record
855        .state
856        .pointer("/transcript/0/content")
857        .and_then(Value::as_str)
858        != Some(expected_text.as_str())
859    {
860        return Err(Error::conflict(
861            "persisted transcript piece text no longer matches the authoritative audio piece",
862        ));
863    }
864    Ok(())
865}
866
867fn ingress_source_id(record: &SessionRecord) -> Option<&str> {
868    record
869        .state
870        .pointer("/ingressSource/idempotencyId")
871        .and_then(Value::as_str)
872}
873
874fn transcript_plan(
875    recording: &RecordingStatus,
876    effective_context_tokens: u64,
877) -> Result<TranscriptPlan, Error> {
878    TranscriptPlan::new(recording, effective_context_tokens).map_err(plan_error)
879}
880
881fn validate_effective_context(effective_context_tokens: u64) -> Result<(), Error> {
882    let piece_tokens = effective_context_tokens / 4;
883    if piece_tokens == 0 {
884        return Err(Error::invalid(
885            "effective ingress context must contain at least four tokens",
886        ));
887    }
888    let characters = piece_tokens
889        .checked_mul(4)
890        .ok_or_else(|| Error::invalid("effective ingress context is too large"))?;
891    usize::try_from(characters)
892        .map(|_| ())
893        .map_err(|_| Error::invalid("effective ingress context exceeds platform limits"))
894}
895
896fn plan_error(error: PlanError) -> Error {
897    match error {
898        PlanError::InvalidInput(message) => Error::invalid(message),
899        PlanError::Conflict(message) => Error::conflict(message),
900        PlanError::Internal(message) => Error::internal(message),
901    }
902}
903
904fn processing_stage(status: &kcode_audio_ingress::TranscriptionStatus) -> &'static str {
905    let plan_complete = status.steps.iter().any(|entry| {
906        entry.step == kcode_audio_ingress::Step::PlanChunks
907            && entry.state == kcode_audio_ingress::StepState::Completed
908    });
909    if !plan_complete {
910        return "chunking";
911    }
912    let chunks_complete = status
913        .steps
914        .iter()
915        .filter(|entry| {
916            matches!(
917                entry.step,
918                kcode_audio_ingress::Step::TranscribeChunk { .. }
919            )
920        })
921        .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
922    if chunks_complete {
923        let analyses_complete = status
924            .steps
925            .iter()
926            .filter(|entry| matches!(entry.step, kcode_audio_ingress::Step::ParseChunk { .. }))
927            .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
928        if !analyses_complete {
929            return "analyzing_speakers";
930        }
931        let training_active = status.steps.iter().any(|entry| {
932            entry.step == kcode_audio_ingress::Step::TrainIdentities
933                && matches!(
934                    entry.state,
935                    kcode_audio_ingress::StepState::Running
936                        | kcode_audio_ingress::StepState::Retrying
937                )
938        });
939        if training_active {
940            "training_speakers"
941        } else {
942            "reconciling"
943        }
944    } else {
945        "transcribing"
946    }
947}
948
949fn audio_error(error: kcode_audio_ingress::Error) -> Error {
950    match error.kind() {
951        AudioErrorKind::InvalidInput => Error::new(ErrorKind::InvalidInput, error.to_string()),
952        AudioErrorKind::NotFound => Error::new(ErrorKind::NotFound, error.to_string()),
953        AudioErrorKind::Conflict => Error::new(ErrorKind::Conflict, error.to_string()),
954        AudioErrorKind::Internal => Error::internal(error),
955    }
956}
957
958fn history_error(error: kcode_session_history::Error) -> Error {
959    let kind = match error.kind {
960        kcode_session_history::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
961        kcode_session_history::ErrorKind::NotFound => ErrorKind::NotFound,
962        kcode_session_history::ErrorKind::Conflict => ErrorKind::Conflict,
963        kcode_session_history::ErrorKind::Storage => ErrorKind::Internal,
964    };
965    Error::new(kind, error.message)
966}
967
968#[cfg(test)]
969mod tests {
970    use std::path::{Path, PathBuf};
971
972    use serde_json::json;
973
974    use super::*;
975
976    struct TestRoot(PathBuf);
977
978    impl TestRoot {
979        fn new() -> Self {
980            let path = std::env::temp_dir().join(format!(
981                "kcode-audio-session-ingress-test-{}",
982                Uuid::new_v4()
983            ));
984            std::fs::create_dir(&path).unwrap();
985            Self(path)
986        }
987
988        fn path(&self) -> &Path {
989            &self.0
990        }
991    }
992
993    impl Drop for TestRoot {
994        fn drop(&mut self) {
995            let _ = std::fs::remove_dir_all(&self.0);
996        }
997    }
998
999    fn history(root: &Path) -> SessionHistory {
1000        SessionHistory::open(kcode_session_history::Config {
1001            directory: root.join("active"),
1002            completed_list: root.join("completed.txt"),
1003            provider_cost_compatibility: None,
1004        })
1005        .unwrap()
1006    }
1007
1008    fn completed_recording(transcript: impl Into<String>) -> RecordingStatus {
1009        completed_recording_for_user("user", transcript)
1010    }
1011
1012    fn completed_recording_for_user(
1013        user_id: impl Into<String>,
1014        transcript: impl Into<String>,
1015    ) -> RecordingStatus {
1016        let now = Utc::now();
1017        RecordingStatus {
1018            id: Uuid::new_v4(),
1019            user_id: user_id.into(),
1020            sha256: "0".repeat(64),
1021            original_filename: "meeting.final.WAV".into(),
1022            size_bytes: 42,
1023            recorded_at: now,
1024            received_at: now,
1025            transcription_model: "transcription-model".into(),
1026            reconciliation_model: "reconciliation-model".into(),
1027            reconciliation_reasoning: "xhigh".into(),
1028            state: RecordingState::Complete {
1029                transcript: transcript.into(),
1030            },
1031            correction_packet: None,
1032        }
1033    }
1034
1035    fn with_speaker_packet(
1036        mut recording: RecordingStatus,
1037        state: ConfirmationState,
1038        confirmed_name: Option<&str>,
1039    ) -> RecordingStatus {
1040        recording.correction_packet = Some(CorrectionPacket {
1041            recording_id: recording.id,
1042            user_id: recording.user_id.clone(),
1043            sha256: recording.sha256.clone(),
1044            original_filename: recording.original_filename.clone(),
1045            size_bytes: recording.size_bytes,
1046            recorded_at: recording.recorded_at,
1047            clean: true,
1048            chunk_count: 1,
1049            chunks: vec![kcode_audio_ingress::CorrectionChunk {
1050                chunk_index: 0,
1051                chunk_count: 1,
1052                audio_start_ms: 0,
1053                audio_end_ms: 1_000,
1054                raw_gemini_response: "raw".into(),
1055                parsed: kcode_audio_ingress::ParsedChunk {
1056                    utterances: Vec::new(),
1057                    notes: Vec::new(),
1058                    clip_valid: true,
1059                    clip_validity_reason: None,
1060                    speakers: Vec::new(),
1061                },
1062                observations: vec![kcode_audio_ingress::CorrectionObservation {
1063                    local_label: "Speaker A".into(),
1064                    speaker_ordinal: 0,
1065                    observation_key: kcode_audio_ingress::ObservationKey {
1066                        object_id: format!(
1067                            "kcode-audio-ingress/recording/{}/chunk/0",
1068                            recording.id
1069                        ),
1070                        piece_index: 0,
1071                    },
1072                    candidate: Some(kcode_audio_ingress::CandidateMapping {
1073                        full_name: "Classifier Candidate".into(),
1074                        cost: 1.0,
1075                        confidence: 3.0,
1076                        runner_up_full_name: None,
1077                        runner_up_cost: None,
1078                        background_population_cost: 4.0,
1079                    }),
1080                    identified_full_name: Some("Classifier Candidate".into()),
1081                    confirmed_full_name: confirmed_name.map(str::to_owned),
1082                }],
1083                clean: true,
1084            }],
1085            confirmation_state: state,
1086        });
1087        recording
1088    }
1089
1090    fn planned_piece_id(
1091        recording: &RecordingStatus,
1092        effective_context_tokens: u64,
1093        index: usize,
1094    ) -> String {
1095        TranscriptPlan::new(recording, effective_context_tokens)
1096            .unwrap()
1097            .pieces()[index]
1098            .id
1099            .clone()
1100    }
1101
1102    async fn fail_piece(history: &SessionHistory, record: SessionRecord) -> SessionRecord {
1103        let started = history
1104            .start_ingress(
1105                &record.id,
1106                kcode_session_history::StartIngress {
1107                    expected_version: record.version,
1108                    provenance_id: format!("test:{}", record.id),
1109                },
1110            )
1111            .await
1112            .unwrap();
1113        history
1114            .fail_ingress(
1115                &record.id,
1116                kcode_session_history::IngressFailure {
1117                    expected_version: started.version,
1118                    stage: "test".into(),
1119                    code: Some("input_too_large".into()),
1120                    message: "terminal test failure".into(),
1121                    rounds_used: None,
1122                    context_tokens: None,
1123                    context_window_tokens: None,
1124                },
1125            )
1126            .await
1127            .unwrap()
1128    }
1129
1130    async fn synchronized_failed_piece(
1131        history: &SessionHistory,
1132        recording: &RecordingStatus,
1133        user_id: &str,
1134        effective_context_tokens: u64,
1135    ) -> SessionRecord {
1136        synchronize_recordings(
1137            history,
1138            std::slice::from_ref(recording),
1139            user_id,
1140            effective_context_tokens,
1141        )
1142        .await
1143        .unwrap();
1144        let source_id = planned_piece_id(recording, effective_context_tokens, 0);
1145        let record = history
1146            .list()
1147            .await
1148            .unwrap()
1149            .into_iter()
1150            .find(|record| ingress_source_id(record) == Some(source_id.as_str()))
1151            .unwrap();
1152        fail_piece(history, record).await
1153    }
1154
1155    #[test]
1156    fn shared_status_is_filtered_to_the_configured_user() {
1157        let mut own = completed_recording_for_user("own-user", "Own transcript");
1158        own.sha256 = "a".repeat(64);
1159        let mut foreign = completed_recording_for_user("foreign-user", "Foreign transcript");
1160        foreign.sha256 = own.sha256.clone();
1161        let recordings = [own.clone(), foreign.clone()];
1162
1163        let visible = recordings
1164            .iter()
1165            .filter(|recording| recording_belongs_to(recording, "own-user"))
1166            .collect::<Vec<_>>();
1167        assert_eq!(visible.len(), 1);
1168        assert_eq!(visible[0].id, own.id);
1169        assert_eq!(
1170            validate_submission_owner(&foreign, "own-user")
1171                .unwrap_err()
1172                .kind(),
1173            ErrorKind::Conflict
1174        );
1175    }
1176
1177    #[test]
1178    fn confirmed_label_retries_must_match_the_complete_mapping() {
1179        let recording = with_speaker_packet(
1180            completed_recording("Transcript"),
1181            ConfirmationState::Confirmed,
1182            Some("Human Choice"),
1183        );
1184        let packet = recording.correction_packet.as_ref().unwrap();
1185        let key = packet.chunks[0].observations[0].observation_key.clone();
1186        let matching = RecordingConfirmation {
1187            recording_id: recording.id,
1188            observations: vec![kcode_audio_ingress::ObservationConfirmation {
1189                observation_key: key.clone(),
1190                confirmed_full_name: " Human Choice ".into(),
1191            }],
1192        };
1193        let conflicting = RecordingConfirmation {
1194            recording_id: recording.id,
1195            observations: vec![kcode_audio_ingress::ObservationConfirmation {
1196                observation_key: key,
1197                confirmed_full_name: "Different Choice".into(),
1198            }],
1199        };
1200        assert!(confirmation_matches(packet, &matching));
1201        assert!(!confirmation_matches(packet, &conflicting));
1202    }
1203
1204    #[tokio::test]
1205    async fn classifier_aware_recordings_wait_for_human_review() {
1206        let root = TestRoot::new();
1207        let history = history(root.path());
1208        let recording = with_speaker_packet(
1209            completed_recording("Transcript"),
1210            ConfirmationState::AutomaticallyTrained,
1211            None,
1212        );
1213        synchronize_recordings(&history, &[recording], "user", 400)
1214            .await
1215            .unwrap();
1216        assert!(history.list().await.unwrap().is_empty());
1217    }
1218
1219    #[tokio::test]
1220    async fn confirmed_labels_accompany_the_transcript() {
1221        let root = TestRoot::new();
1222        let history = history(root.path());
1223        let recording = with_speaker_packet(
1224            completed_recording("Transcript"),
1225            ConfirmationState::Confirmed,
1226            Some("Human Choice"),
1227        );
1228        synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 400)
1229            .await
1230            .unwrap();
1231        let records = history.list().await.unwrap();
1232        assert_eq!(records.len(), 1);
1233        let state = serde_json::to_string(&records[0].state).unwrap();
1234        assert!(state.contains("Human-confirmed speaker-label data"));
1235        assert!(state.contains("Human Choice"));
1236        assert!(state.contains("Classifier Candidate"));
1237    }
1238
1239    #[tokio::test]
1240    async fn bulk_synchronization_creates_no_foreign_pieces() {
1241        let root = TestRoot::new();
1242        let history = history(root.path());
1243        let own = completed_recording_for_user("own-user", "Own transcript");
1244        let foreign = completed_recording_for_user("foreign-user", "Foreign transcript");
1245
1246        synchronize_recordings(&history, &[own.clone(), foreign.clone()], "own-user", 400)
1247            .await
1248            .unwrap();
1249
1250        let records = history.list().await.unwrap();
1251        let own_source = planned_piece_id(&own, 400, 0);
1252        let foreign_source = planned_piece_id(&foreign, 400, 0);
1253        assert_eq!(records.len(), 1);
1254        assert_eq!(ingress_source_id(&records[0]), Some(own_source.as_str()));
1255        assert!(
1256            records
1257                .iter()
1258                .all(|record| ingress_source_id(record) != Some(foreign_source.as_str()))
1259        );
1260    }
1261
1262    #[tokio::test]
1263    async fn synchronization_is_idempotent() {
1264        let root = TestRoot::new();
1265        let history = history(root.path());
1266        let recording = completed_recording("a".repeat(801));
1267        for _ in 0..2 {
1268            synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 400)
1269                .await
1270                .unwrap();
1271        }
1272        assert_eq!(history.list().await.unwrap().len(), 3);
1273    }
1274
1275    #[tokio::test]
1276    async fn context_changes_leave_complete_accepted_set_untouched() {
1277        let root = TestRoot::new();
1278        let history = history(root.path());
1279        let recording = completed_recording("a".repeat(801));
1280        synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 400)
1281            .await
1282            .unwrap();
1283        let before = history
1284            .list()
1285            .await
1286            .unwrap()
1287            .iter()
1288            .filter_map(ingress_source_id)
1289            .map(str::to_owned)
1290            .collect::<HashSet<_>>();
1291        synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 800)
1292            .await
1293            .unwrap();
1294        let after = history
1295            .list()
1296            .await
1297            .unwrap()
1298            .iter()
1299            .filter_map(ingress_source_id)
1300            .map(str::to_owned)
1301            .collect::<HashSet<_>>();
1302        assert_eq!(before, after);
1303    }
1304
1305    #[tokio::test]
1306    async fn exact_current_piece_retry_accepts_equal_optional_state() {
1307        let root = TestRoot::new();
1308        let history = history(root.path());
1309        let recording = completed_recording("Transcript");
1310        let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1311
1312        let retried = retry_ingress_exact(
1313            &history,
1314            std::slice::from_ref(&recording),
1315            "user",
1316            400,
1317            RetryIngress {
1318                piece_id: failed.id,
1319                expected_version: failed.version,
1320                state: Some(failed.state),
1321            },
1322        )
1323        .await
1324        .unwrap();
1325        assert_eq!(retried.phase, "ingress_pending");
1326    }
1327
1328    #[tokio::test]
1329    async fn retry_without_optional_state_remains_compatible() {
1330        let root = TestRoot::new();
1331        let history = history(root.path());
1332        let recording = completed_recording("Transcript");
1333        let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1334
1335        let retried = retry_ingress_exact(
1336            &history,
1337            std::slice::from_ref(&recording),
1338            "user",
1339            400,
1340            RetryIngress {
1341                piece_id: failed.id,
1342                expected_version: failed.version,
1343                state: None,
1344            },
1345        )
1346        .await
1347        .unwrap();
1348        assert_eq!(retried.phase, "ingress_pending");
1349    }
1350
1351    #[tokio::test]
1352    async fn changed_retry_state_conflicts_without_mutation() {
1353        let root = TestRoot::new();
1354        let history = history(root.path());
1355        let recording = completed_recording("Transcript");
1356        let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1357        let mut changed = failed.state.clone();
1358        changed["pendingTurn"] = json!(true);
1359
1360        let error = retry_ingress_exact(
1361            &history,
1362            std::slice::from_ref(&recording),
1363            "user",
1364            400,
1365            RetryIngress {
1366                piece_id: failed.id.clone(),
1367                expected_version: failed.version,
1368                state: Some(changed),
1369            },
1370        )
1371        .await
1372        .unwrap_err();
1373        assert_eq!(error.kind(), ErrorKind::Conflict);
1374        assert_eq!(
1375            history.get(&failed.id).await.unwrap().version,
1376            failed.version
1377        );
1378    }
1379
1380    #[tokio::test]
1381    async fn unrelated_session_history_id_cannot_retry() {
1382        let root = TestRoot::new();
1383        let history = history(root.path());
1384        let recording = completed_recording("Transcript");
1385        let unrelated = history
1386            .start(kcode_session_history::StartSession {
1387                idempotency_id: "unrelated".into(),
1388                started_at: recording.recorded_at.to_rfc3339(),
1389                session_type: "conversation".into(),
1390                duration_minutes: None,
1391                custom_prompt: None,
1392            })
1393            .await
1394            .unwrap()
1395            .value;
1396
1397        let error = retry_ingress_exact(
1398            &history,
1399            std::slice::from_ref(&recording),
1400            "user",
1401            400,
1402            RetryIngress {
1403                piece_id: unrelated.id.clone(),
1404                expected_version: unrelated.version,
1405                state: None,
1406            },
1407        )
1408        .await
1409        .unwrap_err();
1410        assert_eq!(error.kind(), ErrorKind::NotFound);
1411        assert_eq!(
1412            history.get(&unrelated.id).await.unwrap().version,
1413            unrelated.version
1414        );
1415    }
1416
1417    #[tokio::test]
1418    async fn malformed_audio_looking_id_cannot_retry() {
1419        let root = TestRoot::new();
1420        let history = history(root.path());
1421        let recording = completed_recording("Transcript");
1422        let malformed = history
1423            .enqueue_ingress(NewIngressSession {
1424                idempotency_id: "audio:not-a-uuid:0".into(),
1425                started_at: recording.recorded_at.to_rfc3339(),
1426                source_session_type: "audio".into(),
1427                kind: SessionKind::AudioIngress,
1428                effective_context_tokens: 400,
1429                text: "Malformed source".into(),
1430                metadata: json!({}),
1431            })
1432            .await
1433            .unwrap()
1434            .value;
1435
1436        let error = retry_ingress_exact(
1437            &history,
1438            std::slice::from_ref(&recording),
1439            "user",
1440            400,
1441            RetryIngress {
1442                piece_id: malformed.id.clone(),
1443                expected_version: malformed.version,
1444                state: None,
1445            },
1446        )
1447        .await
1448        .unwrap_err();
1449        assert_eq!(error.kind(), ErrorKind::NotFound);
1450        assert_eq!(
1451            history.get(&malformed.id).await.unwrap().version,
1452            malformed.version
1453        );
1454    }
1455
1456    #[tokio::test]
1457    async fn foreign_audio_piece_cannot_retry_or_mutate() {
1458        let root = TestRoot::new();
1459        let history = history(root.path());
1460        let own = completed_recording_for_user("own-user", "Own transcript");
1461        let foreign = completed_recording_for_user("foreign-user", "Foreign transcript");
1462        let failed = synchronized_failed_piece(&history, &foreign, "foreign-user", 400).await;
1463
1464        let error = retry_ingress_exact(
1465            &history,
1466            &[own, foreign],
1467            "own-user",
1468            400,
1469            RetryIngress {
1470                piece_id: failed.id.clone(),
1471                expected_version: failed.version,
1472                state: None,
1473            },
1474        )
1475        .await
1476        .unwrap_err();
1477        assert_eq!(error.kind(), ErrorKind::NotFound);
1478        let unchanged = history.get(&failed.id).await.unwrap();
1479        assert_eq!(unchanged.phase, "ingress_failed");
1480        assert_eq!(unchanged.version, failed.version);
1481    }
1482
1483    #[tokio::test]
1484    async fn retry_rejects_context_and_transcript_drift() {
1485        let root = TestRoot::new();
1486        let history = history(root.path());
1487        let recording = completed_recording("a".repeat(801));
1488        let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1489
1490        let context_error = retry_ingress_exact(
1491            &history,
1492            std::slice::from_ref(&recording),
1493            "user",
1494            800,
1495            RetryIngress {
1496                piece_id: failed.id.clone(),
1497                expected_version: failed.version,
1498                state: None,
1499            },
1500        )
1501        .await
1502        .unwrap_err();
1503        assert_eq!(context_error.kind(), ErrorKind::Conflict);
1504
1505        let mut changed = recording.clone();
1506        changed.state = RecordingState::Complete {
1507            transcript: format!("b{}", "a".repeat(800)),
1508        };
1509        let transcript_error = retry_ingress_exact(
1510            &history,
1511            std::slice::from_ref(&changed),
1512            "user",
1513            400,
1514            RetryIngress {
1515                piece_id: failed.id.clone(),
1516                expected_version: failed.version,
1517                state: None,
1518            },
1519        )
1520        .await
1521        .unwrap_err();
1522        assert_eq!(transcript_error.kind(), ErrorKind::Conflict);
1523        assert_eq!(
1524            history.get(&failed.id).await.unwrap().version,
1525            failed.version
1526        );
1527    }
1528}