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#[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}
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(¤t).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, ¤t)?;
602
603 if input
604 .state
605 .as_ref()
606 .is_some_and(|state| state != ¤t.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, ¤t)?;
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}