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_session_history::{
11 NewIngressSession, RetryIngress as HistoryRetryIngress, SessionHistory, SessionRecord,
12 chatend::SessionKind,
13};
14use serde_json::{Value, json};
15use uuid::Uuid;
16
17const ESTIMATED_CHARACTERS_PER_TOKEN: u64 = 4;
18const INGRESS_CONTEXT_DIVISOR: u64 = 4;
19
20#[derive(Clone, Debug)]
22pub struct Config {
23 pub user_id: String,
25 pub effective_context_tokens: u64,
27}
28
29#[derive(Clone, Debug)]
31pub struct RecordingInput {
32 pub bytes: Vec<u8>,
34 pub recorded_at: DateTime<Utc>,
36 pub original_filename: Option<String>,
38}
39
40#[derive(Clone, Debug)]
42pub struct RecordingSubmission {
43 pub recording: Recording,
45 pub deduplicated: bool,
47}
48
49#[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#[derive(Clone, Debug)]
75pub struct SpeakerReview {
76 pub clean: bool,
77 pub confirmation_state: ConfirmationState,
78 pub observation_count: usize,
79}
80
81#[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#[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#[derive(Clone, Debug)]
114pub struct RetryIngress {
115 pub piece_id: String,
116 pub expected_version: i64,
117 pub state: Option<Value>,
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum ErrorKind {
125 InvalidInput,
126 NotFound,
127 Conflict,
128 Internal,
129}
130
131#[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#[derive(Clone)]
188pub struct Coordinator {
189 audio: AudioIngress,
190 history: SessionHistory,
191 user_id: String,
192 effective_context_tokens: u64,
193 maximum_piece_characters: usize,
194}
195
196impl Coordinator {
197 pub fn new(
198 audio: AudioIngress,
199 history: SessionHistory,
200 config: Config,
201 ) -> Result<Self, Error> {
202 if config.user_id.trim().is_empty() {
203 return Err(Error::invalid("audio user ID must not be empty"));
204 }
205 let maximum_piece_characters = maximum_piece_characters(config.effective_context_tokens)?;
206 Ok(Self {
207 audio,
208 history,
209 user_id: config.user_id,
210 effective_context_tokens: config.effective_context_tokens,
211 maximum_piece_characters,
212 })
213 }
214
215 pub fn health(&self) -> Result<(), Error> {
216 self.audio.status().map_err(audio_error)?;
217 Ok(())
218 }
219
220 pub async fn submit(&self, input: RecordingInput) -> Result<RecordingSubmission, Error> {
221 let submission = self
222 .audio
223 .submit(AudioInput {
224 user_id: self.user_id.clone(),
225 bytes: input.bytes,
226 recorded_at: input.recorded_at,
227 original_filename: input.original_filename,
228 })
229 .await
230 .map_err(audio_error)?;
231 let recording_status = self
232 .audio
233 .status()
234 .map_err(audio_error)?
235 .recordings
236 .into_iter()
237 .find(|recording| recording.id == submission.recording_id)
238 .ok_or_else(Error::not_found)?;
239 validate_submission_owner(&recording_status, &self.user_id)?;
240
241 let histories = self.history.list().await.map_err(history_error)?;
242 let projection = ingress_projection(
243 &recording_status,
244 &histories,
245 self.effective_context_tokens,
246 self.maximum_piece_characters,
247 )?;
248 Ok(RecordingSubmission {
249 recording: Recording::from_status(recording_status, &projection),
250 deduplicated: submission.deduplicated,
251 })
252 }
253
254 pub async fn recordings(&self) -> Result<Vec<Recording>, Error> {
255 let histories = self.history.list().await.map_err(history_error)?;
256 self.audio
257 .status()
258 .map_err(audio_error)?
259 .recordings
260 .into_iter()
261 .filter(|recording| recording_belongs_to(recording, &self.user_id))
262 .map(|recording| {
263 let projection = ingress_projection(
264 &recording,
265 &histories,
266 self.effective_context_tokens,
267 self.maximum_piece_characters,
268 )?;
269 Ok(Recording::from_status(recording, &projection))
270 })
271 .collect()
272 }
273
274 pub async fn recording_by_sha256(&self, sha256: &str) -> Result<Recording, Error> {
275 if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
276 return Err(Error::invalid(
277 "audio SHA-256 must contain exactly 64 hexadecimal characters",
278 ));
279 }
280 let normalized = sha256.to_ascii_lowercase();
281 self.recordings()
282 .await?
283 .into_iter()
284 .find(|recording| recording.sha256 == normalized)
285 .ok_or_else(Error::not_found)
286 }
287
288 pub async fn recording_history(&self, recording_id: Uuid) -> Result<RecordingHistory, Error> {
289 let recording_status = self
290 .audio
291 .status()
292 .map_err(audio_error)?
293 .recordings
294 .into_iter()
295 .find(|recording| {
296 recording.id == recording_id && recording_belongs_to(recording, &self.user_id)
297 })
298 .ok_or_else(Error::not_found)?;
299 let histories = self.history.list().await.map_err(history_error)?;
300 let projection = ingress_projection(
301 &recording_status,
302 &histories,
303 self.effective_context_tokens,
304 self.maximum_piece_characters,
305 )?;
306 let final_transcript = match &recording_status.state {
307 RecordingState::Complete { transcript } => Some(transcript.clone()),
308 _ => None,
309 };
310 let correction_packet = recording_status.correction_packet.clone();
311 let recording = Recording::from_status(recording_status, &projection);
312 Ok(RecordingHistory {
313 recording,
314 final_transcript,
315 correction_packet,
316 pieces: projection.pieces,
317 })
318 }
319
320 pub fn retry_recording(&self, recording_id: Uuid) -> Result<(), Error> {
321 let owned = self
322 .audio
323 .status()
324 .map_err(audio_error)?
325 .recordings
326 .into_iter()
327 .any(|recording| {
328 recording.id == recording_id && recording_belongs_to(&recording, &self.user_id)
329 });
330 if !owned {
331 return Err(Error::not_found());
332 }
333 self.audio.retry(recording_id).map_err(audio_error)
334 }
335
336 pub async fn confirm_speakers(
337 &self,
338 confirmation: RecordingConfirmation,
339 ) -> Result<CorrectionPacket, Error> {
340 let recording_id = confirmation.recording_id;
341 let recording = self
342 .audio
343 .status()
344 .map_err(audio_error)?
345 .recordings
346 .into_iter()
347 .find(|recording| {
348 recording.id == recording_id && recording_belongs_to(recording, &self.user_id)
349 })
350 .ok_or_else(Error::not_found)?;
351 let histories = self.history.list().await.map_err(history_error)?;
352
353 if let Some(packet) = recording
354 .correction_packet
355 .as_ref()
356 .filter(|packet| confirmation_matches(packet, &confirmation))
357 {
358 synchronize_recordings(
359 &self.history,
360 std::slice::from_ref(&recording),
361 &self.user_id,
362 self.effective_context_tokens,
363 self.maximum_piece_characters,
364 )
365 .await?;
366 return Ok(packet.clone());
367 }
368 if recording_has_ingress(recording_id, &histories) {
369 return Err(Error::conflict(
370 "speaker labels are already bound to accepted transcript ingress",
371 ));
372 }
373
374 let packet = self
375 .audio
376 .confirm_speakers(confirmation)
377 .map_err(audio_error)?;
378 let recording = self
379 .audio
380 .status()
381 .map_err(audio_error)?
382 .recordings
383 .into_iter()
384 .find(|recording| {
385 recording.id == recording_id && recording_belongs_to(recording, &self.user_id)
386 })
387 .ok_or_else(Error::not_found)?;
388 synchronize_recordings(
389 &self.history,
390 std::slice::from_ref(&recording),
391 &self.user_id,
392 self.effective_context_tokens,
393 self.maximum_piece_characters,
394 )
395 .await?;
396 Ok(packet)
397 }
398
399 pub async fn retry_ingress(&self, input: RetryIngress) -> Result<SessionRecord, Error> {
400 let recordings = self.audio.status().map_err(audio_error)?.recordings;
401 retry_ingress_exact(
402 &self.history,
403 &recordings,
404 &self.user_id,
405 self.effective_context_tokens,
406 self.maximum_piece_characters,
407 input,
408 )
409 .await
410 }
411
412 pub async fn synchronize_completed_transcripts(&self) -> Result<(), Error> {
413 let recordings = self.audio.status().map_err(audio_error)?.recordings;
414 synchronize_recordings(
415 &self.history,
416 &recordings,
417 &self.user_id,
418 self.effective_context_tokens,
419 self.maximum_piece_characters,
420 )
421 .await
422 }
423}
424
425fn recording_belongs_to(recording: &RecordingStatus, user_id: &str) -> bool {
426 recording.user_id == user_id
427}
428
429fn validate_submission_owner(recording: &RecordingStatus, user_id: &str) -> Result<(), Error> {
430 if recording_belongs_to(recording, user_id) {
431 Ok(())
432 } else {
433 Err(Error::conflict(
434 "identical audio is already attributed to another user",
435 ))
436 }
437}
438
439fn confirmation_matches(packet: &CorrectionPacket, confirmation: &RecordingConfirmation) -> bool {
440 if packet.confirmation_state != ConfirmationState::Confirmed {
441 return false;
442 }
443 let existing = packet
444 .chunks
445 .iter()
446 .flat_map(|chunk| &chunk.observations)
447 .filter_map(|observation| {
448 observation
449 .confirmed_full_name
450 .as_deref()
451 .map(|name| (&observation.observation_key, name))
452 })
453 .collect::<HashMap<_, _>>();
454 existing.len() == confirmation.observations.len()
455 && confirmation.observations.iter().all(|observation| {
456 existing.get(&observation.observation_key).copied()
457 == Some(observation.confirmed_full_name.trim())
458 })
459}
460
461fn recording_has_ingress(recording_id: Uuid, histories: &[SessionRecord]) -> bool {
462 let prefix = format!("audio:{recording_id}:");
463 histories.iter().any(|record| {
464 ingress_source_id(record).is_some_and(|source_id| source_id.starts_with(&prefix))
465 })
466}
467
468#[derive(Debug)]
469struct PieceSpec {
470 id: String,
471 index: u32,
472 count: u32,
473 text: String,
474}
475
476#[derive(Debug)]
477struct SegmentationPlan {
478 specs: Vec<PieceSpec>,
479}
480
481#[derive(Debug, Default)]
482struct IngressProjection {
483 pieces: Vec<IngressPiece>,
484}
485
486impl Recording {
487 fn from_status(recording: RecordingStatus, projection: &IngressProjection) -> Self {
488 let speaker_review = recording
489 .correction_packet
490 .as_ref()
491 .map(|packet| SpeakerReview {
492 clean: packet.clean,
493 confirmation_state: packet.confirmation_state,
494 observation_count: packet
495 .chunks
496 .iter()
497 .map(|chunk| chunk.observations.len())
498 .sum(),
499 });
500 let awaiting_speaker_review = speaker_review
501 .as_ref()
502 .is_some_and(|review| review.confirmation_state != ConfirmationState::Confirmed);
503 let (mut status, transcription_status, attempt_count, last_error) = match recording.state {
504 RecordingState::Queued => ("uploaded".into(), None, 0, None),
505 RecordingState::Processing { attempt, progress } => (
506 processing_stage(&progress).into(),
507 serde_json::to_value(progress).ok(),
508 i64::from(attempt),
509 None,
510 ),
511 RecordingState::Complete { .. } if awaiting_speaker_review => {
512 ("speaker_review".into(), None, 0, None)
513 }
514 RecordingState::Complete { .. } => ("ready_for_ingress".into(), None, 0, None),
515 RecordingState::Failed {
516 attempts, error, ..
517 } => ("failed".into(), None, i64::from(attempts), Some(error)),
518 };
519 if !projection.pieces.is_empty() {
520 status = if projection
521 .pieces
522 .iter()
523 .all(|piece| piece.phase == "complete")
524 {
525 "complete".into()
526 } else if projection
527 .pieces
528 .iter()
529 .any(|piece| piece.phase == "ingress_failed")
530 {
531 "ingress_failed".into()
532 } else if projection
533 .pieces
534 .iter()
535 .any(|piece| piece.phase == "ingress_in_progress")
536 {
537 "ingressing".into()
538 } else {
539 "ready_for_ingress".into()
540 };
541 }
542 let completed_piece_count = projection
543 .pieces
544 .iter()
545 .filter(|piece| piece.phase == "complete")
546 .count();
547 Self {
548 id: recording.id,
549 sha256: recording.sha256,
550 original_filename: recording.original_filename,
551 content_type: "audio/wav",
552 size_bytes: recording.size_bytes,
553 source_created_at: recording.recorded_at.to_rfc3339(),
554 received_at: recording.received_at.to_rfc3339(),
555 updated_at: recording.received_at.to_rfc3339(),
556 status,
557 transcription_model: recording.transcription_model,
558 reconciliation_model: recording.reconciliation_model,
559 reconciliation_reasoning: recording.reconciliation_reasoning,
560 transcription_status,
561 attempt_count,
562 next_attempt_at: None,
563 last_error,
564 speaker_review,
565 transcript_piece_count: projection.pieces.len(),
566 completed_piece_count,
567 }
568 }
569}
570
571impl IngressPiece {
572 fn from_record(recording: &RecordingStatus, spec: &PieceSpec, record: &SessionRecord) -> Self {
573 Self {
574 id: record.id.clone(),
575 recording_id: recording.id,
576 sha256: recording.sha256.clone(),
577 original_filename: recording.original_filename.clone(),
578 source_created_at: recording.recorded_at.to_rfc3339(),
579 piece_index: spec.index,
580 piece_count: spec.count,
581 transcript_text: spec.text.clone(),
582 estimated_tokens: estimate_tokens(&spec.text),
583 phase: record.phase.clone(),
584 provenance_id: record.provenance_id.clone(),
585 state: record.state.clone(),
586 version: record.version,
587 ingress_failure_count: record.ingress_failure_count,
588 ingress_failures: record.ingress_failures.clone(),
589 created_at: record.started_at.clone(),
590 updated_at: record.updated_at.clone(),
591 }
592 }
593}
594
595async fn retry_ingress_exact(
596 history: &SessionHistory,
597 recordings: &[RecordingStatus],
598 user_id: &str,
599 effective_context_tokens: u64,
600 maximum_piece_characters: usize,
601 input: RetryIngress,
602) -> Result<SessionRecord, Error> {
603 let current = history.get(&input.piece_id).await.map_err(history_error)?;
604 let source_id = ingress_source_id(¤t).ok_or_else(Error::not_found)?;
605 let (recording_id, piece_index) =
606 parse_audio_piece_id(source_id).ok_or_else(Error::not_found)?;
607 let recording = recordings
608 .iter()
609 .find(|recording| {
610 recording.id == recording_id
611 && recording_belongs_to(recording, user_id)
612 && matches!(&recording.state, RecordingState::Complete { .. })
613 })
614 .ok_or_else(Error::not_found)?;
615
616 let plan = new_segmentation_plan(
617 recording,
618 effective_context_tokens,
619 maximum_piece_characters,
620 )?;
621 let spec = plan
622 .specs
623 .get(piece_index as usize)
624 .filter(|spec| spec.index == piece_index && spec.id == source_id)
625 .ok_or_else(|| {
626 Error::conflict(
627 "transcript piece no longer matches the authoritative audio segmentation",
628 )
629 })?;
630 validate_persisted_record(recording, spec, ¤t)?;
631
632 if input
633 .state
634 .as_ref()
635 .is_some_and(|state| state != ¤t.state)
636 {
637 return Err(Error::conflict(
638 "retry state does not match the current retained Session History state",
639 ));
640 }
641
642 history
643 .retry_ingress(
644 &input.piece_id,
645 HistoryRetryIngress {
646 expected_version: input.expected_version,
647 state: current.state,
648 },
649 )
650 .await
651 .map_err(history_error)
652}
653
654async fn synchronize_recordings(
655 history: &SessionHistory,
656 recordings: &[RecordingStatus],
657 user_id: &str,
658 effective_context_tokens: u64,
659 maximum_piece_characters: usize,
660) -> Result<(), Error> {
661 let histories = history.list().await.map_err(history_error)?;
662 let mut existing = histories
663 .iter()
664 .filter_map(ingress_source_id)
665 .map(str::to_owned)
666 .collect::<HashSet<_>>();
667
668 for recording in recordings {
669 if !recording_belongs_to(recording, user_id)
670 || !matches!(&recording.state, RecordingState::Complete { .. })
671 || !speaker_labels_authorized(recording)
672 {
673 continue;
674 }
675
676 let persisted = persisted_audio_records(recording.id, &histories)?;
677 let persisted_count = persisted_piece_count(&persisted)?;
678 if persisted_count.is_some_and(|count| persisted.len() == count as usize) {
679 continue;
680 }
681
682 let plan = new_segmentation_plan(
683 recording,
684 effective_context_tokens,
685 maximum_piece_characters,
686 )?;
687 if persisted_count.is_some_and(|count| count != plan.specs.len() as u32) {
688 continue;
689 }
690
691 for (index, summary) in &persisted {
692 let Some(spec) = plan.specs.get(*index as usize) else {
693 return Err(Error::conflict(
694 "persisted audio piece is outside the authoritative segmentation",
695 ));
696 };
697 let current = history.get(&summary.id).await.map_err(history_error)?;
698 validate_persisted_record(recording, spec, ¤t)?;
699 }
700
701 for spec in &plan.specs {
702 if existing.contains(&spec.id) {
703 continue;
704 }
705 let created = history
706 .enqueue_ingress(NewIngressSession {
707 idempotency_id: spec.id.clone(),
708 started_at: recording.recorded_at.to_rfc3339(),
709 source_session_type: "audio".into(),
710 kind: SessionKind::AudioIngress,
711 effective_context_tokens,
712 text: format_ingress_piece(recording, spec)?,
713 metadata: audio_piece_metadata(recording, spec),
714 })
715 .await
716 .map_err(history_error)?;
717 if !created.created {
718 validate_persisted_record(recording, spec, &created.value)?;
719 }
720 existing.insert(spec.id.clone());
721 }
722 }
723 Ok(())
724}
725
726fn speaker_labels_authorized(recording: &RecordingStatus) -> bool {
727 recording
728 .correction_packet
729 .as_ref()
730 .is_none_or(|packet| packet.confirmation_state == ConfirmationState::Confirmed)
731}
732
733fn ingress_projection(
734 recording: &RecordingStatus,
735 histories: &[SessionRecord],
736 effective_context_tokens: u64,
737 maximum_piece_characters: usize,
738) -> Result<IngressProjection, Error> {
739 if !matches!(&recording.state, RecordingState::Complete { .. }) {
740 return Ok(IngressProjection::default());
741 }
742 let persisted = persisted_audio_records(recording.id, histories)?;
743 if persisted.is_empty() {
744 return Ok(IngressProjection::default());
745 }
746 let persisted_count = persisted_piece_count(&persisted)?
747 .unwrap_or_else(|| persisted.last().map_or(0, |(index, _)| index + 1));
748 let current = new_segmentation_plan(
749 recording,
750 effective_context_tokens,
751 maximum_piece_characters,
752 )?;
753 let current_matches = current.specs.len() == persisted_count as usize;
754 let transcript = completed_transcript(recording)?.trim();
755 let mut pieces = Vec::with_capacity(persisted.len());
756 for (index, record) in persisted {
757 let text = if persisted_count == 1 {
758 transcript.to_owned()
759 } else if current_matches {
760 current
761 .specs
762 .get(index as usize)
763 .map(|spec| spec.text.clone())
764 .unwrap_or_default()
765 } else {
766 String::new()
767 };
768 let spec = PieceSpec {
769 id: audio_piece_id(recording.id, index),
770 index,
771 count: persisted_count,
772 text,
773 };
774 pieces.push(IngressPiece::from_record(recording, &spec, record));
775 }
776 Ok(IngressProjection { pieces })
777}
778
779fn new_segmentation_plan(
780 recording: &RecordingStatus,
781 effective_context_tokens: u64,
782 current_maximum_piece_characters: usize,
783) -> Result<SegmentationPlan, Error> {
784 if maximum_piece_characters(effective_context_tokens)? != current_maximum_piece_characters {
785 return Err(Error::internal(
786 "audio ingress context and piece limit are inconsistent",
787 ));
788 }
789 Ok(SegmentationPlan {
790 specs: transcript_piece_specs(recording, current_maximum_piece_characters)?,
791 })
792}
793
794fn persisted_audio_records(
795 recording_id: Uuid,
796 histories: &[SessionRecord],
797) -> Result<Vec<(u32, &SessionRecord)>, Error> {
798 let prefix = format!("audio:{recording_id}:");
799 let mut records = Vec::new();
800 let mut indexes = HashSet::new();
801 for record in histories {
802 let Some(source_id) = ingress_source_id(record) else {
803 continue;
804 };
805 let Some(suffix) = source_id.strip_prefix(&prefix) else {
806 continue;
807 };
808 let index = suffix.parse::<u32>().map_err(|_| {
809 Error::conflict("persisted audio piece has an invalid deterministic identity")
810 })?;
811 if audio_piece_id(recording_id, index) != source_id {
812 return Err(Error::conflict(
813 "persisted audio piece has a noncanonical deterministic identity",
814 ));
815 }
816 if !indexes.insert(index) {
817 return Err(Error::conflict(
818 "multiple persisted audio pieces claim the same deterministic identity",
819 ));
820 }
821 records.push((index, record));
822 }
823 records.sort_by_key(|(index, _)| *index);
824 Ok(records)
825}
826
827fn persisted_piece_count(persisted: &[(u32, &SessionRecord)]) -> Result<Option<u32>, Error> {
828 let mut declared_count = None;
829 for (id_index, record) in persisted {
830 let Some(metadata) = record
831 .state
832 .pointer("/ingressSource/metadata")
833 .and_then(Value::as_object)
834 else {
835 continue;
836 };
837 if let Some(index) = metadata.get("pieceIndex").and_then(Value::as_u64) {
838 let index = u32::try_from(index)
839 .map_err(|_| Error::conflict("persisted audio piece index exceeds u32"))?;
840 if index != *id_index {
841 return Err(Error::conflict(
842 "persisted audio piece index conflicts with its deterministic identity",
843 ));
844 }
845 }
846 let Some(count) = metadata.get("pieceCount").and_then(Value::as_u64) else {
847 continue;
848 };
849 let count = u32::try_from(count)
850 .map_err(|_| Error::conflict("persisted audio piece count exceeds u32"))?;
851 if count == 0 || *id_index >= count {
852 return Err(Error::conflict(
853 "persisted audio piece count conflicts with its deterministic identity",
854 ));
855 }
856 if declared_count
857 .replace(count)
858 .is_some_and(|prior| prior != count)
859 {
860 return Err(Error::conflict(
861 "persisted audio pieces disagree about their piece count",
862 ));
863 }
864 }
865 Ok(declared_count)
866}
867
868fn validate_persisted_record(
869 recording: &RecordingStatus,
870 spec: &PieceSpec,
871 record: &SessionRecord,
872) -> Result<(), Error> {
873 let source_id = ingress_source_id(record).ok_or_else(|| {
874 Error::conflict("replayed audio ingress omitted its deterministic identity")
875 })?;
876 if source_id != spec.id {
877 return Err(Error::conflict(
878 "replayed audio ingress returned a different deterministic identity",
879 ));
880 }
881 if record.state.get("sessionType").and_then(Value::as_str) != Some("audio")
882 || record
883 .state
884 .pointer("/chatendMetadata/kind")
885 .and_then(Value::as_str)
886 != Some("audio_ingress")
887 {
888 return Err(Error::conflict(
889 "persisted transcript piece is not an authoritative audio ingress session",
890 ));
891 }
892 let expected_metadata = audio_piece_metadata(recording, spec);
893 if record.state.pointer("/ingressSource/metadata") != Some(&expected_metadata) {
894 return Err(Error::conflict(
895 "persisted transcript piece metadata no longer matches the authoritative audio piece",
896 ));
897 }
898 let expected_text = format_ingress_piece(recording, spec)?;
899 if record
900 .state
901 .pointer("/transcript/0/content")
902 .and_then(Value::as_str)
903 != Some(expected_text.as_str())
904 {
905 return Err(Error::conflict(
906 "persisted transcript piece text no longer matches the authoritative audio piece",
907 ));
908 }
909 Ok(())
910}
911
912fn completed_transcript(recording: &RecordingStatus) -> Result<&str, Error> {
913 let RecordingState::Complete { transcript } = &recording.state else {
914 return Err(Error::internal(
915 "audio segmentation requested for an incomplete recording",
916 ));
917 };
918 Ok(transcript)
919}
920
921fn transcript_piece_specs(
922 recording: &RecordingStatus,
923 maximum_piece_characters: usize,
924) -> Result<Vec<PieceSpec>, Error> {
925 let RecordingState::Complete { transcript } = &recording.state else {
926 return Ok(Vec::new());
927 };
928 let pieces = split_transcript(transcript, maximum_piece_characters)?;
929 let piece_count = u32::try_from(pieces.len())
930 .map_err(|_| Error::internal("audio transcript contains too many pieces"))?;
931 pieces
932 .into_iter()
933 .enumerate()
934 .map(|(index, text)| {
935 let piece_index = u32::try_from(index)
936 .map_err(|_| Error::internal("audio transcript piece index exceeds u32"))?;
937 Ok(PieceSpec {
938 id: audio_piece_id(recording.id, piece_index),
939 index: piece_index,
940 count: piece_count,
941 text,
942 })
943 })
944 .collect()
945}
946
947fn ingress_source_id(record: &SessionRecord) -> Option<&str> {
948 record
949 .state
950 .pointer("/ingressSource/idempotencyId")
951 .and_then(Value::as_str)
952}
953
954fn parse_audio_piece_id(value: &str) -> Option<(Uuid, u32)> {
955 let mut components = value.split(':');
956 if components.next()? != "audio" {
957 return None;
958 }
959 let recording_id = Uuid::parse_str(components.next()?).ok()?;
960 let piece_index = components.next()?.parse::<u32>().ok()?;
961 if components.next().is_some() || audio_piece_id(recording_id, piece_index) != value {
962 return None;
963 }
964 Some((recording_id, piece_index))
965}
966
967fn audio_piece_id(recording_id: Uuid, piece_index: u32) -> String {
968 format!("audio:{recording_id}:{piece_index}")
969}
970
971fn audio_piece_metadata(recording: &RecordingStatus, spec: &PieceSpec) -> Value {
972 json!({
973 "kind":"audio-transcript",
974 "recordingId":recording.id.to_string(),
975 "sha256":recording.sha256,
976 "originalFilename":recording.original_filename,
977 "extension":file_name_extension(&recording.original_filename),
978 "mimeType":"audio/wav",
979 "sizeBytes":recording.size_bytes,
980 "sourceCreatedAt":recording.recorded_at.to_rfc3339(),
981 "pieceIndex":spec.index,
982 "pieceCount":spec.count,
983 "speakerConfirmationState":recording.correction_packet.as_ref().map(|packet| match packet.confirmation_state {
984 ConfirmationState::Unconfirmed => "unconfirmed",
985 ConfirmationState::AutomaticallyTrained => "automatically_trained",
986 ConfirmationState::Confirmed => "confirmed",
987 }),
988 })
989}
990
991fn format_ingress_piece(recording: &RecordingStatus, spec: &PieceSpec) -> Result<String, Error> {
992 let speaker_mapping = confirmed_speaker_mapping(recording)?;
993 Ok(format!(
994 "Vnote final transcript piece\n\nRecording began: {}\nRecording SHA-256: {}\nOriginal filename: {}\nExtension: {}\nMIME type: audio/wav\nSize: {} bytes\nTranscript piece: {} of {}{}\n\n{}",
995 recording.recorded_at.to_rfc3339(),
996 recording.sha256,
997 recording.original_filename,
998 file_name_extension(&recording.original_filename),
999 recording.size_bytes,
1000 spec.index + 1,
1001 spec.count,
1002 speaker_mapping,
1003 spec.text,
1004 ))
1005}
1006
1007fn confirmed_speaker_mapping(recording: &RecordingStatus) -> Result<String, Error> {
1008 let Some(packet) = recording.correction_packet.as_ref() else {
1009 return Ok(String::new());
1010 };
1011 if packet.confirmation_state != ConfirmationState::Confirmed {
1012 return Err(Error::conflict(
1013 "audio speaker labels require exact human confirmation before ingress",
1014 ));
1015 }
1016 let mut lines = Vec::new();
1017 for chunk in &packet.chunks {
1018 for observation in &chunk.observations {
1019 let confirmed = observation.confirmed_full_name.as_deref().ok_or_else(|| {
1020 Error::internal("confirmed audio packet omitted an observation label")
1021 })?;
1022 let local_label =
1023 serde_json::to_string(&observation.local_label).map_err(Error::internal)?;
1024 let confirmed = serde_json::to_string(confirmed).map_err(Error::internal)?;
1025 let candidate = observation
1026 .identified_full_name
1027 .as_deref()
1028 .map(serde_json::to_string)
1029 .transpose()
1030 .map_err(Error::internal)?
1031 .unwrap_or_else(|| "null".into());
1032 lines.push(format!(
1033 "- chunk {}/{}, source {:.3}-{:.3}s: localLabel={}, confirmedFullName={}, classifierCandidate={}",
1034 chunk.chunk_index + 1,
1035 chunk.chunk_count,
1036 chunk.audio_start_ms as f64 / 1_000.0,
1037 chunk.audio_end_ms as f64 / 1_000.0,
1038 local_label,
1039 confirmed,
1040 candidate,
1041 ));
1042 }
1043 }
1044 if lines.is_empty() {
1045 return Err(Error::internal(
1046 "confirmed audio packet contains no speaker observations",
1047 ));
1048 }
1049 Ok(format!(
1050 "\n\nHuman-confirmed speaker-label data (authoritative; do not infer alternatives):\n{}",
1051 lines.join("\n")
1052 ))
1053}
1054
1055fn file_name_extension(file_name: &str) -> String {
1056 file_name
1057 .rsplit_once('.')
1058 .and_then(|(stem, extension)| {
1059 (!stem.is_empty() && !extension.is_empty()).then_some(extension)
1060 })
1061 .map(|extension| format!(".{extension}"))
1062 .unwrap_or_else(|| "(none)".into())
1063}
1064
1065fn processing_stage(status: &kcode_audio_ingress::TranscriptionStatus) -> &'static str {
1066 let plan_complete = status.steps.iter().any(|entry| {
1067 entry.step == kcode_audio_ingress::Step::PlanChunks
1068 && entry.state == kcode_audio_ingress::StepState::Completed
1069 });
1070 if !plan_complete {
1071 return "chunking";
1072 }
1073 let chunks_complete = status
1074 .steps
1075 .iter()
1076 .filter(|entry| {
1077 matches!(
1078 entry.step,
1079 kcode_audio_ingress::Step::TranscribeChunk { .. }
1080 )
1081 })
1082 .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
1083 if chunks_complete {
1084 let analyses_complete = status
1085 .steps
1086 .iter()
1087 .filter(|entry| matches!(entry.step, kcode_audio_ingress::Step::ParseChunk { .. }))
1088 .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
1089 if !analyses_complete {
1090 return "analyzing_speakers";
1091 }
1092 let training_active = status.steps.iter().any(|entry| {
1093 entry.step == kcode_audio_ingress::Step::TrainIdentities
1094 && matches!(
1095 entry.state,
1096 kcode_audio_ingress::StepState::Running
1097 | kcode_audio_ingress::StepState::Retrying
1098 )
1099 });
1100 if training_active {
1101 "training_speakers"
1102 } else {
1103 "reconciling"
1104 }
1105 } else {
1106 "transcribing"
1107 }
1108}
1109
1110fn maximum_piece_characters(effective_context_tokens: u64) -> Result<usize, Error> {
1111 let piece_tokens = effective_context_tokens / INGRESS_CONTEXT_DIVISOR;
1112 if piece_tokens == 0 {
1113 return Err(Error::invalid(
1114 "effective ingress context must contain at least four tokens",
1115 ));
1116 }
1117 let characters = piece_tokens
1118 .checked_mul(ESTIMATED_CHARACTERS_PER_TOKEN)
1119 .ok_or_else(|| Error::invalid("effective ingress context is too large"))?;
1120 usize::try_from(characters)
1121 .map_err(|_| Error::invalid("effective ingress context exceeds platform limits"))
1122}
1123
1124fn split_transcript(
1125 transcript: &str,
1126 maximum_piece_characters: usize,
1127) -> Result<Vec<String>, Error> {
1128 let mut remaining = transcript.trim();
1129 if remaining.is_empty() {
1130 return Err(Error::internal("completed audio transcript is empty"));
1131 }
1132 let mut pieces = Vec::new();
1133 while remaining.chars().count() > maximum_piece_characters {
1134 let cutoff = remaining
1135 .char_indices()
1136 .nth(maximum_piece_characters)
1137 .map(|(index, _)| index)
1138 .unwrap_or(remaining.len());
1139 let prefix = &remaining[..cutoff];
1140 let minimum = prefix
1141 .char_indices()
1142 .nth(maximum_piece_characters / 2)
1143 .map(|(index, _)| index)
1144 .unwrap_or(0);
1145 let boundary = prefix
1146 .rfind("\n\n")
1147 .filter(|index| *index >= minimum)
1148 .or_else(|| prefix.rfind('\n').filter(|index| *index >= minimum))
1149 .unwrap_or(cutoff);
1150 let piece = remaining[..boundary].trim();
1151 if piece.is_empty() {
1152 return Err(Error::internal("could not split audio transcript"));
1153 }
1154 pieces.push(piece.to_owned());
1155 remaining = remaining[boundary..].trim();
1156 }
1157 if !remaining.is_empty() {
1158 pieces.push(remaining.to_owned());
1159 }
1160 Ok(pieces)
1161}
1162
1163fn estimate_tokens(value: &str) -> u64 {
1164 (value.chars().count() as u64).div_ceil(ESTIMATED_CHARACTERS_PER_TOKEN)
1165}
1166
1167fn audio_error(error: kcode_audio_ingress::Error) -> Error {
1168 match error.kind() {
1169 AudioErrorKind::InvalidInput => Error::new(ErrorKind::InvalidInput, error.to_string()),
1170 AudioErrorKind::NotFound => Error::new(ErrorKind::NotFound, error.to_string()),
1171 AudioErrorKind::Conflict => Error::new(ErrorKind::Conflict, error.to_string()),
1172 AudioErrorKind::Internal => Error::internal(error),
1173 }
1174}
1175
1176fn history_error(error: kcode_session_history::Error) -> Error {
1177 let kind = match error.kind {
1178 kcode_session_history::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
1179 kcode_session_history::ErrorKind::NotFound => ErrorKind::NotFound,
1180 kcode_session_history::ErrorKind::Conflict => ErrorKind::Conflict,
1181 kcode_session_history::ErrorKind::Storage => ErrorKind::Internal,
1182 };
1183 Error::new(kind, error.message)
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188 use std::path::{Path, PathBuf};
1189
1190 use super::*;
1191
1192 struct TestRoot(PathBuf);
1193
1194 impl TestRoot {
1195 fn new() -> Self {
1196 let path = std::env::temp_dir().join(format!(
1197 "kcode-audio-session-ingress-test-{}",
1198 Uuid::new_v4()
1199 ));
1200 std::fs::create_dir(&path).unwrap();
1201 Self(path)
1202 }
1203
1204 fn path(&self) -> &Path {
1205 &self.0
1206 }
1207 }
1208
1209 impl Drop for TestRoot {
1210 fn drop(&mut self) {
1211 let _ = std::fs::remove_dir_all(&self.0);
1212 }
1213 }
1214
1215 fn history(root: &Path) -> SessionHistory {
1216 SessionHistory::open(kcode_session_history::Config {
1217 directory: root.join("active"),
1218 completed_list: root.join("completed.txt"),
1219 provider_cost_compatibility: None,
1220 })
1221 .unwrap()
1222 }
1223
1224 fn completed_recording(transcript: impl Into<String>) -> RecordingStatus {
1225 completed_recording_for_user("user", transcript)
1226 }
1227
1228 fn completed_recording_for_user(
1229 user_id: impl Into<String>,
1230 transcript: impl Into<String>,
1231 ) -> RecordingStatus {
1232 let now = Utc::now();
1233 RecordingStatus {
1234 id: Uuid::new_v4(),
1235 user_id: user_id.into(),
1236 sha256: "0".repeat(64),
1237 original_filename: "meeting.final.WAV".into(),
1238 size_bytes: 42,
1239 recorded_at: now,
1240 received_at: now,
1241 transcription_model: "transcription-model".into(),
1242 reconciliation_model: "reconciliation-model".into(),
1243 reconciliation_reasoning: "xhigh".into(),
1244 state: RecordingState::Complete {
1245 transcript: transcript.into(),
1246 },
1247 correction_packet: None,
1248 }
1249 }
1250
1251 fn with_speaker_packet(
1252 mut recording: RecordingStatus,
1253 state: ConfirmationState,
1254 confirmed_name: Option<&str>,
1255 ) -> RecordingStatus {
1256 recording.correction_packet = Some(CorrectionPacket {
1257 recording_id: recording.id,
1258 user_id: recording.user_id.clone(),
1259 sha256: recording.sha256.clone(),
1260 original_filename: recording.original_filename.clone(),
1261 size_bytes: recording.size_bytes,
1262 recorded_at: recording.recorded_at,
1263 clean: true,
1264 chunk_count: 1,
1265 chunks: vec![kcode_audio_ingress::CorrectionChunk {
1266 chunk_index: 0,
1267 chunk_count: 1,
1268 audio_start_ms: 0,
1269 audio_end_ms: 1_000,
1270 raw_gemini_response: "raw".into(),
1271 parsed: kcode_audio_ingress::ParsedChunk {
1272 utterances: Vec::new(),
1273 notes: Vec::new(),
1274 clip_valid: true,
1275 clip_validity_reason: None,
1276 speakers: Vec::new(),
1277 },
1278 observations: vec![kcode_audio_ingress::CorrectionObservation {
1279 local_label: "Speaker A".into(),
1280 speaker_ordinal: 0,
1281 observation_key: kcode_audio_ingress::ObservationKey {
1282 object_id: format!(
1283 "kcode-audio-ingress/recording/{}/chunk/0",
1284 recording.id
1285 ),
1286 piece_index: 0,
1287 },
1288 candidate: Some(kcode_audio_ingress::CandidateMapping {
1289 full_name: "Classifier Candidate".into(),
1290 cost: 1.0,
1291 confidence: 3.0,
1292 runner_up_full_name: None,
1293 runner_up_cost: None,
1294 background_population_cost: 4.0,
1295 }),
1296 identified_full_name: Some("Classifier Candidate".into()),
1297 confirmed_full_name: confirmed_name.map(str::to_owned),
1298 }],
1299 clean: true,
1300 }],
1301 confirmation_state: state,
1302 });
1303 recording
1304 }
1305
1306 async fn fail_piece(history: &SessionHistory, record: SessionRecord) -> SessionRecord {
1307 let started = history
1308 .start_ingress(
1309 &record.id,
1310 kcode_session_history::StartIngress {
1311 expected_version: record.version,
1312 provenance_id: format!("test:{}", record.id),
1313 },
1314 )
1315 .await
1316 .unwrap();
1317 history
1318 .fail_ingress(
1319 &record.id,
1320 kcode_session_history::IngressFailure {
1321 expected_version: started.version,
1322 stage: "test".into(),
1323 code: Some("input_too_large".into()),
1324 message: "terminal test failure".into(),
1325 rounds_used: None,
1326 context_tokens: None,
1327 context_window_tokens: None,
1328 },
1329 )
1330 .await
1331 .unwrap()
1332 }
1333
1334 async fn synchronized_failed_piece(
1335 history: &SessionHistory,
1336 recording: &RecordingStatus,
1337 user_id: &str,
1338 effective_context_tokens: u64,
1339 ) -> SessionRecord {
1340 synchronize_recordings(
1341 history,
1342 std::slice::from_ref(recording),
1343 user_id,
1344 effective_context_tokens,
1345 maximum_piece_characters(effective_context_tokens).unwrap(),
1346 )
1347 .await
1348 .unwrap();
1349 let source_id = audio_piece_id(recording.id, 0);
1350 let record = history
1351 .list()
1352 .await
1353 .unwrap()
1354 .into_iter()
1355 .find(|record| ingress_source_id(record) == Some(source_id.as_str()))
1356 .unwrap();
1357 fail_piece(history, record).await
1358 }
1359
1360 #[test]
1361 fn transcript_piece_limit_is_one_quarter_of_effective_context() {
1362 let maximum_characters = maximum_piece_characters(400).unwrap();
1363 let pieces = split_transcript(&"a".repeat(801), maximum_characters).unwrap();
1364 assert_eq!(maximum_characters, 400);
1365 assert_eq!(pieces.len(), 3);
1366 assert!(pieces.iter().all(|piece| estimate_tokens(piece) <= 100));
1367 }
1368
1369 #[test]
1370 fn transcript_splitting_prefers_a_late_paragraph_boundary() {
1371 let transcript = format!("{}\n\n{}", "a".repeat(250), "b".repeat(200));
1372 assert_eq!(
1373 split_transcript(&transcript, 400).unwrap(),
1374 vec!["a".repeat(250), "b".repeat(200)]
1375 );
1376 }
1377
1378 #[test]
1379 fn transcript_splitting_counts_unicode_scalars_not_bytes() {
1380 let transcript = "😀".repeat(5);
1381 let pieces = split_transcript(&transcript, 2).unwrap();
1382 assert_eq!(pieces.concat(), transcript);
1383 assert!(pieces.iter().all(|piece| piece.chars().count() <= 2));
1384 }
1385
1386 #[test]
1387 fn audio_piece_ids_parse_only_in_canonical_form() {
1388 let recording_id = Uuid::new_v4();
1389 let id = audio_piece_id(recording_id, 2);
1390 assert_eq!(parse_audio_piece_id(&id), Some((recording_id, 2)));
1391 assert!(parse_audio_piece_id(&format!("audio:{recording_id}:02")).is_none());
1392 assert!(parse_audio_piece_id("audio:not-a-uuid:0").is_none());
1393 }
1394
1395 #[test]
1396 fn shared_status_is_filtered_to_the_configured_user() {
1397 let mut own = completed_recording_for_user("own-user", "Own transcript");
1398 own.sha256 = "a".repeat(64);
1399 let mut foreign = completed_recording_for_user("foreign-user", "Foreign transcript");
1400 foreign.sha256 = own.sha256.clone();
1401 let recordings = [own.clone(), foreign.clone()];
1402
1403 let visible = recordings
1404 .iter()
1405 .filter(|recording| recording_belongs_to(recording, "own-user"))
1406 .collect::<Vec<_>>();
1407 assert_eq!(visible.len(), 1);
1408 assert_eq!(visible[0].id, own.id);
1409 assert_eq!(
1410 validate_submission_owner(&foreign, "own-user")
1411 .unwrap_err()
1412 .kind(),
1413 ErrorKind::Conflict
1414 );
1415 }
1416
1417 #[test]
1418 fn confirmed_label_retries_must_match_the_complete_mapping() {
1419 let recording = with_speaker_packet(
1420 completed_recording("Transcript"),
1421 ConfirmationState::Confirmed,
1422 Some("Human Choice"),
1423 );
1424 let packet = recording.correction_packet.as_ref().unwrap();
1425 let key = packet.chunks[0].observations[0].observation_key.clone();
1426 let matching = RecordingConfirmation {
1427 recording_id: recording.id,
1428 observations: vec![kcode_audio_ingress::ObservationConfirmation {
1429 observation_key: key.clone(),
1430 confirmed_full_name: " Human Choice ".into(),
1431 }],
1432 };
1433 let conflicting = RecordingConfirmation {
1434 recording_id: recording.id,
1435 observations: vec![kcode_audio_ingress::ObservationConfirmation {
1436 observation_key: key,
1437 confirmed_full_name: "Different Choice".into(),
1438 }],
1439 };
1440 assert!(confirmation_matches(packet, &matching));
1441 assert!(!confirmation_matches(packet, &conflicting));
1442 }
1443
1444 #[tokio::test]
1445 async fn classifier_aware_recordings_wait_for_human_review() {
1446 let root = TestRoot::new();
1447 let history = history(root.path());
1448 let recording = with_speaker_packet(
1449 completed_recording("Transcript"),
1450 ConfirmationState::AutomaticallyTrained,
1451 None,
1452 );
1453 synchronize_recordings(&history, &[recording], "user", 400, 400)
1454 .await
1455 .unwrap();
1456 assert!(history.list().await.unwrap().is_empty());
1457 }
1458
1459 #[tokio::test]
1460 async fn confirmed_labels_accompany_the_transcript() {
1461 let root = TestRoot::new();
1462 let history = history(root.path());
1463 let recording = with_speaker_packet(
1464 completed_recording("Transcript"),
1465 ConfirmationState::Confirmed,
1466 Some("Human Choice"),
1467 );
1468 synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 400, 400)
1469 .await
1470 .unwrap();
1471 let records = history.list().await.unwrap();
1472 assert_eq!(records.len(), 1);
1473 let state = serde_json::to_string(&records[0].state).unwrap();
1474 assert!(state.contains("Human-confirmed speaker-label data"));
1475 assert!(state.contains("Human Choice"));
1476 assert!(state.contains("Classifier Candidate"));
1477 }
1478
1479 #[tokio::test]
1480 async fn bulk_synchronization_creates_no_foreign_pieces() {
1481 let root = TestRoot::new();
1482 let history = history(root.path());
1483 let own = completed_recording_for_user("own-user", "Own transcript");
1484 let foreign = completed_recording_for_user("foreign-user", "Foreign transcript");
1485
1486 synchronize_recordings(
1487 &history,
1488 &[own.clone(), foreign.clone()],
1489 "own-user",
1490 400,
1491 400,
1492 )
1493 .await
1494 .unwrap();
1495
1496 let records = history.list().await.unwrap();
1497 assert_eq!(records.len(), 1);
1498 assert_eq!(
1499 ingress_source_id(&records[0]),
1500 Some(audio_piece_id(own.id, 0).as_str())
1501 );
1502 assert!(records.iter().all(
1503 |record| ingress_source_id(record) != Some(audio_piece_id(foreign.id, 0).as_str())
1504 ));
1505 }
1506
1507 #[tokio::test]
1508 async fn synchronization_is_idempotent() {
1509 let root = TestRoot::new();
1510 let history = history(root.path());
1511 let recording = completed_recording("a".repeat(801));
1512 for _ in 0..2 {
1513 synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 400, 400)
1514 .await
1515 .unwrap();
1516 }
1517 assert_eq!(history.list().await.unwrap().len(), 3);
1518 }
1519
1520 #[tokio::test]
1521 async fn context_changes_leave_complete_accepted_set_untouched() {
1522 let root = TestRoot::new();
1523 let history = history(root.path());
1524 let recording = completed_recording("a".repeat(801));
1525 synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 400, 400)
1526 .await
1527 .unwrap();
1528 let before = history
1529 .list()
1530 .await
1531 .unwrap()
1532 .iter()
1533 .filter_map(ingress_source_id)
1534 .map(str::to_owned)
1535 .collect::<HashSet<_>>();
1536 synchronize_recordings(&history, std::slice::from_ref(&recording), "user", 800, 800)
1537 .await
1538 .unwrap();
1539 let after = history
1540 .list()
1541 .await
1542 .unwrap()
1543 .iter()
1544 .filter_map(ingress_source_id)
1545 .map(str::to_owned)
1546 .collect::<HashSet<_>>();
1547 assert_eq!(before, after);
1548 }
1549
1550 #[tokio::test]
1551 async fn exact_current_piece_retry_accepts_equal_optional_state() {
1552 let root = TestRoot::new();
1553 let history = history(root.path());
1554 let recording = completed_recording("Transcript");
1555 let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1556
1557 let retried = retry_ingress_exact(
1558 &history,
1559 std::slice::from_ref(&recording),
1560 "user",
1561 400,
1562 400,
1563 RetryIngress {
1564 piece_id: failed.id,
1565 expected_version: failed.version,
1566 state: Some(failed.state),
1567 },
1568 )
1569 .await
1570 .unwrap();
1571 assert_eq!(retried.phase, "ingress_pending");
1572 }
1573
1574 #[tokio::test]
1575 async fn retry_without_optional_state_remains_compatible() {
1576 let root = TestRoot::new();
1577 let history = history(root.path());
1578 let recording = completed_recording("Transcript");
1579 let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1580
1581 let retried = retry_ingress_exact(
1582 &history,
1583 std::slice::from_ref(&recording),
1584 "user",
1585 400,
1586 400,
1587 RetryIngress {
1588 piece_id: failed.id,
1589 expected_version: failed.version,
1590 state: None,
1591 },
1592 )
1593 .await
1594 .unwrap();
1595 assert_eq!(retried.phase, "ingress_pending");
1596 }
1597
1598 #[tokio::test]
1599 async fn changed_retry_state_conflicts_without_mutation() {
1600 let root = TestRoot::new();
1601 let history = history(root.path());
1602 let recording = completed_recording("Transcript");
1603 let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1604 let mut changed = failed.state.clone();
1605 changed["pendingTurn"] = json!(true);
1606
1607 let error = retry_ingress_exact(
1608 &history,
1609 std::slice::from_ref(&recording),
1610 "user",
1611 400,
1612 400,
1613 RetryIngress {
1614 piece_id: failed.id.clone(),
1615 expected_version: failed.version,
1616 state: Some(changed),
1617 },
1618 )
1619 .await
1620 .unwrap_err();
1621 assert_eq!(error.kind(), ErrorKind::Conflict);
1622 assert_eq!(
1623 history.get(&failed.id).await.unwrap().version,
1624 failed.version
1625 );
1626 }
1627
1628 #[tokio::test]
1629 async fn unrelated_session_history_id_cannot_retry() {
1630 let root = TestRoot::new();
1631 let history = history(root.path());
1632 let recording = completed_recording("Transcript");
1633 let unrelated = history
1634 .start(kcode_session_history::StartSession {
1635 idempotency_id: "unrelated".into(),
1636 started_at: recording.recorded_at.to_rfc3339(),
1637 session_type: "conversation".into(),
1638 duration_minutes: None,
1639 custom_prompt: None,
1640 })
1641 .await
1642 .unwrap()
1643 .value;
1644
1645 let error = retry_ingress_exact(
1646 &history,
1647 std::slice::from_ref(&recording),
1648 "user",
1649 400,
1650 400,
1651 RetryIngress {
1652 piece_id: unrelated.id.clone(),
1653 expected_version: unrelated.version,
1654 state: None,
1655 },
1656 )
1657 .await
1658 .unwrap_err();
1659 assert_eq!(error.kind(), ErrorKind::NotFound);
1660 assert_eq!(
1661 history.get(&unrelated.id).await.unwrap().version,
1662 unrelated.version
1663 );
1664 }
1665
1666 #[tokio::test]
1667 async fn malformed_audio_looking_id_cannot_retry() {
1668 let root = TestRoot::new();
1669 let history = history(root.path());
1670 let recording = completed_recording("Transcript");
1671 let malformed = history
1672 .enqueue_ingress(NewIngressSession {
1673 idempotency_id: "audio:not-a-uuid:0".into(),
1674 started_at: recording.recorded_at.to_rfc3339(),
1675 source_session_type: "audio".into(),
1676 kind: SessionKind::AudioIngress,
1677 effective_context_tokens: 400,
1678 text: "Malformed source".into(),
1679 metadata: json!({}),
1680 })
1681 .await
1682 .unwrap()
1683 .value;
1684
1685 let error = retry_ingress_exact(
1686 &history,
1687 std::slice::from_ref(&recording),
1688 "user",
1689 400,
1690 400,
1691 RetryIngress {
1692 piece_id: malformed.id.clone(),
1693 expected_version: malformed.version,
1694 state: None,
1695 },
1696 )
1697 .await
1698 .unwrap_err();
1699 assert_eq!(error.kind(), ErrorKind::NotFound);
1700 assert_eq!(
1701 history.get(&malformed.id).await.unwrap().version,
1702 malformed.version
1703 );
1704 }
1705
1706 #[tokio::test]
1707 async fn foreign_audio_piece_cannot_retry_or_mutate() {
1708 let root = TestRoot::new();
1709 let history = history(root.path());
1710 let own = completed_recording_for_user("own-user", "Own transcript");
1711 let foreign = completed_recording_for_user("foreign-user", "Foreign transcript");
1712 let failed = synchronized_failed_piece(&history, &foreign, "foreign-user", 400).await;
1713
1714 let error = retry_ingress_exact(
1715 &history,
1716 &[own, foreign],
1717 "own-user",
1718 400,
1719 400,
1720 RetryIngress {
1721 piece_id: failed.id.clone(),
1722 expected_version: failed.version,
1723 state: None,
1724 },
1725 )
1726 .await
1727 .unwrap_err();
1728 assert_eq!(error.kind(), ErrorKind::NotFound);
1729 let unchanged = history.get(&failed.id).await.unwrap();
1730 assert_eq!(unchanged.phase, "ingress_failed");
1731 assert_eq!(unchanged.version, failed.version);
1732 }
1733
1734 #[tokio::test]
1735 async fn retry_rejects_context_and_transcript_drift() {
1736 let root = TestRoot::new();
1737 let history = history(root.path());
1738 let recording = completed_recording("a".repeat(801));
1739 let failed = synchronized_failed_piece(&history, &recording, "user", 400).await;
1740
1741 let context_error = retry_ingress_exact(
1742 &history,
1743 std::slice::from_ref(&recording),
1744 "user",
1745 800,
1746 800,
1747 RetryIngress {
1748 piece_id: failed.id.clone(),
1749 expected_version: failed.version,
1750 state: None,
1751 },
1752 )
1753 .await
1754 .unwrap_err();
1755 assert_eq!(context_error.kind(), ErrorKind::Conflict);
1756
1757 let mut changed = recording.clone();
1758 changed.state = RecordingState::Complete {
1759 transcript: format!("b{}", "a".repeat(800)),
1760 };
1761 let transcript_error = retry_ingress_exact(
1762 &history,
1763 std::slice::from_ref(&changed),
1764 "user",
1765 400,
1766 400,
1767 RetryIngress {
1768 piece_id: failed.id.clone(),
1769 expected_version: failed.version,
1770 state: None,
1771 },
1772 )
1773 .await
1774 .unwrap_err();
1775 assert_eq!(transcript_error.kind(), ErrorKind::Conflict);
1776 assert_eq!(
1777 history.get(&failed.id).await.unwrap().version,
1778 failed.version
1779 );
1780 }
1781}