use crate::Provider;
use crate::generated::{session_document, session_persistence_version_authority};
use crate::lifecycle::run_primitive::{TurnMetadataOverride, TurnRequestContext};
use crate::lifecycle::{CoreBoundaryStageError, RunId};
use crate::peer_meta::PeerMeta;
use crate::realtime_transcript::{
RealtimeTranscriptApplyOutcome, RealtimeTranscriptEvent, RealtimeUserContentIdentity,
SESSION_REALTIME_TRANSCRIPT_STATE_KEY,
};
use crate::realtime_transcript_revision::{self, SessionRealtimeTranscriptState};
use crate::realtime_transcript_sidecar::{
PreparedRealtimeTranscriptRebase, RealtimeTranscriptSidecarError,
RealtimeTranscriptSnapshotReasonV1, SessionRealtimeTranscriptProjection,
};
use crate::service::MobToolAuthorityContext;
use crate::session_durable_config_authority;
use crate::time_compat::SystemTime;
#[cfg(target_arch = "wasm32")]
use crate::tokio;
use crate::tool_scope::ToolFilter;
use crate::types::{
AssistantBlock, BlockAssistantMessage, ContentBlock, ContentInput, Message, SessionId,
StopReason, ToolDef, ToolName, ToolProvenance, ToolResult, Usage, UserMessage,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct SessionLineageId(String);
impl SessionLineageId {
pub fn new(value: impl Into<String>) -> Result<Self, InvalidSessionLineageId> {
let value = value.into();
if value.trim().is_empty() {
return Err(InvalidSessionLineageId);
}
Ok(Self(value))
}
#[must_use]
pub fn for_session(session_id: &SessionId) -> Self {
Self(format!("session:{session_id}"))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for SessionLineageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<'de> Deserialize<'de> for SessionLineageId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidSessionLineageId;
impl std::fmt::Display for InvalidSessionLineageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("session lineage id must not be empty")
}
}
impl std::error::Error for InvalidSessionLineageId {}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct SessionGeneration(u64);
impl SessionGeneration {
pub const INITIAL: Self = Self(0);
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
mod digest_accumulator;
mod head_metadata;
mod import_0810;
mod transcript_history;
pub(crate) use digest_accumulator::TranscriptMessages;
pub use head_metadata::{
SessionHeadMetadataCell, SessionHeadMetadataCellIdentity, SessionHeadMetadataCellMutation,
SessionHeadMetadataDigest, SessionHeadMetadataIdentity, SessionHeadMetadataProjection,
SessionHeadMetadataValueDigest,
};
pub(crate) use import_0810::is_released_checkpoint_metadata_key;
pub use import_0810::{
ImportedReleased0810Session, Released0810ImportError, Released0810ImportEvidence,
Released0810ImportReceipt, import_released_0810_session,
released_0810_transcript_serialized_rows_digest,
};
#[cfg(test)]
pub(crate) use transcript_history::graph::TRANSCRIPT_DIGEST_FORMAT_RELEASED_0810;
pub(crate) use transcript_history::graph::import_released_0810_history;
pub(crate) use transcript_history::validate::validate_transcript_history_state;
use transcript_history::validate::{
assistant_tool_use_ids, message_role_name, validate_transcript_tool_result_shape,
};
pub use transcript_history::{
ProvenReleased0810RewriteRemap, TRANSCRIPT_HISTORY_FORMAT_CURRENT, TranscriptEndpointWitness,
TranscriptGraphPrefixAccumulator, TranscriptHistoryState, TranscriptParentAdvance,
TranscriptRevisionBody, TranscriptRevisionEdge, TranscriptRewriteAuditReceiptBatch,
TranscriptRewriteCommit, TranscriptRewriteParentTransition, TranscriptRewritePatch,
TranscriptRewritePrefixAccumulator, TranscriptRewriteRecord, ValidatedTranscriptHistory,
ValidatedTranscriptRewriteSuffix, extend_transcript_rewrite_prefix_accumulator,
remap_proven_released_0810_rewrite_record, transcript_history_full_body_materializations,
transcript_rewrite_prefix_digest,
};
pub use crate::generated::session_persistence_version_authority::SESSION_VERSION;
pub use crate::generated::session_persistence_version_authority::SESSION_METADATA_SCHEMA_VERSION;
pub fn session_version() -> u32 {
session_persistence_version_authority::session_envelope_version()
}
pub fn session_metadata_schema_version() -> u32 {
session_persistence_version_authority::session_metadata_schema_version()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TranscriptReplacement {
Message { message: Message },
UserContentBlock {
block_index: usize,
block: ContentBlock,
},
AssistantBlock {
block_index: usize,
block: AssistantBlock,
},
ToolResultContentBlock {
result_index: usize,
block_index: usize,
block: ContentBlock,
},
}
pub const SESSION_TRANSCRIPT_HISTORY_STATE_KEY: &str = "session_transcript_history_state_v1";
pub const SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY: &str =
"session_transcript_rewrite_prefix_authority_v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum TranscriptRewriteSelection {
MessageRange { start: usize, end: usize },
EditMessageRange { range: TranscriptEditRewriteRange },
CompactionMessageRange { range: CompactionRewriteRange },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TranscriptEditRewriteRange {
start: usize,
end: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CompactionRewriteRange {
start: usize,
end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscriptRewriteSemantic {
Edit,
Compaction,
}
impl TranscriptRewriteSelection {
pub fn bounds(&self) -> (usize, usize) {
match self {
Self::MessageRange { start, end } => (*start, *end),
Self::EditMessageRange { range } => (range.start, range.end),
Self::CompactionMessageRange { range } => (range.start, range.end),
}
}
pub fn semantic(&self) -> TranscriptRewriteSemantic {
match self {
Self::MessageRange { .. } | Self::EditMessageRange { .. } => {
TranscriptRewriteSemantic::Edit
}
Self::CompactionMessageRange { .. } => TranscriptRewriteSemantic::Compaction,
}
}
fn into_current_edit_semantic(self) -> Self {
match self {
Self::MessageRange { start, end } => Self::EditMessageRange {
range: TranscriptEditRewriteRange { start, end },
},
current => current,
}
}
fn is_legacy_untyped(&self) -> bool {
matches!(self, Self::MessageRange { .. })
}
fn validated_compaction(
start: usize,
end: usize,
_authority: &crate::agent::compact::ValidatedCompactionRewrite,
) -> Self {
Self::CompactionMessageRange {
range: CompactionRewriteRange { start, end },
}
}
fn migrated_legacy_compaction(start: usize, end: usize) -> Self {
Self::CompactionMessageRange {
range: CompactionRewriteRange { start, end },
}
}
#[cfg(test)]
pub(crate) fn typed_compaction_for_test(start: usize, end: usize) -> Self {
Self::CompactionMessageRange {
range: CompactionRewriteRange { start, end },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct TranscriptRewriteReason {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl TranscriptRewriteReason {
pub fn new(kind: impl Into<String>) -> Self {
Self {
kind: kind.into(),
note: None,
}
}
}
impl std::fmt::Display for TranscriptRewriteReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.note {
Some(note) => write!(f, "{}: {note}", self.kind),
None => f.write_str(&self.kind),
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum TranscriptEditError {
#[error("message index {message_index} out of bounds for {message_count} messages")]
MessageIndexOutOfBounds {
message_index: usize,
message_count: usize,
},
#[error("{block_kind} index {block_index} out of bounds for {block_count} blocks")]
BlockIndexOutOfBounds {
block_kind: &'static str,
block_index: usize,
block_count: usize,
},
#[error("replacement expected {expected} at message index {message_index}, found {actual}")]
MessageRoleMismatch {
message_index: usize,
expected: &'static str,
actual: &'static str,
},
#[error("invalid transcript rewrite range {start}..{end} for {message_count} messages")]
InvalidRewriteRange {
start: usize,
end: usize,
message_count: usize,
},
#[error("transcript rewrite does not change transcript revision {revision}")]
NoOpRewrite { revision: String },
#[error("transcript rewrite parent revision mismatch: expected {expected}, actual {actual}")]
RevisionConflict { expected: String, actual: String },
#[error("transcript history state is malformed: {0}")]
HistoryStateMalformed(String),
#[error("invalid transcript shape after rewrite: {0}")]
InvalidTranscriptShape(String),
}
fn canonicalize_digest_image_blocks(blocks: &mut [crate::types::ContentBlock]) {
for block in blocks.iter_mut() {
if let crate::types::ContentBlock::Image {
media_type,
data: crate::types::ImageData::Inline { data },
} = block
{
let blob_id = crate::blob::content_blob_id(media_type, data);
*block = crate::types::ContentBlock::Image {
media_type: media_type.clone(),
data: crate::types::ImageData::Blob { blob_id },
};
}
}
}
fn canonicalize_raw_json_for_digest(
raw: &serde_json::value::RawValue,
) -> Box<serde_json::value::RawValue> {
crate::types::canonicalize_raw_json(raw).unwrap_or_else(|_| raw.to_owned())
}
fn canonicalize_digest_structured_blocks(blocks: &mut [crate::types::ContentBlock]) {
for block in blocks {
if let crate::types::ContentBlock::Structured { data } = block {
*data = canonicalize_raw_json_for_digest(data);
}
}
}
fn canonicalize_message_images_for_digest(messages: &[Message]) -> Vec<Message> {
let mut canonical = messages.to_vec();
for message in &mut canonical {
canonicalize_message_images_for_digest_in_place(message);
}
canonical
}
fn canonicalize_message_images_for_digest_in_place(message: &mut Message) {
match message {
Message::User(user) => canonicalize_digest_image_blocks(&mut user.content),
Message::ToolResults { results, .. } => {
for result in results.iter_mut() {
canonicalize_digest_image_blocks(&mut result.content);
}
}
Message::SystemNotice(notice) => {
for block in &mut notice.blocks {
match block {
crate::types::SystemNoticeBlock::Comms { content, .. }
| crate::types::SystemNoticeBlock::ExternalEvent { content, .. } => {
canonicalize_digest_image_blocks(content);
}
_ => {}
}
}
}
_ => {}
}
}
fn canonicalize_message_raw_json_for_digest_in_place(message: &mut Message) {
match message {
Message::User(user) => canonicalize_digest_structured_blocks(&mut user.content),
Message::ToolResults { results, .. } => {
for result in results.iter_mut() {
canonicalize_digest_structured_blocks(&mut result.content);
}
}
Message::SystemNotice(notice) => {
for block in &mut notice.blocks {
match block {
crate::types::SystemNoticeBlock::Comms { content, .. }
| crate::types::SystemNoticeBlock::ExternalEvent { content, .. } => {
canonicalize_digest_structured_blocks(content);
}
_ => {}
}
}
}
Message::BlockAssistant(assistant) => {
for block in &mut assistant.blocks {
if let crate::types::AssistantBlock::ToolUse { args, .. } = block {
*args = canonicalize_raw_json_for_digest(args);
}
}
}
_ => {}
}
}
pub fn validate_current_persisted_transcript_history_slice(
bytes: &[u8],
) -> Result<u64, serde_json::Error> {
let rewrite_count =
transcript_history::graph::validate_current_transcript_history_slice(bytes)?;
u64::try_from(rewrite_count).map_err(|_| {
persisted_session_decode_error("persisted transcript-history occurrence count exceeds u64")
})
}
#[derive(Debug, Default)]
pub(crate) struct SharedTranscriptHistoryState {
inner: std::sync::Mutex<Option<std::sync::Arc<TranscriptHistoryState>>>,
}
impl Clone for SharedTranscriptHistoryState {
fn clone(&self) -> Self {
Self {
inner: std::sync::Mutex::new(self.locked().clone()),
}
}
}
impl SharedTranscriptHistoryState {
fn locked(&self) -> std::sync::MutexGuard<'_, Option<std::sync::Arc<TranscriptHistoryState>>> {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn clear(&self) {
*self.locked() = None;
}
fn set(&self, state: std::sync::Arc<TranscriptHistoryState>) {
*self.locked() = Some(state);
}
fn get(&self) -> Option<std::sync::Arc<TranscriptHistoryState>> {
self.locked().clone()
}
}
fn digest_timestamp_sentinel() -> crate::types::MessageTimestamp {
chrono::DateTime::<chrono::Utc>::UNIX_EPOCH
}
pub(crate) fn canonicalize_messages_for_digest(messages: &[Message]) -> Vec<Message> {
let mut canonical = canonicalize_message_images_for_digest(messages);
for message in &mut canonical {
canonicalize_message_raw_json_for_digest_in_place(message);
erase_message_construction_bookkeeping(message);
}
canonical
}
pub(crate) fn canonicalize_released_0810_messages_for_digest(messages: &[Message]) -> Vec<Message> {
let mut canonical = canonicalize_message_images_for_digest(messages);
for message in &mut canonical {
erase_message_construction_bookkeeping(message);
}
canonical
}
fn erase_message_construction_bookkeeping(message: &mut Message) {
match message {
Message::System(system) => {
system.created_at = digest_timestamp_sentinel();
}
Message::SystemNotice(notice) => {
notice.created_at = digest_timestamp_sentinel();
}
Message::User(user) => {
user.identity = crate::types::TranscriptMessageIdentity::default();
user.created_at = digest_timestamp_sentinel();
}
Message::BlockAssistant(assistant) => {
assistant.identity = crate::types::TranscriptMessageIdentity::default();
assistant.created_at = digest_timestamp_sentinel();
}
Message::ToolResults { created_at, .. } => {
*created_at = digest_timestamp_sentinel();
}
}
}
pub(crate) fn canonicalize_message_for_digest(message: &Message) -> Message {
let mut canonical = message.clone();
canonicalize_message_images_for_digest_in_place(&mut canonical);
canonicalize_message_raw_json_for_digest_in_place(&mut canonical);
erase_message_construction_bookkeeping(&mut canonical);
canonical
}
pub fn transcript_messages_digest(messages: &[Message]) -> Result<String, serde_json::Error> {
sha256_json_digest(&canonicalize_messages_for_digest(messages))
}
pub(crate) fn transcript_messages_digest_uncounted(
messages: &[Message],
) -> Result<String, serde_json::Error> {
let canonical = canonicalize_messages_for_digest(messages);
let bytes = serde_json::to_vec(&canonical)?;
Ok(format!("sha256:{:x}", Sha256::digest(bytes)))
}
fn sha256_json_digest<T: Serialize + ?Sized>(value: &T) -> Result<String, serde_json::Error> {
crate::digest_observability::record_content_digest_computation();
let bytes = serde_json::to_vec(value)?;
crate::digest_observability::record_content_digest_bytes(bytes.len() as u64);
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(digest.len() * 2);
const HEX: &[u8; 16] = b"0123456789abcdef";
for byte in digest {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
Ok(format!("sha256:{out}"))
}
#[derive(Debug, Default, Clone)]
pub(crate) struct SessionHistoryCaches {
shared_state: SharedTranscriptHistoryState,
head_canonical_metadata: head_metadata::SessionHeadMetadataTracker,
}
fn head_canonical_metadata_cell_carries_key(key: &str) -> bool {
!import_0810::is_released_checkpoint_metadata_key(key)
&& !matches!(
key,
SESSION_TRANSCRIPT_HISTORY_STATE_KEY
| SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY
| SESSION_REALTIME_TRANSCRIPT_STATE_KEY
)
}
#[cfg(test)]
std::thread_local! {
static SESSION_HEAD_METADATA_CANONICALIZATION_COUNT: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn reset_session_head_metadata_canonicalization_count() {
SESSION_HEAD_METADATA_CANONICALIZATION_COUNT.set(0);
}
#[cfg(test)]
pub(crate) fn session_head_metadata_canonicalization_count() -> u64 {
SESSION_HEAD_METADATA_CANONICALIZATION_COUNT.get()
}
#[cfg(test)]
pub(crate) fn record_session_head_metadata_canonicalization() {
SESSION_HEAD_METADATA_CANONICALIZATION_COUNT.set(
SESSION_HEAD_METADATA_CANONICALIZATION_COUNT
.get()
.saturating_add(1),
);
}
#[derive(Debug, Clone)]
pub struct Session {
version: u32,
id: SessionId,
pub(crate) messages: TranscriptMessages,
created_at: SystemTime,
updated_at: SystemTime,
metadata: serde_json::Map<String, serde_json::Value>,
realtime_transcript: Box<SessionRealtimeTranscriptProjection>,
history_caches: Box<SessionHistoryCaches>,
transcript_history_metadata_validation: TranscriptHistoryMetadataValidation,
usage: Usage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TranscriptHistoryMetadataValidation {
Validated,
RequiresValidation,
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
struct SessionSerde {
version: u32,
id: SessionId,
messages: Vec<Message>,
created_at: SystemTime,
updated_at: SystemTime,
#[serde(default)]
metadata: serde_json::Map<String, serde_json::Value>,
#[serde(default)]
usage: Usage,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
struct SessionSerdeRef<'a> {
version: u32,
id: &'a SessionId,
messages: &'a [Message],
created_at: &'a SystemTime,
updated_at: &'a SystemTime,
metadata: &'a serde_json::Map<String, serde_json::Value>,
usage: &'a Usage,
}
struct SessionWholeBlobMetadataRef<'a> {
base: &'a serde_json::Map<String, serde_json::Value>,
history: Option<&'a TranscriptHistoryState>,
realtime: Option<&'a SessionRealtimeTranscriptState>,
}
impl Serialize for SessionWholeBlobMetadataRef<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
for (key, value) in self.base {
if key == SESSION_REALTIME_TRANSCRIPT_STATE_KEY
|| (self.history.is_some()
&& (key == SESSION_TRANSCRIPT_HISTORY_STATE_KEY
|| key == SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY))
{
continue;
}
map.serialize_entry(key, value)?;
}
if let Some(history) = self.history {
map.serialize_entry(SESSION_TRANSCRIPT_HISTORY_STATE_KEY, history)?;
map.serialize_entry(
SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY,
history.rewrite_prefix(),
)?;
}
if let Some(realtime) = self.realtime {
map.serialize_entry(SESSION_REALTIME_TRANSCRIPT_STATE_KEY, realtime)?;
}
map.end()
}
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
struct SessionWholeBlobSerdeRef<'a> {
version: u32,
id: &'a SessionId,
messages: &'a [Message],
created_at: &'a SystemTime,
updated_at: &'a SystemTime,
metadata: SessionWholeBlobMetadataRef<'a>,
usage: &'a Usage,
}
fn persisted_envelope_ref<'a>(
session: &'a Session,
metadata_override: Option<&'a serde_json::Map<String, serde_json::Value>>,
) -> SessionSerdeRef<'a> {
SessionSerdeRef {
version: session.version,
id: &session.id,
messages: session.messages(),
created_at: &session.created_at,
updated_at: &session.updated_at,
metadata: metadata_override.unwrap_or(&session.metadata),
usage: &session.usage,
}
}
impl Serialize for Session {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let _digest_site = crate::digest_observability::enter_digest_site(
crate::digest_observability::DIGEST_SITE_ENCODE,
);
if import_0810::contains_released_checkpoint_metadata(&self.metadata) {
return Err(<S::Error as serde::ser::Error>::custom(
"released checkpoint metadata cannot be serialized by the current Session domain",
));
}
if self.transcript_history_metadata_validation
== TranscriptHistoryMetadataValidation::RequiresValidation
&& (self
.metadata
.contains_key(SESSION_TRANSCRIPT_HISTORY_STATE_KEY)
|| self.history_caches.shared_state.get().is_some())
{
return Err(<S::Error as serde::ser::Error>::custom(
"transcript-history graph lacks verified materialization or construction authority",
));
}
let history = self.history_caches.shared_state.get();
let serde_repr = SessionWholeBlobSerdeRef {
version: self.version,
id: &self.id,
messages: self.messages(),
created_at: &self.created_at,
updated_at: &self.updated_at,
metadata: SessionWholeBlobMetadataRef {
base: &self.metadata,
history: history.as_deref(),
realtime: self.realtime_transcript.whole_blob_projection(),
},
usage: &self.usage,
};
serde_repr.serialize(serializer)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TranscriptHistoryWireKind {
Released0810,
Current,
}
fn transcript_history_wire_kind(
metadata: &serde_json::Map<String, serde_json::Value>,
) -> Result<Option<TranscriptHistoryWireKind>, String> {
let Some(value) = metadata.get(SESSION_TRANSCRIPT_HISTORY_STATE_KEY) else {
return Ok(None);
};
let object = value
.as_object()
.ok_or_else(|| "transcript-history graph must be an object".to_string())?;
match object.get("format") {
None => Ok(Some(TranscriptHistoryWireKind::Released0810)),
Some(serde_json::Value::String(format)) if format == TRANSCRIPT_HISTORY_FORMAT_CURRENT => {
Ok(Some(TranscriptHistoryWireKind::Current))
}
Some(serde_json::Value::String(format)) => {
Err(format!("unsupported transcript graph format {format}"))
}
Some(_) => Err("transcript graph format must be a string".to_string()),
}
}
fn compact_transcript_history_metadata_for_snapshot(
metadata: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<Option<std::sync::Arc<TranscriptHistoryState>>, String> {
let Some(value) = metadata.remove(SESSION_TRANSCRIPT_HISTORY_STATE_KEY) else {
return Ok(None);
};
let state: TranscriptHistoryState =
serde_json::from_value(value).map_err(|error| error.to_string())?;
metadata.remove(SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY);
Ok(Some(std::sync::Arc::new(state)))
}
impl ValidatedTranscriptHistory {
#[doc(hidden)]
pub fn from_store_replayed_compact_graph(
anchor_revision: String,
anchor_messages: Vec<Message>,
anchor_row_prefix: crate::session_store::SessionMessageRowPrefixAccumulator,
edges: Vec<TranscriptRevisionEdge>,
expected_rewrite_prefix: &TranscriptRewritePrefixAccumulator,
expected_graph_prefix: &TranscriptGraphPrefixAccumulator,
) -> Result<Self, TranscriptEditError> {
let state = TranscriptHistoryState::from_store_replayed_compact_graph(
anchor_revision,
anchor_messages,
anchor_row_prefix,
edges,
expected_rewrite_prefix,
expected_graph_prefix,
)?;
Ok(Self::adopt_session_validated(std::sync::Arc::new(state)))
}
pub fn from_rewrite_records_with_proved<I>(
records: I,
proved: Option<&ValidatedTranscriptHistory>,
) -> Result<Option<Self>, TranscriptEditError>
where
I: IntoIterator<Item = TranscriptRewriteRecord>,
{
Ok(
TranscriptHistoryState::from_rewrite_records_with_proved(records, proved)?.map(
|state| {
ValidatedTranscriptHistory::adopt_session_validated(std::sync::Arc::new(state))
},
),
)
}
}
impl<'de> Deserialize<'de> for Session {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let _digest_site = crate::digest_observability::enter_digest_site(
crate::digest_observability::DIGEST_SITE_DECODE,
);
let serde_repr = SessionSerde::deserialize(deserializer)?;
let version = session_persistence_version_authority::restore_session_envelope_version(
serde_repr.version,
)
.map_err(<D::Error as serde::de::Error>::custom)?;
let mut metadata = serde_repr.metadata;
if import_0810::contains_released_checkpoint_metadata(&metadata) {
return Err(<D::Error as serde::de::Error>::custom(
"embedded released checkpoint metadata requires the explicit one-time 0.8.10 importer",
));
}
let realtime_transcript = match metadata.remove(SESSION_REALTIME_TRANSCRIPT_STATE_KEY) {
Some(value) => {
let state = serde_json::from_value(value)
.map_err(<D::Error as serde::de::Error>::custom)?;
SessionRealtimeTranscriptProjection::from_inline_snapshot(&serde_repr.id, state)
.map_err(<D::Error as serde::de::Error>::custom)?
}
None => SessionRealtimeTranscriptProjection::empty(&serde_repr.id),
};
let history_wire_kind = transcript_history_wire_kind(&metadata)
.map_err(<D::Error as serde::de::Error>::custom)?;
if matches!(
history_wire_kind,
Some(TranscriptHistoryWireKind::Released0810)
) {
return Err(<D::Error as serde::de::Error>::custom(
"released 0.8.10 transcript history requires the explicit one-time importer",
));
}
let history_caches = Box::<SessionHistoryCaches>::default();
let mut session = Session {
version,
id: serde_repr.id,
messages: TranscriptMessages::from_vec(serde_repr.messages),
created_at: serde_repr.created_at,
updated_at: serde_repr.updated_at,
metadata,
realtime_transcript: Box::new(realtime_transcript),
history_caches,
transcript_history_metadata_validation: if history_wire_kind.is_some() {
TranscriptHistoryMetadataValidation::RequiresValidation
} else {
TranscriptHistoryMetadataValidation::Validated
},
usage: serde_repr.usage,
};
if let Some(TranscriptHistoryWireKind::Current) = history_wire_kind {
let state = compact_transcript_history_metadata_for_snapshot(&mut session.metadata)
.map_err(<D::Error as serde::de::Error>::custom)?
.ok_or_else(|| {
<D::Error as serde::de::Error>::custom(
"transcript-history graph disappeared during ingress",
)
})?;
let exact_live_prefix = state
.derive_live_row_lineage_after_final_semantic_replay(session.messages())
.map_err(<D::Error as serde::de::Error>::custom)?
.ok_or_else(|| {
<D::Error as serde::de::Error>::custom(
"live transcript does not preserve the graph-proved audited endpoint",
)
})?;
let endpoint_prefix = state
.final_endpoint_witness()
.ok_or_else(|| {
<D::Error as serde::de::Error>::custom(
"compact transcript graph has no final endpoint witness",
)
})?
.row_prefix()
.clone();
if !session.install_exact_message_row_lineage(endpoint_prefix, exact_live_prefix) {
return Err(<D::Error as serde::de::Error>::custom(
"failed to install exact live message-row authority",
));
}
session.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::Validated;
session
.history_caches
.shared_state
.set(std::sync::Arc::clone(&state));
}
Ok(session)
}
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
struct SessionMetadataDocumentSerde {
version: u32,
id: SessionId,
#[serde(default)]
metadata: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct SessionMetadataDocument {
session_id: SessionId,
metadata: serde_json::Map<String, serde_json::Value>,
}
impl SessionMetadataDocument {
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
pub fn session_metadata_value(&self) -> Option<&serde_json::Value> {
self.metadata.get(SESSION_METADATA_KEY)
}
pub fn lifecycle_terminal_value(&self) -> Option<&serde_json::Value> {
self.metadata.get(SESSION_LIFECYCLE_TERMINAL_KEY)
}
pub fn try_into_view(self) -> Result<PersistedSessionMetadataView, serde_json::Error> {
PersistedSessionMetadataView::try_from_metadata_map(self.session_id, &self.metadata)
}
}
pub fn session_metadata_document_from_slice(
bytes: &[u8],
) -> Result<SessionMetadataDocument, serde_json::Error> {
let serde_repr: SessionMetadataDocumentSerde = serde_json::from_slice(bytes)?;
session_persistence_version_authority::restore_session_envelope_version(serde_repr.version)
.map_err(<serde_json::Error as serde::de::Error>::custom)?;
if import_0810::contains_released_checkpoint_metadata(&serde_repr.metadata) {
return Err(<serde_json::Error as serde::de::Error>::custom(
"released 0.8.10 proof metadata requires the explicit one-time importer",
));
}
let history_wire_kind = transcript_history_wire_kind(&serde_repr.metadata)
.map_err(<serde_json::Error as serde::de::Error>::custom)?;
if matches!(
history_wire_kind,
Some(TranscriptHistoryWireKind::Released0810)
) {
return Err(<serde_json::Error as serde::de::Error>::custom(
"released 0.8.10 transcript history requires the explicit one-time importer",
));
}
Ok(SessionMetadataDocument {
session_id: serde_repr.id,
metadata: serde_repr.metadata,
})
}
#[derive(Debug, Clone)]
pub struct SerializedSessionArtifact {
bytes: Arc<Vec<u8>>,
raw_sha256: [u8; 32],
row_sha256_token: Arc<str>,
}
#[derive(Debug)]
pub struct DecodedWholeBlobSessionDocument {
session: Session,
row_sha256_token: String,
}
impl DecodedWholeBlobSessionDocument {
#[must_use]
pub fn session(&self) -> &Session {
&self.session
}
#[must_use]
pub fn row_sha256_token(&self) -> &str {
&self.row_sha256_token
}
#[must_use]
pub fn into_session(self) -> Session {
self.session
}
}
impl SerializedSessionArtifact {
fn from_parts(bytes: Vec<u8>, raw_sha256: [u8; 32]) -> Self {
Self {
bytes: Arc::new(bytes),
raw_sha256,
row_sha256_token: Arc::from(row_sha256_token(raw_sha256)),
}
}
pub(crate) fn from_raw_bytes(bytes: Vec<u8>) -> Self {
let raw_sha256 = sha256_key(&bytes);
Self::from_parts(bytes, raw_sha256)
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.bytes.as_ref()
}
#[must_use]
pub fn bytes_arc(&self) -> Arc<Vec<u8>> {
Arc::clone(&self.bytes)
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
Arc::try_unwrap(self.bytes).unwrap_or_else(|shared| shared.as_ref().clone())
}
#[must_use]
pub const fn raw_sha256(&self) -> &[u8; 32] {
&self.raw_sha256
}
#[must_use]
pub fn row_sha256_token(&self) -> &str {
&self.row_sha256_token
}
}
struct SessionArtifactWriter {
bytes: Vec<u8>,
hasher: Sha256,
}
impl SessionArtifactWriter {
fn new() -> Self {
Self {
bytes: Vec::new(),
hasher: Sha256::new(),
}
}
fn finish(self) -> SerializedSessionArtifact {
crate::digest_observability::record_session_encode_bytes(self.bytes.len() as u64);
let digest = self.hasher.finalize();
let mut raw_sha256 = [0u8; 32];
raw_sha256.copy_from_slice(&digest);
SerializedSessionArtifact::from_parts(self.bytes, raw_sha256)
}
}
impl std::io::Write for SessionArtifactWriter {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
self.bytes.extend_from_slice(buffer);
self.hasher.update(buffer);
Ok(buffer.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn sha256_key(bytes: &[u8]) -> [u8; 32] {
let digest = Sha256::digest(bytes);
let mut key = [0u8; 32];
key.copy_from_slice(&digest);
key
}
fn row_sha256_token(raw_sha256: [u8; 32]) -> String {
use std::fmt::Write as _;
let mut token = String::with_capacity("row-sha256:".len() + 64);
token.push_str("row-sha256:");
for byte in raw_sha256 {
let _ = write!(token, "{byte:02x}");
}
token
}
fn persisted_session_decode_error(message: impl Into<String>) -> serde_json::Error {
serde_json::Error::io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
message.into(),
))
}
impl Session {
pub(crate) fn normalize_persisted_transcript_history_ingress(
&mut self,
) -> Result<(), TranscriptEditError> {
let history_wire_kind = transcript_history_wire_kind(&self.metadata)
.map_err(TranscriptEditError::HistoryStateMalformed)?;
let Some(history_wire_kind) = history_wire_kind else {
return Ok(());
};
if matches!(history_wire_kind, TranscriptHistoryWireKind::Released0810) {
return Err(TranscriptEditError::HistoryStateMalformed(
"released 0.8.10 transcript history requires the explicit one-time importer"
.to_string(),
));
}
let state = compact_transcript_history_metadata_for_snapshot(&mut self.metadata)
.map_err(TranscriptEditError::HistoryStateMalformed)?
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"transcript-history graph disappeared during ingress".to_string(),
)
})?;
let exact_live_prefix = state
.derive_live_row_lineage_after_final_semantic_replay(self.messages())
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"live transcript does not preserve the graph-proved audited endpoint"
.to_string(),
)
})?;
let endpoint_prefix = state
.final_endpoint_witness()
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"compact transcript graph has no final endpoint witness".to_string(),
)
})?
.row_prefix()
.clone();
if !self.install_exact_message_row_lineage(endpoint_prefix, exact_live_prefix) {
return Err(TranscriptEditError::HistoryStateMalformed(
"failed to install exact live message-row authority".to_string(),
));
}
self.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::Validated;
self.history_caches
.shared_state
.set(std::sync::Arc::clone(&state));
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_head_parts(
version: u32,
id: SessionId,
messages: Vec<Message>,
exact_row_prefix: Option<crate::SessionMessageRowPrefixAccumulator>,
created_at: SystemTime,
updated_at: SystemTime,
metadata: serde_json::Map<String, serde_json::Value>,
usage: Usage,
head_canonical_metadata: Option<Arc<SessionHeadMetadataProjection>>,
) -> Result<Self, String> {
let version =
session_persistence_version_authority::restore_session_envelope_version(version)
.map_err(|err| err.to_string())?;
if import_0810::contains_released_checkpoint_metadata(&metadata) {
return Err(
"embedded released checkpoint metadata requires the explicit one-time 0.8.10 importer"
.to_string(),
);
}
let transcript = TranscriptMessages::from_vec(messages);
if let Some(prefix) = exact_row_prefix
&& !transcript.install_exact_row_prefix(prefix)
{
return Err(
"exact message-row prefix count differs from materialized messages".to_string(),
);
}
let realtime_transcript = Box::new(SessionRealtimeTranscriptProjection::empty(&id));
let history_caches = Box::<SessionHistoryCaches>::default();
let mut session = Self {
version,
id,
messages: transcript,
created_at,
updated_at,
transcript_history_metadata_validation: if metadata
.contains_key(SESSION_TRANSCRIPT_HISTORY_STATE_KEY)
{
TranscriptHistoryMetadataValidation::RequiresValidation
} else {
TranscriptHistoryMetadataValidation::Validated
},
metadata,
realtime_transcript,
history_caches,
usage,
};
if let Some(projection) = head_canonical_metadata {
session
.install_head_canonical_metadata_projection(&projection)
.map_err(|error| {
format!(
"failed to install HeadCanonical metadata baseline for session {}: {error}",
session.id
)
})?;
}
Ok(session)
}
pub fn to_persisted_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
Ok(self.to_persisted_artifact()?.into_bytes())
}
pub fn to_persisted_artifact(&self) -> Result<SerializedSessionArtifact, serde_json::Error> {
let mut writer = SessionArtifactWriter::new();
serde_json::to_writer(&mut writer, self)?;
Ok(writer.finish())
}
pub fn from_persisted_bytes(serialized: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(serialized)
}
#[doc(hidden)]
pub fn decode_whole_blob_document(
serialized: &[u8],
) -> Result<DecodedWholeBlobSessionDocument, serde_json::Error> {
let session = Self::from_persisted_bytes(serialized)?;
let message_count = u64::try_from(session.messages().len()).map_err(|_| {
<serde_json::Error as serde::de::Error>::custom(
"WholeBlob transcript row count exceeds u64",
)
})?;
if session.exact_message_row_prefix_at(message_count).is_none() {
session.messages.mark_lazy_whole_blob_row_lineage();
}
Ok(DecodedWholeBlobSessionDocument {
session,
row_sha256_token: row_sha256_token(sha256_key(serialized)),
})
}
pub(crate) fn exact_message_row_prefix_at(
&self,
row_count: u64,
) -> Option<crate::SessionMessageRowPrefixAccumulator> {
self.messages.exact_row_prefix_at(row_count)
}
pub(crate) fn install_exact_message_row_prefix(
&self,
prefix: crate::SessionMessageRowPrefixAccumulator,
) -> bool {
self.messages.install_exact_row_prefix(prefix)
}
pub(crate) fn install_exact_message_row_lineage(
&self,
anchor: crate::SessionMessageRowPrefixAccumulator,
current: crate::SessionMessageRowPrefixAccumulator,
) -> bool {
self.messages.install_exact_row_lineage(anchor, current)
}
pub(crate) fn exact_message_row_lineage_extends(
&self,
anchor: &crate::SessionMessageRowPrefixAccumulator,
current_count: u64,
) -> bool {
self.messages
.exact_row_lineage_extends(anchor, current_count)
}
}
pub const SESSION_DEFERRED_TURN_STATE_KEY: &str = "session_deferred_turn_state";
pub(crate) const SESSION_PENDING_CALLBACK_BATCH_KEY: &str = "session_pending_callback_batch_v1";
pub const SESSION_BUILD_STATE_KEY: &str = "session_build_state";
pub const SESSION_TOOL_VISIBILITY_STATE_KEY: &str = "session_tool_visibility_state_v1";
pub const SESSION_LIFECYCLE_TERMINAL_KEY: &str = "session_lifecycle_terminal";
pub const VIEW_IMAGE_TOOL_NAME: &str = "view_image";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("metadata key `{key}` is reserved for session authority")]
pub struct ReservedSessionMetadataKey {
key: String,
}
impl ReservedSessionMetadataKey {
fn new(key: &str) -> Self {
Self {
key: key.to_string(),
}
}
}
fn is_session_authority_metadata_key(key: &str) -> bool {
key == SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY
|| crate::surface_metadata::ReservedMetadataKey::is_session_authority(key)
}
#[allow(clippy::panic)]
fn fail_closed_generated_restore(authority: &'static str, err: serde_json::Error) -> ! {
tracing::error!(
authority,
error = %err,
"generated authority rejected durable restore"
);
panic!("generated {authority} authority rejected durable restore: {err}");
}
#[derive(Clone)]
pub struct TransientTurnContextStateHandle {
boundary: Arc<TransientTurnContextBoundaryCoordinator>,
}
struct TransientTurnContextBoundaryCoordinator {
incarnation_id: uuid::Uuid,
lifecycle: std::sync::Mutex<TransientTurnContextBoundaryLifecycle>,
notify: tokio::sync::Notify,
}
struct TransientTurnContextBoundaryLifecycle {
actor_live: bool,
next_generation: u64,
next_request_id: u64,
window: TransientTurnContextBoundaryWindow,
}
enum TransientTurnContextBoundaryWindow {
Closed,
Open {
run_id: RunId,
generation: u64,
request: Option<RegisteredTransientTurnContextBoundaryRequest>,
},
Parked {
run_id: RunId,
generation: u64,
request_id: u64,
contexts: Vec<TurnRequestContext>,
},
Resolved {
run_id: RunId,
request_id: u64,
contexts: Vec<TurnRequestContext>,
resolution: TransientTurnContextBoundaryResolution,
},
}
struct RegisteredTransientTurnContextBoundaryRequest {
request_id: u64,
contexts: Vec<TurnRequestContext>,
}
#[derive(Clone)]
enum TransientTurnContextBoundaryResolution {
Committed,
Aborted,
}
impl Default for TransientTurnContextBoundaryCoordinator {
fn default() -> Self {
Self {
incarnation_id: uuid::Uuid::new_v4(),
lifecycle: std::sync::Mutex::new(TransientTurnContextBoundaryLifecycle {
actor_live: true,
next_generation: 0,
next_request_id: 0,
window: TransientTurnContextBoundaryWindow::Closed,
}),
notify: tokio::sync::Notify::new(),
}
}
}
impl TransientTurnContextBoundaryCoordinator {
fn lock(&self) -> std::sync::MutexGuard<'_, TransientTurnContextBoundaryLifecycle> {
self.lifecycle.lock().unwrap_or_else(|poisoned| {
tracing::warn!(
"transient turn-context boundary lock poisoned; retaining exact actor authority"
);
poisoned.into_inner()
})
}
fn abort_request(&self, request_id: u64) -> Result<(), CoreBoundaryStageError> {
let mut lifecycle = self.lock();
let parked_owner = match &lifecycle.window {
TransientTurnContextBoundaryWindow::Parked {
run_id,
request_id: current,
contexts,
..
} if *current == request_id => Some((run_id.clone(), contexts.clone())),
_ => None,
};
if let Some((run_id, contexts)) = parked_owner {
lifecycle.window = TransientTurnContextBoundaryWindow::Resolved {
run_id,
request_id,
contexts,
resolution: TransientTurnContextBoundaryResolution::Aborted,
};
drop(lifecycle);
self.notify.notify_waiters();
return Ok(());
}
match &mut lifecycle.window {
TransientTurnContextBoundaryWindow::Open { request, .. }
if request
.as_ref()
.is_some_and(|request| request.request_id == request_id) =>
{
*request = None;
}
TransientTurnContextBoundaryWindow::Resolved {
request_id: current,
..
} if *current == request_id => return Ok(()),
_ => {
return Err(CoreBoundaryStageError::stale(format!(
"transient boundary request {request_id} no longer owns its actor window"
)));
}
}
drop(lifecycle);
self.notify.notify_waiters();
Ok(())
}
fn close_run(&self, run_id: &RunId) {
let mut lifecycle = self.lock();
let owns_window = match &lifecycle.window {
TransientTurnContextBoundaryWindow::Open {
run_id: current, ..
}
| TransientTurnContextBoundaryWindow::Parked {
run_id: current, ..
}
| TransientTurnContextBoundaryWindow::Resolved {
run_id: current, ..
} => current == run_id,
TransientTurnContextBoundaryWindow::Closed => false,
};
if owns_window {
lifecycle.window = TransientTurnContextBoundaryWindow::Closed;
drop(lifecycle);
self.notify.notify_waiters();
}
}
fn revoke_actor(&self) {
let mut lifecycle = self.lock();
lifecycle.actor_live = false;
lifecycle.window = TransientTurnContextBoundaryWindow::Closed;
drop(lifecycle);
self.notify.notify_waiters();
}
}
#[must_use]
pub(crate) struct TransientTurnContextBoundaryRunGuard {
boundary: Arc<TransientTurnContextBoundaryCoordinator>,
run_id: RunId,
}
impl Drop for TransientTurnContextBoundaryRunGuard {
fn drop(&mut self) {
self.boundary.close_run(&self.run_id);
}
}
struct PendingTransientTurnContextBoundaryPreparation {
boundary: Arc<TransientTurnContextBoundaryCoordinator>,
request_id: u64,
armed: bool,
}
impl Drop for PendingTransientTurnContextBoundaryPreparation {
fn drop(&mut self) {
if self.armed {
let _ = self.boundary.abort_request(self.request_id);
}
}
}
#[must_use = "prepared transient turn context must be committed or aborted"]
pub struct PreparedTransientTurnContextBoundary {
state: TransientTurnContextStateHandle,
expected_run_id: RunId,
generation: u64,
request_id: u64,
armed: bool,
_not_sync: std::marker::PhantomData<std::cell::Cell<()>>,
}
impl std::fmt::Debug for PreparedTransientTurnContextBoundary {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PreparedTransientTurnContextBoundary")
.field("actor_incarnation", &self.state.boundary.incarnation_id)
.field("expected_run_id", &self.expected_run_id)
.field("generation", &self.generation)
.field("request_id", &self.request_id)
.finish_non_exhaustive()
}
}
impl PreparedTransientTurnContextBoundary {
#[must_use]
pub fn expected_run_id(&self) -> &RunId {
&self.expected_run_id
}
#[must_use]
pub fn boundary_generation(&self) -> u64 {
self.generation
}
pub fn into_stage_output(
self,
session_snapshot: Option<Vec<u8>>,
) -> crate::lifecycle::CoreBoundaryStageOutput {
debug_assert!(
session_snapshot.is_none(),
"transient turn context cannot carry a durable Session snapshot"
);
crate::lifecycle::CoreBoundaryStageOutput::prepared(None, Box::new(self))
}
fn resolve(
&mut self,
resolution: TransientTurnContextBoundaryResolution,
) -> Result<(), CoreBoundaryStageError> {
if !self.armed {
return Err(CoreBoundaryStageError::stale(
"prepared transient boundary authority was already resolved",
));
}
let mut lifecycle = self.state.boundary.lock();
if !lifecycle.actor_live {
self.armed = false;
return Err(CoreBoundaryStageError::stale(format!(
"actor incarnation {} was revoked",
self.state.boundary.incarnation_id
)));
}
let matches_exact = matches!(
&lifecycle.window,
TransientTurnContextBoundaryWindow::Parked {
run_id,
generation,
request_id,
..
} if run_id == &self.expected_run_id
&& *generation == self.generation
&& *request_id == self.request_id
);
if !matches_exact {
self.armed = false;
return Err(CoreBoundaryStageError::stale(
"prepared transient boundary no longer owns the exact parked generation",
));
}
let contexts = match std::mem::replace(
&mut lifecycle.window,
TransientTurnContextBoundaryWindow::Closed,
) {
TransientTurnContextBoundaryWindow::Parked { contexts, .. } => contexts,
_ => {
self.armed = false;
return Err(CoreBoundaryStageError::stale(
"prepared transient boundary lost its parked context",
));
}
};
lifecycle.window = TransientTurnContextBoundaryWindow::Resolved {
run_id: self.expected_run_id.clone(),
request_id: self.request_id,
contexts,
resolution,
};
self.armed = false;
drop(lifecycle);
self.state.boundary.notify.notify_waiters();
Ok(())
}
}
impl crate::lifecycle::core_executor::CoreBoundaryStageCommitAuthority
for PreparedTransientTurnContextBoundary
{
fn commit(&mut self) -> Result<(), CoreBoundaryStageError> {
self.resolve(TransientTurnContextBoundaryResolution::Committed)
}
fn abort(&mut self) -> Result<(), CoreBoundaryStageError> {
self.resolve(TransientTurnContextBoundaryResolution::Aborted)
}
}
impl Drop for PreparedTransientTurnContextBoundary {
fn drop(&mut self) {
if self.armed {
let _ = self.resolve(TransientTurnContextBoundaryResolution::Aborted);
}
}
}
impl Default for TransientTurnContextStateHandle {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for TransientTurnContextStateHandle {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TransientTurnContextStateHandle")
.field("actor_incarnation", &self.boundary.incarnation_id)
.finish_non_exhaustive()
}
}
impl TransientTurnContextStateHandle {
#[must_use]
pub fn new() -> Self {
Self {
boundary: Arc::new(TransientTurnContextBoundaryCoordinator::default()),
}
}
pub(crate) fn begin_boundary_run(
&self,
run_id: RunId,
) -> Result<TransientTurnContextBoundaryRunGuard, CoreBoundaryStageError> {
self.open_next_boundary(&run_id)?;
Ok(TransientTurnContextBoundaryRunGuard {
boundary: Arc::clone(&self.boundary),
run_id,
})
}
pub(crate) fn open_next_boundary(&self, run_id: &RunId) -> Result<u64, CoreBoundaryStageError> {
let mut lifecycle = self.boundary.lock();
if !lifecycle.actor_live {
return Err(CoreBoundaryStageError::stale(format!(
"actor incarnation {} was revoked",
self.boundary.incarnation_id
)));
}
match &lifecycle.window {
TransientTurnContextBoundaryWindow::Open {
run_id: current,
generation,
..
} if current == run_id => return Ok(*generation),
TransientTurnContextBoundaryWindow::Parked { .. }
| TransientTurnContextBoundaryWindow::Resolved { .. } => {
return Err(CoreBoundaryStageError::fault(
"runner attempted to open a boundary while its predecessor was unresolved",
));
}
TransientTurnContextBoundaryWindow::Open {
run_id: current, ..
} => {
return Err(CoreBoundaryStageError::stale(format!(
"run {run_id} cannot replace boundary owned by {current}"
)));
}
TransientTurnContextBoundaryWindow::Closed => {}
}
lifecycle.next_generation = lifecycle
.next_generation
.checked_add(1)
.ok_or_else(|| CoreBoundaryStageError::fault("boundary generation overflow"))?;
let generation = lifecycle.next_generation;
lifecycle.window = TransientTurnContextBoundaryWindow::Open {
run_id: run_id.clone(),
generation,
request: None,
};
drop(lifecycle);
self.boundary.notify.notify_waiters();
Ok(generation)
}
pub async fn prepare_active_turn_boundary(
&self,
expected_run_id: &RunId,
contexts: Vec<TurnRequestContext>,
) -> Result<PreparedTransientTurnContextBoundary, CoreBoundaryStageError> {
if contexts.is_empty() {
return Err(CoreBoundaryStageError::fault(
"transient boundary preparation requires at least one context value",
));
}
let request_id = {
let mut lifecycle = self.boundary.lock();
if !lifecycle.actor_live {
return Err(CoreBoundaryStageError::stale(format!(
"actor incarnation {} was revoked",
self.boundary.incarnation_id
)));
}
let (run_id, request) = match &mut lifecycle.window {
TransientTurnContextBoundaryWindow::Open {
run_id, request, ..
} => (run_id, request),
TransientTurnContextBoundaryWindow::Closed => {
return Err(CoreBoundaryStageError::unavailable(format!(
"run {expected_run_id} has no open cooperative model boundary"
)));
}
TransientTurnContextBoundaryWindow::Parked { .. }
| TransientTurnContextBoundaryWindow::Resolved { .. } => {
return Err(CoreBoundaryStageError::unavailable(format!(
"the next boundary for run {expected_run_id} was already claimed"
)));
}
};
if run_id != expected_run_id {
return Err(CoreBoundaryStageError::stale(format!(
"open boundary belongs to run {run_id}, not {expected_run_id}"
)));
}
if request.is_some() {
return Err(CoreBoundaryStageError::unavailable(format!(
"the next boundary for run {expected_run_id} already has a preparation"
)));
}
lifecycle.next_request_id = lifecycle
.next_request_id
.checked_add(1)
.ok_or_else(|| CoreBoundaryStageError::fault("boundary request id overflow"))?;
let request_id = lifecycle.next_request_id;
let TransientTurnContextBoundaryWindow::Open { request, .. } = &mut lifecycle.window
else {
return Err(CoreBoundaryStageError::fault(
"boundary window changed while registering preparation",
));
};
*request = Some(RegisteredTransientTurnContextBoundaryRequest {
request_id,
contexts,
});
request_id
};
let mut pending = PendingTransientTurnContextBoundaryPreparation {
boundary: Arc::clone(&self.boundary),
request_id,
armed: true,
};
self.boundary.notify.notify_waiters();
loop {
let notified = self.boundary.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
let poll = {
let lifecycle = self.boundary.lock();
if lifecycle.actor_live {
match &lifecycle.window {
TransientTurnContextBoundaryWindow::Parked {
run_id,
generation,
request_id: parked_request_id,
..
} if *parked_request_id == request_id => {
Ok(Some(PreparedTransientTurnContextBoundary {
state: self.clone(),
expected_run_id: run_id.clone(),
generation: *generation,
request_id,
armed: true,
_not_sync: std::marker::PhantomData,
}))
}
TransientTurnContextBoundaryWindow::Open { request, .. }
if request
.as_ref()
.is_some_and(|request| request.request_id == request_id) =>
{
Ok(None)
}
_ => Err(CoreBoundaryStageError::unavailable(format!(
"run {expected_run_id} ended before transient boundary request {request_id} parked"
))),
}
} else {
Err(CoreBoundaryStageError::stale(format!(
"actor incarnation {} was revoked while preparing boundary",
self.boundary.incarnation_id
)))
}
};
match poll {
Ok(Some(prepared)) => {
pending.armed = false;
return Ok(prepared);
}
Ok(None) => notified.as_mut().await,
Err(error) => return Err(error),
}
}
}
pub(crate) async fn take_pending_at_exact_boundary(
&self,
run_id: &RunId,
) -> Result<Vec<TurnRequestContext>, CoreBoundaryStageError> {
let request_id = {
let mut lifecycle = self.boundary.lock();
if !lifecycle.actor_live {
return Err(CoreBoundaryStageError::stale(format!(
"actor incarnation {} was revoked",
self.boundary.incarnation_id
)));
}
let (generation, request) = match &mut lifecycle.window {
TransientTurnContextBoundaryWindow::Open {
run_id: current,
generation,
request,
} if current == run_id => (*generation, request.take()),
TransientTurnContextBoundaryWindow::Open {
run_id: current, ..
} => {
return Err(CoreBoundaryStageError::stale(format!(
"runner {run_id} reached boundary owned by {current}"
)));
}
TransientTurnContextBoundaryWindow::Closed => {
return Err(CoreBoundaryStageError::unavailable(format!(
"run {run_id} reached a boundary with no open generation"
)));
}
TransientTurnContextBoundaryWindow::Parked { .. }
| TransientTurnContextBoundaryWindow::Resolved { .. } => {
return Err(CoreBoundaryStageError::fault(
"runner re-entered an unresolved transient model boundary",
));
}
};
let Some(request) = request else {
lifecycle.window = TransientTurnContextBoundaryWindow::Closed;
return Ok(Vec::new());
};
let request_id = request.request_id;
lifecycle.window = TransientTurnContextBoundaryWindow::Parked {
run_id: run_id.clone(),
generation,
request_id,
contexts: request.contexts,
};
request_id
};
self.boundary.notify.notify_waiters();
struct RunnerParkGuard {
boundary: Arc<TransientTurnContextBoundaryCoordinator>,
request_id: u64,
armed: bool,
}
impl Drop for RunnerParkGuard {
fn drop(&mut self) {
if self.armed {
let _ = self.boundary.abort_request(self.request_id);
}
}
}
let mut park_guard = RunnerParkGuard {
boundary: Arc::clone(&self.boundary),
request_id,
armed: true,
};
loop {
let notified = self.boundary.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
let poll = {
let mut lifecycle = self.boundary.lock();
if lifecycle.actor_live {
match &lifecycle.window {
TransientTurnContextBoundaryWindow::Parked {
request_id: parked_request_id,
..
} if *parked_request_id == request_id => Ok(None),
TransientTurnContextBoundaryWindow::Resolved {
run_id: resolved_run_id,
request_id: resolved_request_id,
..
} if resolved_run_id == run_id && *resolved_request_id == request_id => {
let (resolution, contexts) = match std::mem::replace(
&mut lifecycle.window,
TransientTurnContextBoundaryWindow::Closed,
) {
TransientTurnContextBoundaryWindow::Resolved {
contexts,
resolution,
..
} => (resolution, contexts),
_ => unreachable!("matched resolved transient boundary"),
};
let contexts = if matches!(
resolution,
TransientTurnContextBoundaryResolution::Committed
) {
contexts
} else {
Vec::new()
};
Ok(Some(contexts))
}
_ => Err(CoreBoundaryStageError::stale(format!(
"parked transient request {request_id} lost exact authority"
))),
}
} else {
Err(CoreBoundaryStageError::stale(format!(
"actor incarnation {} was revoked while parked",
self.boundary.incarnation_id
)))
}
};
match poll {
Ok(Some(contexts)) => {
park_guard.armed = false;
return Ok(contexts);
}
Ok(None) => notified.as_mut().await,
Err(error) => {
park_guard.armed = false;
return Err(error);
}
}
}
}
#[doc(hidden)]
pub fn revoke_boundary_actor(&self) {
self.boundary.revoke_actor();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionLifecycleTerminal {
Active,
Archived,
}
impl SessionLifecycleTerminal {
#[must_use]
pub fn is_archived(self) -> bool {
matches!(self, Self::Archived)
}
}
impl From<SessionLifecycleTerminal> for session_document::SessionDocumentLifecycle {
fn from(value: SessionLifecycleTerminal) -> Self {
match value {
SessionLifecycleTerminal::Active => Self::Active,
SessionLifecycleTerminal::Archived => Self::Archived,
}
}
}
impl From<session_document::SessionDocumentLifecycle> for SessionLifecycleTerminal {
fn from(value: session_document::SessionDocumentLifecycle) -> Self {
match value {
session_document::SessionDocumentLifecycle::Active => Self::Active,
session_document::SessionDocumentLifecycle::Archived => Self::Archived,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct SessionDeferredTurnState {
#[serde(default, skip_serializing_if = "DeferredFirstTurnPhase::is_inactive")]
pub(crate) first_turn_phase: DeferredFirstTurnPhase,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) pending_initial_prompt: Option<PendingDeferredPrompt>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) pending_tool_results: Vec<PendingToolResultsMessage>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeferredFirstTurnPhase {
#[default]
Inactive,
Pending,
Consumed,
}
impl DeferredFirstTurnPhase {
pub fn is_inactive(&self) -> bool {
matches!(self, Self::Inactive)
}
}
impl From<DeferredFirstTurnPhase> for session_document::SessionFirstTurnPhase {
fn from(value: DeferredFirstTurnPhase) -> Self {
match value {
DeferredFirstTurnPhase::Inactive => Self::Inactive,
DeferredFirstTurnPhase::Pending => Self::Pending,
DeferredFirstTurnPhase::Consumed => Self::Consumed,
}
}
}
impl From<session_document::SessionFirstTurnPhase> for DeferredFirstTurnPhase {
fn from(value: session_document::SessionFirstTurnPhase) -> Self {
match value {
session_document::SessionFirstTurnPhase::Inactive => Self::Inactive,
session_document::SessionFirstTurnPhase::Pending => Self::Pending,
session_document::SessionFirstTurnPhase::Consumed => Self::Consumed,
}
}
}
fn is_default_hook_run_overrides(value: &crate::HookRunOverrides) -> bool {
value == &crate::HookRunOverrides::default()
}
fn is_default_call_timeout_override(value: &crate::CallTimeoutOverride) -> bool {
value == &crate::CallTimeoutOverride::default()
}
fn is_tool_filter_all(value: &ToolFilter) -> bool {
matches!(value, ToolFilter::All)
}
fn is_zero(value: &u64) -> bool {
*value == 0
}
pub fn capability_base_filter_for_image_tool_results(image_tool_results: bool) -> ToolFilter {
if image_tool_results {
ToolFilter::All
} else {
ToolFilter::Deny([VIEW_IMAGE_TOOL_NAME.to_string()].into_iter().collect())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct ToolVisibilityWitness {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_seen_provenance: Option<ToolProvenance>,
}
impl ToolVisibilityWitness {
pub fn has_identity_witness(&self) -> bool {
self.last_seen_provenance.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct DeferredToolLoadAuthority {
pub name: ToolName,
pub witness: ToolVisibilityWitness,
}
impl DeferredToolLoadAuthority {
pub fn new(name: impl Into<ToolName>, witness: ToolVisibilityWitness) -> Self {
Self {
name: name.into(),
witness,
}
}
pub fn into_parts(self) -> (ToolName, ToolVisibilityWitness) {
(self.name, self.witness)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct WitnessedToolFilter {
pub filter: ToolFilter,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub witnesses: BTreeMap<ToolName, ToolVisibilityWitness>,
}
impl WitnessedToolFilter {
pub fn new(filter: ToolFilter, witnesses: BTreeMap<ToolName, ToolVisibilityWitness>) -> Self {
Self { filter, witnesses }
}
pub fn into_parts(self) -> (ToolFilter, BTreeMap<ToolName, ToolVisibilityWitness>) {
(self.filter, self.witnesses)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InheritedToolVisibilityAuthority {
filter: ToolFilter,
witnesses: BTreeMap<ToolName, ToolVisibilityWitness>,
}
impl InheritedToolVisibilityAuthority {
pub(crate) fn from_generated_composition_authority(
filter: ToolFilter,
witnesses: BTreeMap<ToolName, ToolVisibilityWitness>,
) -> Self {
Self { filter, witnesses }
}
pub fn filter(&self) -> &ToolFilter {
&self.filter
}
pub fn witnesses(&self) -> &BTreeMap<ToolName, ToolVisibilityWitness> {
&self.witnesses
}
pub(crate) fn into_initial_visibility_state(self) -> SessionToolVisibilityState {
SessionToolVisibilityState {
inherited_base_filter: self.filter,
filter_witnesses: self.witnesses,
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct SessionToolVisibilityState {
#[serde(default, skip_serializing_if = "is_tool_filter_all")]
pub capability_base_filter: ToolFilter,
#[serde(default, skip_serializing_if = "is_tool_filter_all")]
pub inherited_base_filter: ToolFilter,
#[serde(default, skip_serializing_if = "is_tool_filter_all")]
pub active_filter: ToolFilter,
#[serde(default, skip_serializing_if = "is_tool_filter_all")]
pub staged_filter: ToolFilter,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub active_requested_deferred_names: BTreeSet<ToolName>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub staged_requested_deferred_names: BTreeSet<ToolName>,
#[serde(default, skip_serializing_if = "is_zero")]
pub active_revision: u64,
#[serde(default, skip_serializing_if = "is_zero")]
pub staged_revision: u64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub requested_witnesses: BTreeMap<ToolName, ToolVisibilityWitness>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub filter_witnesses: BTreeMap<ToolName, ToolVisibilityWitness>,
}
impl SessionToolVisibilityState {
#[cfg(test)]
pub(crate) fn projected_boundary_applied(&self) -> Self {
let mut projected = self.clone();
projected.active_filter = self.staged_filter.clone();
projected.active_requested_deferred_names = self.staged_requested_deferred_names.clone();
projected.active_revision = self.staged_revision;
projected
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorizedSessionToolVisibilityState {
state: SessionToolVisibilityState,
}
impl AuthorizedSessionToolVisibilityState {
pub(crate) fn from_generated_authority(state: SessionToolVisibilityState) -> Self {
Self { state }
}
pub fn as_state(&self) -> &SessionToolVisibilityState {
&self.state
}
pub fn into_state(self) -> SessionToolVisibilityState {
self.state
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub struct SessionBuildState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<crate::OutputSchema>,
#[serde(default, skip_serializing_if = "is_default_hook_run_overrides")]
pub hooks_override: crate::HookRunOverrides,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub budget_limits: Option<crate::BudgetLimits>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recoverable_tool_defs: Vec<ToolDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub silent_comms_intents: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_inline_peer_notifications: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_context: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_instructions: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shell_env: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mob_tool_authority_context: Option<MobToolAuthorityContext>,
#[serde(default, skip_serializing_if = "is_default_call_timeout_override")]
pub call_timeout_override: crate::CallTimeoutOverride,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct PendingDeferredPrompt {
pub prompt: ContentInput,
pub accepted_at: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct PendingToolResultsMessage {
pub results: Vec<ToolResult>,
pub accepted_at: SystemTime,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DeferredToolResultsIngressError {
#[error("callback result ingress contains duplicate tool id '{0}'")]
DuplicateToolUseId(String),
#[error("callback result for tool id '{0}' conflicts with its staged payload")]
ConflictingRedelivery(String),
#[error("callback result tool id '{0}' is outside the staged pending set")]
WrongToolUseId(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub(crate) struct PendingCallbackToolBatch {
pub run_id: RunId,
pub tool_use_order: Vec<String>,
pub pending_tool_use_ids: Vec<String>,
pub completed_results: Vec<ToolResult>,
pub session_effects: Vec<crate::ops::SessionEffect>,
pub async_ops: Vec<crate::ops::AsyncOpRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "state", rename_all = "snake_case")]
enum CallbackToolBatchState {
Pending {
batch: PendingCallbackToolBatch,
},
Applied {
tool_use_order: Vec<String>,
results: Vec<ToolResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
async_ops: Vec<crate::ops::AsyncOpRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
post_tool_messages: Vec<Message>,
#[serde(default)]
post_tool_messages_applied: bool,
},
}
pub(crate) enum ResolvedPendingCallbackToolResults {
NoState,
Pending {
batch: PendingCallbackToolBatch,
ordered_results: Vec<ToolResult>,
},
AlreadyApplied {
async_ops: Vec<crate::ops::AsyncOpRef>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[doc(hidden)]
pub enum CallbackResultIngress {
NoPendingBatch,
Pending { pending_tool_use_ids: Vec<String> },
AlreadyApplied,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub(crate) enum PendingCallbackBatchError {
#[error("a pending callback batch is already staged")]
AlreadyStaged,
#[error("no pending callback batch is staged")]
Missing,
#[error("pending callback batch is malformed: {0}")]
Malformed(String),
#[error("callback results contain duplicate tool id '{0}'")]
DuplicateResult(String),
#[error("mob authority replacement cannot cross a durable callback staging boundary")]
NonDurableAuthorityEffect,
#[error("callback result ids {actual:?} do not match pending ids {expected:?}")]
ResultSetMismatch {
expected: BTreeSet<String>,
actual: BTreeSet<String>,
},
#[error("callback result redelivery conflicts with the already applied payload")]
ConflictingRedelivery,
}
fn unique_tool_results(
results: Vec<ToolResult>,
) -> Result<BTreeMap<String, ToolResult>, PendingCallbackBatchError> {
let mut by_id = BTreeMap::new();
for result in results {
let id = result.tool_use_id.clone();
if by_id.insert(id.clone(), result).is_some() {
return Err(PendingCallbackBatchError::DuplicateResult(id));
}
}
Ok(by_id)
}
fn validate_pending_callback_batch(
messages: &[Message],
batch: &PendingCallbackToolBatch,
) -> Result<(), PendingCallbackBatchError> {
let Some(assistant) = messages.last() else {
return Err(PendingCallbackBatchError::Malformed(
"staged callback batch has no assistant transcript tail".to_string(),
));
};
let assistant_order = assistant_tool_use_ids(assistant)
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>();
if assistant_order != batch.tool_use_order {
return Err(PendingCallbackBatchError::Malformed(format!(
"assistant tool ids {assistant_order:?} do not match staged order {:?}",
batch.tool_use_order
)));
}
let assistant_set = assistant_order.iter().cloned().collect::<BTreeSet<_>>();
if assistant_set.len() != assistant_order.len() {
return Err(PendingCallbackBatchError::Malformed(
"assistant tool-use batch contains duplicate ids".to_string(),
));
}
let pending_set = batch
.pending_tool_use_ids
.iter()
.cloned()
.collect::<BTreeSet<_>>();
if pending_set.len() != batch.pending_tool_use_ids.len() || pending_set.is_empty() {
return Err(PendingCallbackBatchError::Malformed(
"staged callback batch must contain at least one unique pending tool id".to_string(),
));
}
let completed = unique_tool_results(batch.completed_results.clone())?;
let completed_set = completed.keys().cloned().collect::<BTreeSet<_>>();
if !pending_set.is_disjoint(&completed_set)
|| pending_set
.union(&completed_set)
.cloned()
.collect::<BTreeSet<_>>()
!= assistant_set
{
return Err(PendingCallbackBatchError::Malformed(format!(
"pending ids {pending_set:?} plus completed ids {completed_set:?} do not partition assistant ids {assistant_set:?}"
)));
}
if batch.session_effects.iter().any(|effect| {
matches!(
effect,
crate::ops::SessionEffect::ReplaceMobToolAuthorityContext { .. }
)
}) {
return Err(PendingCallbackBatchError::NonDurableAuthorityEffect);
}
Ok(())
}
impl PartialEq for PendingToolResultsMessage {
fn eq(&self, other: &Self) -> bool {
self.accepted_at == other.accepted_at
&& serde_json::to_value(&self.results).ok() == serde_json::to_value(&other.results).ok()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConsumedDeferredTurnInputs {
pub(crate) restore_first_turn_pending: bool,
pub(crate) pending_initial_prompt: Option<PendingDeferredPrompt>,
pub(crate) pending_tool_results: Vec<PendingToolResultsMessage>,
}
impl ConsumedDeferredTurnInputs {
pub fn is_empty(&self) -> bool {
!self.restore_first_turn_pending
&& self.pending_initial_prompt.is_none()
&& self.pending_tool_results.is_empty()
}
pub fn pending_initial_prompt(&self) -> Option<&PendingDeferredPrompt> {
self.pending_initial_prompt.as_ref()
}
pub fn pending_tool_results(&self) -> &[PendingToolResultsMessage] {
&self.pending_tool_results
}
}
const SESSION_DOCUMENT_FIRST_TURN_KEY: &str = "first_turn";
fn usize_to_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
fn validate_deferred_turn_snapshot(
state: SessionDeferredTurnState,
) -> Result<SessionDeferredTurnState, session_document::SessionDocumentError> {
let mut authority = session_document::SessionDocumentMachineAuthority::new();
let key = session_document::SessionDocumentKey::new(SESSION_DOCUMENT_FIRST_TURN_KEY);
authority.recover_session_first_turn_phase(
key,
state.first_turn_phase.into(),
state.pending_initial_prompt.is_some(),
usize_to_u64(state.pending_tool_results.len()),
)?;
Ok(state)
}
impl SessionDeferredTurnState {
pub fn first_turn_phase(&self) -> DeferredFirstTurnPhase {
self.first_turn_phase
}
pub fn pending_initial_prompt(&self) -> Option<&PendingDeferredPrompt> {
self.pending_initial_prompt.as_ref()
}
pub fn pending_tool_results(&self) -> &[PendingToolResultsMessage] {
&self.pending_tool_results
}
pub fn pending_tool_results_len(&self) -> usize {
self.pending_tool_results.len()
}
pub(crate) fn pending_initial_prompt_mut_for_blob_rewrite(
&mut self,
) -> Option<&mut PendingDeferredPrompt> {
self.pending_initial_prompt.as_mut()
}
pub(crate) fn pending_tool_results_mut_for_blob_rewrite(
&mut self,
) -> &mut [PendingToolResultsMessage] {
&mut self.pending_tool_results
}
fn document_authority(
&self,
) -> (
session_document::SessionDocumentMachineAuthority,
session_document::SessionDocumentKey,
) {
let mut authority = session_document::SessionDocumentMachineAuthority::new();
let key = session_document::SessionDocumentKey::new(SESSION_DOCUMENT_FIRST_TURN_KEY);
if let Err(err) = authority.recover_session_first_turn_phase(
key.clone(),
self.first_turn_phase.into(),
self.pending_initial_prompt.is_some(),
usize_to_u64(self.pending_tool_results.len()),
) {
tracing::warn!(
error = %err,
"generated session document authority rejected first-turn recovery"
);
}
(authority, key)
}
fn mirror_first_turn_phase(
&mut self,
effects: &[session_document::SessionDocumentEffect],
) -> Option<bool> {
for effect in effects {
if let session_document::SessionDocumentEffect::SessionFirstTurnPhaseResolved {
phase,
was_pending,
} = effect
{
self.first_turn_phase = (*phase).into();
return Some(*was_pending);
}
}
None
}
pub fn mark_initial_turn_pending(&mut self) {
let (mut authority, key) = self.document_authority();
match authority.mark_session_initial_turn_pending(key) {
Ok(effects) => {
self.mirror_first_turn_phase(&effects);
}
Err(err) => tracing::warn!(
error = %err,
"generated session document authority rejected pending mark"
),
}
}
pub fn mark_initial_turn_started(&mut self) -> bool {
let (mut authority, key) = self.document_authority();
match authority.start_session_initial_turn(key) {
Ok(effects) => self.mirror_first_turn_phase(&effects).unwrap_or(false),
Err(err) => {
tracing::warn!(
error = %err,
"generated session document authority rejected first-turn start"
);
false
}
}
}
pub fn restore_initial_turn_pending(&mut self) {
let (mut authority, key) = self.document_authority();
match authority.restore_session_consumed_inputs(
key.clone(),
true,
self.pending_initial_prompt.is_some(),
usize_to_u64(self.pending_tool_results.len()),
) {
Ok(_) => {
if let Some(phase) = authority.session_first_turn_phase_for(&key) {
self.first_turn_phase = phase.into();
}
}
Err(err) => tracing::warn!(
error = %err,
"generated session document authority rejected pending restore"
),
}
}
pub fn allows_initial_turn_overrides(&self) -> bool {
let (mut authority, key) = self.document_authority();
match authority.resolve_session_first_turn_overrides_allowed(key) {
Ok(effects) => effects
.iter()
.find_map(|effect| {
match effect {
session_document::SessionDocumentEffect::SessionFirstTurnOverridesResolved {
allowed,
} => Some(*allowed),
_ => None,
}
})
.unwrap_or(false),
Err(err) => {
tracing::warn!(
error = %err,
"generated session document authority rejected override resolution"
);
false
}
}
}
pub fn stage_initial_prompt(&mut self, prompt: ContentInput, accepted_at: SystemTime) {
let prompt_has_content = prompt.has_images() || !prompt.text_content().trim().is_empty();
let (mut authority, key) = self.document_authority();
match authority.stage_session_initial_prompt(key, prompt_has_content) {
Ok(effects) => {
let decision = effects.iter().find_map(|effect| {
match effect {
session_document::SessionDocumentEffect::SessionInitialPromptStageResolved {
decision,
} => Some(*decision),
_ => None,
}
});
match decision {
Some(session_document::SessionInitialPromptStageDecision::Store) => {
self.pending_initial_prompt = Some(PendingDeferredPrompt {
prompt,
accepted_at,
});
}
Some(session_document::SessionInitialPromptStageDecision::Clear) => {
self.pending_initial_prompt = None;
}
None => tracing::warn!(
"generated session document authority returned no prompt-stage decision"
),
}
}
Err(err) => tracing::warn!(
error = %err,
"generated session document authority rejected initial prompt stage"
),
}
}
pub fn try_stage_tool_results(
&mut self,
results: Vec<ToolResult>,
accepted_at: SystemTime,
) -> Result<usize, DeferredToolResultsIngressError> {
let mut incoming_by_id = BTreeMap::new();
for result in &results {
if incoming_by_id
.insert(result.tool_use_id.clone(), result)
.is_some()
{
return Err(DeferredToolResultsIngressError::DuplicateToolUseId(
result.tool_use_id.clone(),
));
}
}
let mut staged_by_id = BTreeMap::new();
for pending in &self.pending_tool_results {
for result in &pending.results {
match staged_by_id.insert(result.tool_use_id.clone(), result) {
Some(previous) if previous != result => {
return Err(DeferredToolResultsIngressError::ConflictingRedelivery(
result.tool_use_id.clone(),
));
}
_ => {}
}
}
}
if !staged_by_id.is_empty() {
for (id, incoming) in &incoming_by_id {
match staged_by_id.get(id) {
Some(staged) if *staged == *incoming => {}
Some(_) => {
return Err(DeferredToolResultsIngressError::ConflictingRedelivery(
id.clone(),
));
}
None => {
return Err(DeferredToolResultsIngressError::WrongToolUseId(id.clone()));
}
}
}
return Ok(0);
}
let (mut authority, key) = self.document_authority();
let accepted = match authority.stage_session_tool_results(key, usize_to_u64(results.len()))
{
Ok(effects) => effects.iter().find_map(|effect| match effect {
session_document::SessionDocumentEffect::SessionToolResultsStageResolved {
accepted_count,
} => Some(*accepted_count),
_ => None,
}),
Err(err) => {
tracing::warn!(
error = %err,
"generated session document authority rejected tool-results stage"
);
return Ok(0);
}
};
let Some(accepted) = accepted else {
tracing::warn!(
"generated session document authority returned no tool-results decision"
);
return Ok(0);
};
if accepted == 0 {
return Ok(0);
}
let accepted = usize::try_from(accepted).unwrap_or(usize::MAX);
self.pending_tool_results.push(PendingToolResultsMessage {
results,
accepted_at,
});
Ok(accepted)
}
pub fn stage_tool_results(
&mut self,
results: Vec<ToolResult>,
accepted_at: SystemTime,
) -> usize {
match self.try_stage_tool_results(results, accepted_at) {
Ok(accepted) => accepted,
Err(error) => {
tracing::warn!(%error, "deferred callback-result ingress was rejected");
0
}
}
}
pub fn has_pending_tool_results(&self) -> bool {
!self.pending_tool_results.is_empty()
}
pub fn consume_for_started_turn(&mut self) -> ConsumedDeferredTurnInputs {
let (mut authority, key) = self.document_authority();
let was_pending = match authority.consume_session_deferred_inputs(key) {
Ok(effects) => self.mirror_first_turn_phase(&effects).unwrap_or(false),
Err(err) => {
tracing::warn!(
error = %err,
"generated session document authority rejected started-turn consumption"
);
return ConsumedDeferredTurnInputs::default();
}
};
ConsumedDeferredTurnInputs {
restore_first_turn_pending: was_pending,
pending_initial_prompt: self.pending_initial_prompt.take(),
pending_tool_results: std::mem::take(&mut self.pending_tool_results),
}
}
pub fn restore_consumed_turn_inputs(&mut self, consumed: ConsumedDeferredTurnInputs) {
if consumed.is_empty() {
return;
}
let (mut authority, key) = self.document_authority();
let effects = match authority.restore_session_consumed_inputs(
key,
consumed.restore_first_turn_pending,
consumed.pending_initial_prompt.is_some(),
usize_to_u64(consumed.pending_tool_results.len()),
) {
Ok(effects) => effects,
Err(err) => {
tracing::warn!(
error = %err,
"generated session document authority rejected consumed input restore"
);
return;
}
};
let Some((restore_first_turn_pending, restore_initial_prompt, restore_tool_results)) =
effects.iter().find_map(|effect| match effect {
session_document::SessionDocumentEffect::SessionConsumedInputsRestoreResolved {
restore_first_turn_pending,
restore_initial_prompt,
restore_tool_results,
} => Some((
*restore_first_turn_pending,
*restore_initial_prompt,
*restore_tool_results,
)),
_ => None,
})
else {
tracing::warn!(
"generated session document authority returned no consumed-input restore decision"
);
return;
};
if restore_first_turn_pending {
self.restore_initial_turn_pending();
}
if restore_initial_prompt && self.pending_initial_prompt.is_none() {
self.pending_initial_prompt = consumed.pending_initial_prompt;
}
if restore_tool_results {
let mut restored = consumed.pending_tool_results;
restored.extend(std::mem::take(&mut self.pending_tool_results));
self.pending_tool_results = restored;
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SystemMessageAppendError {
Conflict {
key: String,
existing_text: String,
existing_source: Option<String>,
},
}
impl std::fmt::Display for SystemMessageAppendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Conflict { key, .. } => {
write!(
f,
"System-message append conflict for idempotency key `{key}`"
)
}
}
}
}
impl std::error::Error for SystemMessageAppendError {}
impl Session {
#[doc(hidden)]
pub fn classify_callback_result_ingress(
&self,
incoming: &[ToolResult],
) -> Result<CallbackResultIngress, crate::error::AgentError> {
match self
.resolve_pending_callback_tool_results(incoming.to_vec())
.map_err(|error| {
crate::error::AgentError::ConfigError(format!(
"callback result ingress was rejected: {error}"
))
})? {
ResolvedPendingCallbackToolResults::NoState => {
Ok(CallbackResultIngress::NoPendingBatch)
}
ResolvedPendingCallbackToolResults::AlreadyApplied { .. } => {
Ok(CallbackResultIngress::AlreadyApplied)
}
ResolvedPendingCallbackToolResults::Pending { batch, .. } => {
Ok(CallbackResultIngress::Pending {
pending_tool_use_ids: batch.pending_tool_use_ids,
})
}
}
}
pub fn new() -> Self {
let now = SystemTime::now();
let id = SessionId::new();
Self {
version: session_version(),
realtime_transcript: Box::new(SessionRealtimeTranscriptProjection::empty(&id)),
id,
messages: TranscriptMessages::default(),
created_at: now,
updated_at: now,
metadata: serde_json::Map::new(),
history_caches: Box::default(),
transcript_history_metadata_validation: TranscriptHistoryMetadataValidation::Validated,
usage: Usage::default(),
}
}
pub fn with_id(id: SessionId) -> Self {
let mut session = Self::new();
session.realtime_transcript = Box::new(SessionRealtimeTranscriptProjection::empty(&id));
session.id = id;
session
}
pub fn id(&self) -> &SessionId {
&self.id
}
pub fn version(&self) -> u32 {
self.version
}
pub fn messages(&self) -> &[Message] {
&self.messages
}
pub fn transcript_content_digest(&self) -> Result<String, serde_json::Error> {
self.messages.digest()
}
pub fn transcript_prefix_digest(&self, count: usize) -> Result<String, serde_json::Error> {
if count > self.messages.len() {
return Err(<serde_json::Error as serde::ser::Error>::custom(format!(
"transcript prefix digest requested for {count} messages but the transcript has {}",
self.messages.len()
)));
}
if let Some(witness) = self.messages.prefix_digest_witness(count) {
return Ok(witness);
}
transcript_messages_digest(&self.messages[..count])
}
#[doc(hidden)]
#[must_use]
pub fn transcript_mutation_epoch(&self) -> u64 {
self.messages.mutation_epoch()
}
#[allow(dead_code)] pub(crate) fn replace_messages_internal(
&mut self,
messages: Vec<Message>,
reason: TranscriptRewriteReason,
) -> Result<Option<TranscriptRewriteCommit>, TranscriptEditError> {
if transcript_messages_digest(self.messages()).ok()
== transcript_messages_digest(&messages).ok()
{
return Ok(None);
}
let commit = self.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange {
start: 0,
end: self.messages.len(),
},
messages,
reason,
Some("meerkat-core".to_string()),
None,
)?;
Ok(Some(commit))
}
pub(crate) fn replace_messages_for_compaction_internal(
&mut self,
messages: Vec<Message>,
authority: &crate::agent::compact::ValidatedCompactionRewrite,
) -> Result<Option<TranscriptRewriteCommit>, TranscriptEditError> {
let parent_revision = self
.transcript_content_digest()
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
if !authority.authorizes_parent_digest(
&parent_revision,
self.messages.len(),
messages.len(),
) {
return Err(TranscriptEditError::InvalidTranscriptShape(
"validated compaction witness does not authorize this exact transcript rebuild"
.to_string(),
));
}
if authority.is_no_op() {
return Ok(None);
}
let summary_count = messages
.iter()
.filter(|message| {
matches!(message, Message::User(user) if user.transcript_role.is_compaction_summary())
})
.count();
if messages.len() >= self.messages.len() || summary_count != 1 {
return Err(TranscriptEditError::InvalidTranscriptShape(
"validated compaction rewrite must shrink the transcript and carry exactly one CompactionSummary"
.to_string(),
));
}
let selection =
TranscriptRewriteSelection::validated_compaction(0, self.messages.len(), authority);
let commit = self.commit_transcript_rewrite_bound(
selection,
messages,
TranscriptRewriteReason::new("compaction"),
Some("meerkat-core".to_string()),
Some(authority.parent_revision().to_string()),
Some(authority.revision()),
)?;
Ok(Some(commit))
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn stage_validated_compaction_for_test(
&mut self,
replacement: Vec<Message>,
summary_tokens: u64,
) -> Result<
(
TranscriptRewriteCommit,
crate::memory::CompactionProjectionIntent,
),
String,
> {
let messages_before = self.messages.len();
let authority = crate::agent::compact::ValidatedCompactionRewrite::for_test(
self.messages(),
&replacement,
)
.map_err(|error| error.to_string())?;
let commit = self
.replace_messages_for_compaction_internal(replacement, &authority)
.map_err(|error| error.to_string())?
.ok_or_else(|| "test compaction rewrite was a no-op".to_string())?;
let projection = crate::memory::CompactionProjectionId::from_validated_transcript_rewrite(
self.id().clone(),
&commit,
&authority,
)
.ok_or_else(|| {
"core-owned test compaction did not mint a projection identity".to_string()
})?;
let intent = crate::memory::CompactionProjectionIntent {
projection,
summary_tokens,
messages_before,
messages_after: self.messages.len(),
};
self.add_compaction_projection_intent(intent.clone())
.map_err(|error| error.to_string())?;
Ok((commit, intent))
}
pub fn replace_synthetic_notices(
&mut self,
kind: crate::types::SystemNoticeKind,
replacements: Vec<Message>,
) -> Result<(), TranscriptEditError> {
if !kind.is_synthetic_refresh_projection() {
return Err(TranscriptEditError::InvalidTranscriptShape(format!(
"system notice kind {kind:?} is durable transcript content, not a synthetic refresh projection"
)));
}
for (index, message) in replacements.iter().enumerate() {
let matches_kind = matches!(
message,
Message::SystemNotice(notice)
if notice.kind == kind && notice.is_synthetic_refresh_projection()
);
if !matches_kind {
return Err(TranscriptEditError::InvalidTranscriptShape(format!(
"replacement {index} for synthetic notice kind {kind:?} is not a system notice of that kind"
)));
}
}
let is_refresh_notice = |message: &Message| {
matches!(
message,
Message::SystemNotice(notice)
if notice.kind == kind && notice.is_synthetic_refresh_projection()
)
};
let existing_count = self
.messages
.iter()
.filter(|message| is_refresh_notice(message))
.count();
let tail_start = self.messages.len().saturating_sub(existing_count);
let existing_are_contiguous_tail =
self.messages[tail_start..].iter().all(&is_refresh_notice);
if existing_count == replacements.len()
&& existing_are_contiguous_tail
&& self.messages[tail_start..]
.iter()
.zip(replacements.iter())
.all(|(existing, replacement)| {
canonicalize_message_for_digest(existing)
== canonicalize_message_for_digest(replacement)
})
{
return Ok(());
}
let lowest_mutated_index = self
.messages
.iter()
.position(&is_refresh_notice)
.unwrap_or(self.messages.len());
let mut refreshed = self
.messages
.iter()
.filter(|message| !is_refresh_notice(message))
.cloned()
.collect::<Vec<_>>();
refreshed.extend(replacements);
let realtime_rebase = self.prepare_realtime_transcript_rebase_after_rewrite(
&refreshed,
RealtimeTranscriptSnapshotReasonV1::TranscriptRewrite,
)?;
if let Some(history) = self.validated_transcript_history_state()? {
let head_len = history
.final_endpoint_witness()
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"compact graph has no final endpoint witness".to_string(),
)
})?
.message_count();
if self.messages.len() < head_len
|| self.messages[..head_len].iter().any(&is_refresh_notice)
{
return Err(TranscriptEditError::InvalidTranscriptShape(
"synthetic notice refresh would rewrite the audited transcript prefix; route it through a typed transcript rewrite"
.to_string(),
));
}
}
let updated_at = SystemTime::now();
if lowest_mutated_index == self.messages.len() {
let appended = refreshed.split_off(lowest_mutated_index);
self.messages.extend_batch(appended);
} else {
*self.messages.begin_in_place_scan() = refreshed;
self.messages
.finish_in_place_scan(Some(lowest_mutated_index));
}
self.mark_content_mutated(updated_at);
self.realtime_transcript
.apply_prepared_rebase(realtime_rebase);
Ok(())
}
pub fn created_at(&self) -> SystemTime {
self.created_at
}
pub fn updated_at(&self) -> SystemTime {
self.updated_at
}
pub fn push(&mut self, message: Message) {
self.messages.push(message);
self.mark_content_mutated(SystemTime::now());
}
pub fn push_batch(&mut self, messages: Vec<Message>) {
if messages.is_empty() {
return;
}
self.messages.extend_batch(messages);
self.mark_content_mutated(SystemTime::now());
}
pub async fn externalize_media(
&mut self,
blob_store: &dyn crate::BlobStore,
start: usize,
) -> Result<(), crate::blob::BlobStoreError> {
let buffer = self.messages.begin_in_place_scan();
let lowest_mutated = match crate::image_content::externalize_messages_from_reporting_lowest(
blob_store, buffer, start,
)
.await
{
Ok(lowest_mutated) => lowest_mutated,
Err(error) => {
self.messages.finish_in_place_scan(Some(start));
return Err(error);
}
};
self.messages.finish_in_place_scan(lowest_mutated);
Ok(())
}
pub async fn hydrate_realtime_user_images(
&mut self,
blob_store: &dyn crate::BlobStore,
max_decoded_bytes: usize,
) -> Result<(), crate::image_content::RealtimeUserImageHydrationError> {
self.hydrate_realtime_user_images_with_usage(blob_store, max_decoded_bytes)
.await
.map(|_| ())
}
pub async fn hydrate_realtime_user_images_with_usage(
&mut self,
blob_store: &dyn crate::BlobStore,
max_decoded_bytes: usize,
) -> Result<usize, crate::image_content::RealtimeUserImageHydrationError> {
let buffer = self.messages.begin_in_place_scan();
let (decoded_total, lowest_mutated) =
match crate::image_content::hydrate_user_images_for_realtime_projection_reporting_lowest(
blob_store,
buffer,
max_decoded_bytes,
)
.await
{
Ok(outcome) => outcome,
Err(error) => {
self.messages.finish_in_place_scan(Some(0));
return Err(error);
}
};
self.messages.finish_in_place_scan(lowest_mutated);
Ok(decoded_total)
}
fn mark_content_mutated(&mut self, at: SystemTime) {
self.updated_at = at;
}
pub fn touch(&mut self) {
self.mark_content_mutated(SystemTime::now());
}
pub fn last_n(&self, n: usize) -> &[Message] {
let start = self.messages.len().saturating_sub(n);
&self.messages[start..]
}
pub fn total_tokens(&self) -> u64 {
self.usage.total_tokens()
}
pub fn total_usage(&self) -> Usage {
self.usage.clone()
}
pub fn record_usage(&mut self, turn_usage: Usage) {
self.usage.add(&turn_usage);
self.mark_content_mutated(SystemTime::now());
}
pub fn append_external_user_content(&mut self, content: ContentInput) {
self.push(Message::User(UserMessage::with_blocks(
content.into_blocks(),
)));
}
pub fn append_external_assistant_blocks(
&mut self,
blocks: Vec<AssistantBlock>,
stop_reason: StopReason,
usage: Usage,
) {
if !blocks.is_empty() {
self.push(Message::BlockAssistant(BlockAssistantMessage::new(
blocks,
stop_reason,
)));
}
if usage != Usage::default() {
self.record_usage(usage);
}
}
pub fn append_realtime_transcript_event(
&mut self,
event: RealtimeTranscriptEvent,
) -> RealtimeTranscriptApplyOutcome {
let (commit, recorded) =
self.realtime_transcript
.apply_event(event)
.unwrap_or_else(|err| {
fail_closed_generated_restore(
"realtime-transcript",
<serde_json::Error as serde::de::Error>::custom(err),
)
});
if recorded {
self.mark_content_mutated(SystemTime::now());
}
self.push_batch(commit.messages);
if commit.usage != Usage::default() {
self.record_usage(commit.usage);
}
commit.outcome
}
#[must_use]
pub fn preflight_realtime_user_content_event(
&self,
event: &RealtimeTranscriptEvent,
) -> Option<crate::RealtimeUserContentApplyOutcome> {
realtime_transcript_revision::preflight_realtime_user_content_event(
self.realtime_transcript.state(),
event,
)
.unwrap_or_else(|err| {
fail_closed_generated_restore(
"realtime-user-content-preflight",
<serde_json::Error as serde::de::Error>::custom(err),
)
})
}
#[must_use]
pub fn in_flight_realtime_assistant_response_ids(&self) -> Vec<String> {
realtime_transcript_revision::in_flight_realtime_assistant_response_ids(
self.realtime_transcript.state(),
)
}
#[must_use]
pub fn realtime_user_content_identities(&self) -> Vec<RealtimeUserContentIdentity> {
realtime_transcript_revision::realtime_user_content_identities(
self.realtime_transcript.state(),
)
}
#[must_use]
pub fn pending_realtime_user_content_blob(
&self,
) -> Option<crate::PendingRealtimeUserContentBlob> {
realtime_transcript_revision::pending_realtime_user_content_blob(
self.realtime_transcript.state(),
)
}
pub fn stage_pending_realtime_user_content_blob(
&mut self,
pending: crate::PendingRealtimeUserContentBlob,
) -> Result<
crate::generated::session_document::RealtimeUserContentBlobStageDisposition,
realtime_transcript_revision::RealtimeTranscriptShellError,
> {
match self
.realtime_transcript
.stage_pending_user_content_blob(pending)
{
Ok(disposition) => {
if disposition
== crate::generated::session_document::RealtimeUserContentBlobStageDisposition::StageNew
{
self.mark_content_mutated(SystemTime::now());
}
Ok(disposition)
}
Err(RealtimeTranscriptSidecarError::Reducer(error)) => Err(error),
Err(error) => fail_closed_generated_restore(
"realtime-user-content-stage",
<serde_json::Error as serde::de::Error>::custom(error),
),
}
}
pub fn resolve_pending_realtime_user_content_blob_recovery(
&self,
request: Option<&crate::PendingRealtimeUserContentBlob>,
pending_blob_valid: bool,
) -> Result<
crate::generated::session_document::RealtimeUserContentBlobRecoveryDisposition,
realtime_transcript_revision::RealtimeTranscriptShellError,
> {
realtime_transcript_revision::resolve_pending_realtime_user_content_blob_recovery(
self.realtime_transcript.state(),
request,
pending_blob_valid,
)
}
pub fn clear_invalid_pending_realtime_user_content_blob(
&mut self,
request: Option<&crate::PendingRealtimeUserContentBlob>,
) -> Result<(), realtime_transcript_revision::RealtimeTranscriptShellError> {
match self
.realtime_transcript
.clear_invalid_pending_user_content_blob(request)
{
Ok(()) => {
self.mark_content_mutated(SystemTime::now());
Ok(())
}
Err(RealtimeTranscriptSidecarError::Reducer(error)) => Err(error),
Err(error) => fail_closed_generated_restore(
"realtime-user-content-clear",
<serde_json::Error as serde::de::Error>::custom(error),
),
}
}
#[must_use]
pub fn realtime_user_content_tombstones(
&self,
) -> Vec<crate::realtime_transcript::RealtimeUserContentTombstone> {
realtime_transcript_revision::realtime_user_content_tombstones(
self.realtime_transcript.state(),
)
}
fn prepare_realtime_transcript_rebase_after_rewrite(
&self,
messages: &[Message],
reason: RealtimeTranscriptSnapshotReasonV1,
) -> Result<PreparedRealtimeTranscriptRebase, TranscriptEditError> {
let state =
realtime_transcript_revision::reconcile_realtime_transcript_state_after_rewrite(
self.realtime_transcript.state().clone(),
messages,
)
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?;
self.realtime_transcript
.prepare_rebase_snapshot(state, reason)
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))
}
pub fn append_system_message(&mut self, content: impl Into<String>) {
use crate::types::SystemMessage;
self.push(Message::System(SystemMessage::new(content)));
}
pub fn append_system_message_idempotent(
&mut self,
content: impl Into<String>,
source: Option<String>,
idempotency_key: Option<String>,
created_at: crate::types::MessageTimestamp,
) -> Result<crate::service::AppendSystemContextStatus, SystemMessageAppendError> {
use crate::types::{SystemMessage, SystemMessageIdentity};
let content = content.into();
if let Some(key) = idempotency_key.as_deref() {
for message in self.messages() {
let Message::System(existing) = message else {
continue;
};
let Some(identity) = existing.identity.as_ref() else {
continue;
};
if identity.idempotency_key.as_deref() != Some(key) {
continue;
}
if existing.content == content && identity.source == source {
return Ok(crate::service::AppendSystemContextStatus::Duplicate);
}
return Err(SystemMessageAppendError::Conflict {
key: key.to_string(),
existing_text: existing.content.clone(),
existing_source: identity.source.clone(),
});
}
}
let identity =
(source.is_some() || idempotency_key.is_some()).then_some(SystemMessageIdentity {
source,
idempotency_key,
});
self.push(Message::System(SystemMessage {
content,
created_at,
identity,
}));
Ok(crate::service::AppendSystemContextStatus::Applied)
}
pub fn messages_for_model_boundary(&self) -> Vec<Message> {
self.messages().to_vec()
}
pub fn last_assistant_text(&self) -> Option<String> {
self.messages.iter().rev().find_map(|m| match m {
Message::BlockAssistant(a) => {
let mut buf = String::new();
for block in &a.blocks {
match block {
crate::types::AssistantBlock::Text { text, .. }
| crate::types::AssistantBlock::Transcript { text, .. } => {
buf.push_str(text);
}
_ => {}
}
}
if buf.is_empty() { None } else { Some(buf) }
}
_ => None,
})
}
pub fn tool_call_count(&self) -> usize {
self.messages
.iter()
.filter_map(|m| match m {
Message::BlockAssistant(a) => Some(
a.blocks
.iter()
.filter(|b| matches!(b, crate::types::AssistantBlock::ToolUse { .. }))
.count(),
),
_ => None,
})
.sum()
}
pub fn metadata(&self) -> &serde_json::Map<String, serde_json::Value> {
&self.metadata
}
pub(crate) fn whole_blob_realtime_transcript_state(
&self,
) -> Option<&SessionRealtimeTranscriptState> {
self.realtime_transcript.whole_blob_projection()
}
pub(crate) fn inject_realtime_whole_blob_projection(
&self,
metadata: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<(), serde_json::Error> {
if let Some(projection) = self.whole_blob_realtime_transcript_state() {
metadata.insert(
SESSION_REALTIME_TRANSCRIPT_STATE_KEY.to_string(),
serde_json::to_value(projection)?,
);
}
Ok(())
}
pub(crate) fn realtime_component_event_prefix(
&self,
) -> Result<crate::ComponentEventPrefixAuthority, RealtimeTranscriptSidecarError> {
self.realtime_transcript.successor_prefix()
}
pub(crate) fn realtime_component_event_acknowledged_prefix(
&self,
) -> &crate::ComponentEventPrefixAuthority {
self.realtime_transcript.acknowledged_prefix()
}
#[doc(hidden)]
pub fn activate_realtime_component_sidecar(
&mut self,
) -> Result<(), RealtimeTranscriptSidecarError> {
let Some(value) = self.metadata.get(SESSION_REALTIME_TRANSCRIPT_STATE_KEY) else {
return Ok(());
};
if !self.realtime_transcript.is_pristine() {
return Err(RealtimeTranscriptSidecarError::Incoherent(
"inline realtime projection cannot replace an active component sidecar".to_string(),
));
}
let state = serde_json::from_value(value.clone())?;
let projection =
SessionRealtimeTranscriptProjection::from_inline_snapshot(&self.id, state)?;
self.metadata.remove(SESSION_REALTIME_TRANSCRIPT_STATE_KEY);
*self.realtime_transcript = projection;
Ok(())
}
#[doc(hidden)]
pub fn prepare_realtime_component_event_suffix(
&self,
) -> Result<Option<crate::PreparedComponentEventSuffix>, RealtimeTranscriptSidecarError> {
self.realtime_transcript.prepare_suffix()
}
pub(crate) fn install_verified_realtime_component_sequence(
&mut self,
sequence: &crate::VerifiedComponentEventSequence,
) -> Result<(), RealtimeTranscriptSidecarError> {
*self.realtime_transcript =
SessionRealtimeTranscriptProjection::from_verified_sequence(&self.id, sequence)?;
Ok(())
}
pub(crate) fn acknowledge_realtime_component_event_suffix(
&mut self,
prepared: &crate::PreparedComponentEventSuffix,
committed: &crate::ComponentEventPrefixAuthority,
) -> Result<(), RealtimeTranscriptSidecarError> {
self.realtime_transcript
.acknowledge_suffix(prepared, committed)
}
pub(crate) fn head_canonical_metadata_projection(
&self,
) -> Result<Arc<SessionHeadMetadataProjection>, serde_json::Error> {
self.history_caches
.head_canonical_metadata
.projection(&self.metadata)
.map_err(<serde_json::Error as serde::ser::Error>::custom)
}
pub(crate) fn install_head_canonical_metadata_projection(
&mut self,
projection: &Arc<SessionHeadMetadataProjection>,
) -> Result<(), String> {
self.history_caches
.head_canonical_metadata
.install_snapshot(projection)
}
pub(crate) fn acknowledge_head_canonical_metadata_projection(
&mut self,
projection: &Arc<SessionHeadMetadataProjection>,
) -> Result<(), String> {
self.history_caches
.head_canonical_metadata
.acknowledge(projection, &self.metadata)
}
pub(crate) fn validate_head_canonical_metadata_acknowledgement(
&self,
projection: &Arc<SessionHeadMetadataProjection>,
) -> Result<(), String> {
self.history_caches
.head_canonical_metadata
.validate_acknowledgement(projection, &self.metadata)
}
fn mark_head_canonical_metadata_key_mutated(&mut self, key: &str) {
self.history_caches
.head_canonical_metadata
.mark_key_mutated(key);
}
fn adopt_head_canonical_metadata_baseline_from(&mut self, source: &Session) {
self.history_caches.head_canonical_metadata =
source.history_caches.head_canonical_metadata.clone();
}
fn set_metadata_unchecked(&mut self, key: &str, value: serde_json::Value) {
if self.metadata.get(key) == Some(&value) {
return;
}
self.mark_head_canonical_metadata_key_mutated(key);
self.metadata.insert(key.to_string(), value);
if key == SESSION_TRANSCRIPT_HISTORY_STATE_KEY {
self.metadata
.remove(SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY);
self.history_caches.shared_state.clear();
self.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::RequiresValidation;
}
self.mark_content_mutated(SystemTime::now());
}
fn install_validated_transcript_history_state(
&mut self,
state: TranscriptHistoryState,
) -> Result<(), serde_json::Error> {
let state = std::sync::Arc::new(state);
let unchanged = self
.history_caches
.shared_state
.get()
.is_some_and(|current| {
current.graph_prefix() == state.graph_prefix()
&& current.rewrite_prefix() == state.rewrite_prefix()
&& current.head() == state.head()
});
self.metadata.remove(SESSION_TRANSCRIPT_HISTORY_STATE_KEY);
self.metadata
.remove(SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY);
if unchanged {
self.history_caches.shared_state.set(state);
self.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::Validated;
return Ok(());
}
self.history_caches.shared_state.set(state);
self.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::Validated;
Ok(())
}
#[must_use]
pub fn transcript_rewrite_prefix_authority(
&self,
) -> Option<TranscriptRewritePrefixAccumulator> {
if let Some(state) = self.history_caches.shared_state.get() {
return Some(state.rewrite_prefix().clone());
}
serde_json::from_value(
self.metadata
.get(SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY)?
.clone(),
)
.ok()
}
#[cfg(test)]
pub(crate) fn set_metadata_unchecked_for_test(&mut self, key: &str, value: serde_json::Value) {
self.set_metadata_unchecked(key, value);
}
fn fork_metadata_projection(&self) -> serde_json::Map<String, serde_json::Value> {
let mut metadata = self.metadata.clone();
metadata.retain(|key, _| !is_session_authority_metadata_key(key));
metadata
}
fn remove_metadata_unchecked(&mut self, key: &str) {
let removed = self.metadata.remove(key).is_some();
let mut changed = removed;
if key == SESSION_TRANSCRIPT_HISTORY_STATE_KEY {
changed |= self.history_caches.shared_state.get().is_some();
changed |= self
.metadata
.remove(SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY)
.is_some();
self.history_caches.shared_state.clear();
self.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::Validated;
}
if changed {
self.mark_head_canonical_metadata_key_mutated(key);
self.mark_content_mutated(SystemTime::now());
}
}
pub fn try_set_metadata(
&mut self,
key: &str,
value: serde_json::Value,
) -> Result<(), ReservedSessionMetadataKey> {
if is_session_authority_metadata_key(key) {
return Err(ReservedSessionMetadataKey::new(key));
}
self.set_metadata_unchecked(key, value);
Ok(())
}
pub fn set_metadata(&mut self, key: &str, value: serde_json::Value) {
if let Err(err) = self.try_set_metadata(key, value) {
tracing::warn!(error = %err, "rejected raw session metadata mutation");
}
}
pub fn backfill_metadata_if_absent(&mut self, key: &str, value: serde_json::Value) -> bool {
if is_session_authority_metadata_key(key) {
tracing::warn!(
metadata_key = key,
"rejected raw session metadata backfill for authority key"
);
return false;
}
if self.metadata.contains_key(key) {
false
} else {
self.metadata.insert(key.to_string(), value);
self.mark_head_canonical_metadata_key_mutated(key);
true
}
}
pub fn remove_metadata(&mut self, key: &str) {
if is_session_authority_metadata_key(key) {
tracing::warn!(
metadata_key = key,
"rejected raw session metadata removal for authority key"
);
return;
}
if self.metadata.remove(key).is_some() {
self.mark_head_canonical_metadata_key_mutated(key);
self.mark_content_mutated(SystemTime::now());
}
}
pub fn set_session_metadata(
&mut self,
metadata: SessionMetadata,
) -> Result<(), serde_json::Error> {
let metadata =
session_durable_config_authority::authorize_session_metadata_persist(metadata)
.map_err(<serde_json::Error as serde::ser::Error>::custom)?
.into_metadata();
let value = serde_json::to_value(metadata)?;
self.set_metadata_unchecked(SESSION_METADATA_KEY, value);
Ok(())
}
pub fn session_metadata(&self) -> Option<SessionMetadata> {
match self.try_session_metadata() {
Ok(metadata) => metadata,
Err(err) => fail_closed_generated_restore("session-metadata", err),
}
}
pub fn try_session_metadata(&self) -> Result<Option<SessionMetadata>, serde_json::Error> {
try_session_metadata_from_map(&self.metadata)
}
pub fn set_deferred_turn_state(
&mut self,
state: SessionDeferredTurnState,
) -> Result<(), serde_json::Error> {
let state = validate_deferred_turn_snapshot(state)
.map_err(<serde_json::Error as serde::ser::Error>::custom)?;
let value = serde_json::to_value(state)?;
self.set_metadata_unchecked(SESSION_DEFERRED_TURN_STATE_KEY, value);
Ok(())
}
pub fn try_deferred_turn_state(
&self,
) -> Result<Option<SessionDeferredTurnState>, serde_json::Error> {
self.metadata
.get(SESSION_DEFERRED_TURN_STATE_KEY)
.map(|value| {
let state = serde_json::from_value(value.clone())?;
validate_deferred_turn_snapshot(state)
.map_err(<serde_json::Error as serde::de::Error>::custom)
})
.transpose()
}
pub fn deferred_turn_state(&self) -> Option<SessionDeferredTurnState> {
match self.try_deferred_turn_state() {
Ok(state) => state,
Err(err) => fail_closed_generated_restore("deferred-turn", err),
}
}
pub(crate) fn stage_pending_callback_tool_batch(
&mut self,
batch: PendingCallbackToolBatch,
) -> Result<(), PendingCallbackBatchError> {
if matches!(
self.callback_tool_batch_state()?,
Some(CallbackToolBatchState::Pending { .. })
) {
return Err(PendingCallbackBatchError::AlreadyStaged);
}
validate_pending_callback_batch(self.messages(), &batch)?;
let value = serde_json::to_value(CallbackToolBatchState::Pending { batch })
.map_err(|error| PendingCallbackBatchError::Malformed(error.to_string()))?;
self.set_metadata_unchecked(SESSION_PENDING_CALLBACK_BATCH_KEY, value);
Ok(())
}
fn callback_tool_batch_state(
&self,
) -> Result<Option<CallbackToolBatchState>, PendingCallbackBatchError> {
self.metadata
.get(SESSION_PENDING_CALLBACK_BATCH_KEY)
.map(|value| {
serde_json::from_value(value.clone())
.map_err(|error| PendingCallbackBatchError::Malformed(error.to_string()))
})
.transpose()
}
pub(crate) fn pending_callback_tool_batch(
&self,
) -> Result<Option<PendingCallbackToolBatch>, PendingCallbackBatchError> {
match self.callback_tool_batch_state()? {
Some(CallbackToolBatchState::Pending { batch }) => {
validate_pending_callback_batch(self.messages(), &batch)?;
Ok(Some(batch))
}
Some(CallbackToolBatchState::Applied { .. }) | None => Ok(None),
}
}
pub(crate) fn resolve_pending_callback_tool_results(
&self,
incoming: Vec<ToolResult>,
) -> Result<ResolvedPendingCallbackToolResults, PendingCallbackBatchError> {
let Some(state) = self.callback_tool_batch_state()? else {
return Ok(ResolvedPendingCallbackToolResults::NoState);
};
let batch = match state {
CallbackToolBatchState::Pending { batch } => batch,
CallbackToolBatchState::Applied {
tool_use_order,
results,
async_ops,
..
} => {
let incoming_by_id = unique_tool_results(incoming)?;
let expected = tool_use_order.iter().cloned().collect::<BTreeSet<_>>();
let actual = incoming_by_id.keys().cloned().collect::<BTreeSet<_>>();
if actual != expected {
return Err(PendingCallbackBatchError::ResultSetMismatch { expected, actual });
}
let delivered = tool_use_order
.iter()
.map(|id| incoming_by_id.get(id).cloned())
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
PendingCallbackBatchError::Malformed(
"applied callback receipt is missing an ordered result".to_string(),
)
})?;
return if delivered == results {
Ok(ResolvedPendingCallbackToolResults::AlreadyApplied { async_ops })
} else {
Err(PendingCallbackBatchError::ConflictingRedelivery)
};
}
};
validate_pending_callback_batch(self.messages(), &batch)?;
let incoming_by_id = unique_tool_results(incoming)?;
let expected = batch
.pending_tool_use_ids
.iter()
.cloned()
.collect::<BTreeSet<_>>();
let actual = incoming_by_id.keys().cloned().collect::<BTreeSet<_>>();
if actual != expected {
return Err(PendingCallbackBatchError::ResultSetMismatch { expected, actual });
}
let mut all_by_id = unique_tool_results(batch.completed_results.clone())?;
all_by_id.extend(incoming_by_id);
let ordered = batch
.tool_use_order
.iter()
.map(|id| {
all_by_id.remove(id).ok_or_else(|| {
PendingCallbackBatchError::Malformed(format!(
"no result is available for assistant tool id '{id}'"
))
})
})
.collect::<Result<Vec<_>, _>>()?;
if !all_by_id.is_empty() {
return Err(PendingCallbackBatchError::Malformed(format!(
"results contain ids absent from assistant tool-use order: {:?}",
all_by_id.keys().collect::<Vec<_>>()
)));
}
Ok(ResolvedPendingCallbackToolResults::Pending {
batch,
ordered_results: ordered,
})
}
pub(crate) fn commit_pending_callback_tool_results(
&mut self,
batch: &PendingCallbackToolBatch,
ordered_results: Vec<ToolResult>,
post_tool_messages: Vec<Message>,
) -> Result<(), PendingCallbackBatchError> {
let current = self
.pending_callback_tool_batch()?
.ok_or(PendingCallbackBatchError::Missing)?;
if ¤t != batch {
return Err(PendingCallbackBatchError::Malformed(
"pending callback batch changed between prepare and commit".to_string(),
));
}
let actual_order = ordered_results
.iter()
.map(|result| result.tool_use_id.clone())
.collect::<Vec<_>>();
if actual_order != batch.tool_use_order {
return Err(PendingCallbackBatchError::Malformed(format!(
"resolved result order {actual_order:?} does not match assistant order {:?}",
batch.tool_use_order
)));
}
self.push(Message::tool_results(ordered_results.clone()));
let pending_ids = batch
.pending_tool_use_ids
.iter()
.cloned()
.collect::<BTreeSet<_>>();
let applied_callback_results = ordered_results
.into_iter()
.filter(|result| pending_ids.contains(&result.tool_use_id))
.collect();
let value = serde_json::to_value(CallbackToolBatchState::Applied {
tool_use_order: batch.pending_tool_use_ids.clone(),
results: applied_callback_results,
async_ops: batch.async_ops.clone(),
post_tool_messages,
post_tool_messages_applied: false,
})
.map_err(|error| PendingCallbackBatchError::Malformed(error.to_string()))?;
self.set_metadata_unchecked(SESSION_PENDING_CALLBACK_BATCH_KEY, value);
Ok(())
}
pub(crate) fn apply_pending_callback_resume_effects(
&mut self,
) -> Result<Vec<crate::event::AssistantImageEvent>, PendingCallbackBatchError> {
let Some(CallbackToolBatchState::Applied {
tool_use_order,
results,
async_ops,
post_tool_messages,
post_tool_messages_applied,
}) = self.callback_tool_batch_state()?
else {
return Ok(Vec::new());
};
if post_tool_messages_applied {
return Ok(Vec::new());
}
let image_events = post_tool_messages
.iter()
.filter_map(|message| match message {
Message::BlockAssistant(assistant) => Some(assistant.blocks.as_slice()),
_ => None,
})
.flatten()
.filter_map(crate::event::AssistantImageEvent::from_assistant_block)
.collect::<Vec<_>>();
let applied_state = CallbackToolBatchState::Applied {
tool_use_order,
results,
async_ops,
post_tool_messages: post_tool_messages.clone(),
post_tool_messages_applied: true,
};
let value = serde_json::to_value(applied_state)
.map_err(|error| PendingCallbackBatchError::Malformed(error.to_string()))?;
self.push_batch(post_tool_messages);
self.set_metadata_unchecked(SESSION_PENDING_CALLBACK_BATCH_KEY, value);
Ok(image_events)
}
pub fn set_lifecycle_terminal(
&mut self,
terminal: SessionLifecycleTerminal,
) -> Result<(), serde_json::Error> {
let value = serde_json::to_value(terminal)?;
self.set_metadata_unchecked(SESSION_LIFECYCLE_TERMINAL_KEY, value);
Ok(())
}
pub fn try_lifecycle_terminal(
&self,
) -> Result<Option<SessionLifecycleTerminal>, serde_json::Error> {
try_lifecycle_terminal_from_map(&self.metadata)
}
pub fn lifecycle_terminal(&self) -> Option<SessionLifecycleTerminal> {
match self.try_lifecycle_terminal() {
Ok(state) => state,
Err(err) => fail_closed_generated_restore("session-lifecycle-terminal", err),
}
}
pub fn set_build_state(&mut self, state: SessionBuildState) -> Result<(), serde_json::Error> {
let state = session_durable_config_authority::authorize_session_build_state_persist(state)
.map_err(<serde_json::Error as serde::ser::Error>::custom)?
.into_state();
let value = serde_json::to_value(state)?;
self.set_metadata_unchecked(SESSION_BUILD_STATE_KEY, value);
Ok(())
}
pub fn build_state(&self) -> Option<SessionBuildState> {
match self.try_build_state() {
Ok(state) => state,
Err(err) => fail_closed_generated_restore("session-build-state", err),
}
}
pub fn try_build_state(&self) -> Result<Option<SessionBuildState>, serde_json::Error> {
let Some(value) = self.metadata.get(SESSION_BUILD_STATE_KEY) else {
return Ok(None);
};
let state = serde_json::from_value::<SessionBuildState>(value.clone())?;
session_durable_config_authority::restore_session_build_state(state)
.map(Some)
.map_err(<serde_json::Error as serde::de::Error>::custom)
}
pub fn set_tool_visibility_state(
&mut self,
state: AuthorizedSessionToolVisibilityState,
) -> Result<(), serde_json::Error> {
let value = serde_json::to_value(state.into_state())?;
self.set_metadata_unchecked(SESSION_TOOL_VISIBILITY_STATE_KEY, value);
Ok(())
}
#[cfg(test)]
pub(crate) fn clear_tool_visibility_state(&mut self) {
self.remove_metadata_unchecked(SESSION_TOOL_VISIBILITY_STATE_KEY);
}
pub fn tool_visibility_state(
&self,
) -> Result<Option<SessionToolVisibilityState>, serde_json::Error> {
self.try_tool_visibility_state()
}
pub fn try_tool_visibility_state(
&self,
) -> Result<Option<SessionToolVisibilityState>, serde_json::Error> {
self.metadata
.get(SESSION_TOOL_VISIBILITY_STATE_KEY)
.map(|value| serde_json::from_value(value.clone()))
.transpose()
}
pub fn transcript_history_state(
&self,
) -> Result<Option<TranscriptHistoryState>, serde_json::Error> {
if let Some(state) = self.history_caches.shared_state.get() {
return Ok(Some(state.as_ref().clone()));
}
self.metadata
.get(SESSION_TRANSCRIPT_HISTORY_STATE_KEY)
.map(|value| serde_json::from_value(value.clone()))
.transpose()
}
pub fn transcript_history_state_shared(
&self,
) -> Result<Option<std::sync::Arc<TranscriptHistoryState>>, serde_json::Error> {
if let Some(state) = self.history_caches.shared_state.get() {
return Ok(Some(state));
}
let Some(state) = self.transcript_history_state()? else {
return Ok(None);
};
let state = std::sync::Arc::new(state);
self.history_caches
.shared_state
.set(std::sync::Arc::clone(&state));
Ok(Some(state))
}
pub fn validated_transcript_history_state(
&self,
) -> Result<Option<ValidatedTranscriptHistory>, TranscriptEditError> {
let Some(state) = self
.transcript_history_state_shared()
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?
else {
return Ok(None);
};
if self.transcript_history_metadata_validation
== TranscriptHistoryMetadataValidation::Validated
{
return Ok(Some(ValidatedTranscriptHistory::adopt_session_validated(
state,
)));
}
Err(TranscriptEditError::HistoryStateMalformed(
"transcript-history graph has structural bytes but no verified materialization or construction authority"
.to_string(),
))
}
pub fn already_validated_transcript_history_state(
&self,
) -> Result<Option<ValidatedTranscriptHistory>, serde_json::Error> {
if self.transcript_history_metadata_validation
!= TranscriptHistoryMetadataValidation::Validated
{
return Ok(None);
}
Ok(self
.transcript_history_state_shared()?
.map(ValidatedTranscriptHistory::adopt_session_validated))
}
pub(crate) fn live_transcript_extends_history_head(
&self,
state: &TranscriptHistoryState,
_live_revision: &str,
) -> Result<bool, TranscriptEditError> {
let current_count = u64::try_from(self.messages.len()).map_err(|_| {
TranscriptEditError::HistoryStateMalformed(
"live transcript row count exceeds u64".to_string(),
)
})?;
let endpoint = state.final_endpoint_witness().ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"compact transcript graph has no final endpoint witness".to_string(),
)
})?;
Ok(self.exact_message_row_lineage_extends(endpoint.row_prefix(), current_count))
}
pub fn compaction_projection_intents(
&self,
) -> Result<Vec<crate::memory::CompactionProjectionIntent>, serde_json::Error> {
self.metadata
.get(crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY)
.map(|value| serde_json::from_value(value.clone()))
.transpose()
.map(Option::unwrap_or_default)
}
pub fn validated_compaction_projection_intents(
&self,
) -> Result<Vec<crate::memory::CompactionProjectionIntent>, serde_json::Error> {
let intents = self.compaction_projection_intents()?;
if intents.is_empty() {
return Ok(intents);
}
let history = self
.validated_transcript_history_state()
.map_err(|error| <serde_json::Error as serde::ser::Error>::custom(error.to_string()))?;
let mut unique = std::collections::HashSet::new();
for intent in &intents {
if intent.projection.session_id() != self.id() {
return Err(<serde_json::Error as serde::ser::Error>::custom(
"compaction projection outbox intent has a foreign session id",
));
}
if !unique.insert(intent.projection.clone()) {
return Err(<serde_json::Error as serde::ser::Error>::custom(
"compaction projection outbox contains a duplicate rewrite identity",
));
}
let backed = history.as_ref().is_some_and(|history| {
history.commits().any(|commit| {
intent
.projection
.matches_transcript_rewrite(self.id(), commit)
})
});
if !backed {
return Err(<serde_json::Error as serde::ser::Error>::custom(format!(
"compaction projection outbox intent {} has no matching TranscriptRewriteCommit",
intent.projection.revision()
)));
}
}
Ok(intents)
}
pub fn add_compaction_projection_intent(
&mut self,
intent: crate::memory::CompactionProjectionIntent,
) -> Result<(), serde_json::Error> {
if intent.projection.session_id() != self.id() {
return Err(<serde_json::Error as serde::ser::Error>::custom(
"compaction projection intent session does not match snapshot session",
));
}
let history = self
.validated_transcript_history_state()
.map_err(|error| <serde_json::Error as serde::ser::Error>::custom(error.to_string()))?
.ok_or_else(|| {
<serde_json::Error as serde::ser::Error>::custom(
"compaction projection intent requires transcript history state",
)
})?;
let owns_commit = history.commits().any(|commit| {
commit.parent_revision == intent.projection.parent_revision()
&& commit.revision == intent.projection.revision()
&& intent
.projection
.matches_transcript_rewrite(self.id(), commit)
});
if !owns_commit {
return Err(<serde_json::Error as serde::ser::Error>::custom(
"compaction projection intent is not backed by the session transcript graph",
));
}
let mut intents = self.validated_compaction_projection_intents()?;
if let Some(existing) = intents
.iter()
.find(|existing| existing.projection == intent.projection)
{
if existing == &intent {
return Ok(());
}
return Err(<serde_json::Error as serde::ser::Error>::custom(
"compaction projection intent conflicts with an existing rewrite identity",
));
}
intents.push(intent);
self.set_metadata_unchecked(
crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY,
serde_json::to_value(intents)?,
);
Ok(())
}
pub fn complete_compaction_projection_intent(
&mut self,
projection: &crate::memory::CompactionProjectionId,
) -> Result<Option<crate::memory::CompactionProjectionIntent>, serde_json::Error> {
let mut intents = self.compaction_projection_intents()?;
let Some(position) = intents
.iter()
.position(|intent| &intent.projection == projection)
else {
return Ok(None);
};
let completed = intents.remove(position);
if intents.is_empty() {
self.remove_metadata_unchecked(
crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY,
);
} else {
self.set_metadata_unchecked(
crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY,
serde_json::to_value(intents)?,
);
}
Ok(Some(completed))
}
pub fn validate_transcript_history_state(&self) -> Result<(), TranscriptEditError> {
if self.transcript_history_metadata_validation
== TranscriptHistoryMetadataValidation::Validated
{
return Ok(());
}
if self.history_caches.shared_state.get().is_some()
|| self
.metadata
.contains_key(SESSION_TRANSCRIPT_HISTORY_STATE_KEY)
{
return Err(TranscriptEditError::HistoryStateMalformed(
"transcript-history graph has not crossed verified materialization or construction authority"
.to_string(),
));
}
Ok(())
}
pub fn clear_transcript_history_state(&mut self) {
self.remove_metadata_unchecked(SESSION_TRANSCRIPT_HISTORY_STATE_KEY);
}
pub fn adopt_recovered_head_state(&mut self, head: &Session) -> Result<(), String> {
const RECOVERY_OWNED_KEYS: [&str; 3] = [
SESSION_TRANSCRIPT_HISTORY_STATE_KEY,
SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY,
SESSION_LIFECYCLE_TERMINAL_KEY,
];
let recovered_archived = self
.try_lifecycle_terminal()
.map_err(|error| format!("recovered lifecycle-terminal is malformed: {error}"))?
== Some(SessionLifecycleTerminal::Archived);
let head_terminal = head
.try_lifecycle_terminal()
.map_err(|error| format!("durable-head lifecycle-terminal is malformed: {error}"))?;
let head_archived = head_terminal == Some(SessionLifecycleTerminal::Archived);
let mut lifecycle_authority = session_document::SessionDocumentMachineAuthority::new();
let lifecycle_merge = lifecycle_authority
.resolve_session_document_lifecycle_merge(
session_document::SessionDocumentKey::new(self.id.to_string()),
recovered_archived,
head_archived,
)
.map_err(|error| {
format!("session document authority rejected recovered lifecycle merge: {error}")
})?
.into_iter()
.find_map(|effect| {
match effect {
session_document::SessionDocumentEffect::SessionDocumentLifecycleMergeResolved {
merge,
} => Some(merge),
_ => None,
}
})
.ok_or_else(|| {
"session document authority emitted no recovered lifecycle merge".to_string()
})?;
let SessionSerdeRef {
version: _identity_version,
id: _identity_id,
messages: _recovery_owned_messages,
created_at: _identity_created_at,
updated_at: head_updated_at,
metadata: head_metadata,
usage: head_usage,
} = persisted_envelope_ref(head, None);
self.usage = head_usage.clone();
self.adopt_head_canonical_metadata_baseline_from(head);
self.metadata.retain(|key, _| {
RECOVERY_OWNED_KEYS.contains(&key.as_str()) || head_metadata.contains_key(key)
});
for (key, value) in head_metadata {
if RECOVERY_OWNED_KEYS.contains(&key.as_str()) {
continue;
}
self.metadata.insert(key.clone(), value.clone());
}
match lifecycle_merge {
session_document::SessionDocumentLifecycleMerge::CarryArchived => self
.set_lifecycle_terminal(SessionLifecycleTerminal::Archived)
.map_err(|error| {
format!("failed to realize absorbing Archived terminal: {error}")
})?,
session_document::SessionDocumentLifecycleMerge::CarryAuthority => {
match head_terminal {
Some(terminal) => self.set_lifecycle_terminal(terminal).map_err(|error| {
format!("failed to realize durable-head lifecycle terminal: {error}")
})?,
None => {
self.remove_metadata_unchecked(SESSION_LIFECYCLE_TERMINAL_KEY);
}
}
}
}
self.mark_content_mutated(*head_updated_at);
Ok(())
}
pub fn transcript_revision_body(
&self,
revision: &str,
) -> Result<Option<TranscriptRevisionBody>, serde_json::Error> {
let Some(history) = self
.validated_transcript_history_state()
.map_err(|error| <serde_json::Error as serde::ser::Error>::custom(error.to_string()))?
else {
return Ok(None);
};
if !history.state().contains_revision(revision) {
return Ok(None);
}
history
.materialize_revision(revision)
.map(Some)
.map_err(|error| <serde_json::Error as serde::ser::Error>::custom(error.to_string()))
}
pub fn transcript_revision_messages(
&self,
revision: &str,
) -> Result<Option<Vec<Message>>, serde_json::Error> {
Ok(self
.transcript_revision_body(revision)?
.map(|body| body.messages))
}
pub fn apply_transcript_history_state(
&mut self,
mut state: TranscriptHistoryState,
) -> Result<(), TranscriptEditError> {
state.compact_mechanical_revision_bodies()?;
self.apply_proved_transcript_history_state(state)
}
pub fn apply_validated_transcript_history_state(
&mut self,
validated: ValidatedTranscriptHistory,
) -> Result<(), TranscriptEditError> {
let mut state = validated.into_state();
state.prune_mechanical_revision_bodies();
self.apply_proved_transcript_history_state(state)
}
pub fn install_validated_audited_transcript_history_preserving_live(
&mut self,
validated: ValidatedTranscriptHistory,
) -> Result<(), TranscriptEditError> {
let mut state = validated.into_state();
state.canonicalize_to_latest_audited_head();
let live_revision = self
.transcript_content_digest()
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
if !self.live_transcript_extends_history_head(&state, &live_revision)? {
return Err(TranscriptEditError::HistoryStateMalformed(format!(
"audited transcript head {} is not a prefix ancestor of live revision {live_revision}",
state.head()
)));
}
self.install_validated_transcript_history_state(state)
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))
}
pub fn with_validated_transcript_history_projection(
&self,
validated: ValidatedTranscriptHistory,
) -> Result<Self, TranscriptEditError> {
let mut projected = self.clone_persisted_envelope_without_transcript_history();
projected.apply_validated_transcript_history_state(validated)?;
Ok(projected)
}
pub fn with_validated_transcript_rewrite_parent_projection(
&self,
validated: &ValidatedTranscriptHistory,
commit: &TranscriptRewriteCommit,
) -> Result<Self, TranscriptEditError> {
let carried = self.validated_transcript_history_state()?.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"session carries no transcript graph for parent projection".to_string(),
)
})?;
if !carried.shares_exact_state_with(validated) {
return Err(TranscriptEditError::HistoryStateMalformed(
"parent projection proof does not belong to this Session".to_string(),
));
}
let commit_index = commit
.rewrite_generation
.checked_sub(1)
.and_then(|index| usize::try_from(index).ok())
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(format!(
"rewrite occurrence generation {} cannot address this graph",
commit.rewrite_generation
))
})?;
let bound = validated.state().commit(commit_index).ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(format!(
"rewrite occurrence generation {} is outside the proved graph",
commit.rewrite_generation
))
})?;
if bound != commit {
return Err(TranscriptEditError::HistoryStateMalformed(format!(
"rewrite occurrence generation {} does not match the proved graph commit",
commit.rewrite_generation
)));
}
let parent_body = validated.materialize_rewrite_parent(commit)?;
let mut projected = self.clone_persisted_envelope_without_transcript_history();
if commit_index != 0 {
let previous_commit = validated.state().commit(commit_index - 1).ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"rewrite parent projection lost its preceding occurrence".to_string(),
)
})?;
projected.apply_validated_transcript_history_state(
validated.project_at_rewrite_commit(previous_commit)?,
)?;
}
let realtime_rebase = projected.prepare_realtime_transcript_rebase_after_rewrite(
&parent_body.messages,
RealtimeTranscriptSnapshotReasonV1::RecoveryRebase,
)?;
projected
.realtime_transcript
.apply_prepared_rebase(realtime_rebase);
projected.messages.replace(parent_body.messages);
projected.mark_content_mutated(parent_body.created_at);
let projected_revision = projected
.transcript_content_digest()
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
if projected_revision != commit.parent_revision {
return Err(TranscriptEditError::HistoryStateMalformed(format!(
"proved parent projection digest {projected_revision} differs from commit parent {}",
commit.parent_revision
)));
}
Ok(projected)
}
fn clone_persisted_envelope_without_transcript_history(&self) -> Self {
let metadata = self
.metadata
.iter()
.filter(|(key, _)| key.as_str() != SESSION_TRANSCRIPT_HISTORY_STATE_KEY)
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
Self {
version: self.version,
id: self.id.clone(),
messages: self.messages.clone(),
created_at: self.created_at,
updated_at: self.updated_at,
metadata,
realtime_transcript: self.realtime_transcript.clone(),
history_caches: Box::default(),
transcript_history_metadata_validation: TranscriptHistoryMetadataValidation::Validated,
usage: self.usage.clone(),
}
}
fn apply_proved_transcript_history_state(
&mut self,
state: TranscriptHistoryState,
) -> Result<(), TranscriptEditError> {
let head_body = state.materialize_revision(state.head())?;
let realtime_rebase = self.prepare_realtime_transcript_rebase_after_rewrite(
&head_body.messages,
RealtimeTranscriptSnapshotReasonV1::RecoveryRebase,
)?;
let mut updated_at = head_body.created_at;
for commit in state.commits() {
if commit.committed_at > updated_at {
updated_at = commit.committed_at;
}
}
self.install_validated_transcript_history_state(state)
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?;
self.realtime_transcript
.apply_prepared_rebase(realtime_rebase);
self.messages.replace(head_body.messages);
self.mark_content_mutated(updated_at);
Ok(())
}
pub fn transcript_revision(&self) -> Result<String, serde_json::Error> {
self.transcript_content_digest()
}
pub fn transcript_rewrite_generation(&self) -> Result<u64, serde_json::Error> {
Ok(self
.transcript_history_state_shared()?
.and_then(|state| state.last_commit().map(|commit| commit.rewrite_generation))
.unwrap_or(0))
}
pub fn commit_transcript_rewrite(
&mut self,
selection: TranscriptRewriteSelection,
replacement: Vec<Message>,
reason: TranscriptRewriteReason,
actor: Option<String>,
expected_parent_revision: Option<String>,
) -> Result<TranscriptRewriteCommit, TranscriptEditError> {
let selection = selection.into_current_edit_semantic();
if selection.semantic() == TranscriptRewriteSemantic::Compaction {
return Err(TranscriptEditError::InvalidTranscriptShape(
"typed compaction rewrites require a core-validated compaction witness".to_string(),
));
}
self.commit_transcript_rewrite_authorized(
selection,
replacement,
reason,
actor,
expected_parent_revision,
)
}
fn commit_transcript_rewrite_authorized(
&mut self,
selection: TranscriptRewriteSelection,
replacement: Vec<Message>,
reason: TranscriptRewriteReason,
actor: Option<String>,
expected_parent_revision: Option<String>,
) -> Result<TranscriptRewriteCommit, TranscriptEditError> {
self.commit_transcript_rewrite_bound(
selection,
replacement,
reason,
actor,
expected_parent_revision,
None,
)
}
fn commit_transcript_rewrite_bound(
&mut self,
selection: TranscriptRewriteSelection,
replacement: Vec<Message>,
reason: TranscriptRewriteReason,
actor: Option<String>,
expected_parent_revision: Option<String>,
expected_revision: Option<&str>,
) -> Result<TranscriptRewriteCommit, TranscriptEditError> {
let parent_revision = self
.transcript_revision()
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?;
if let Some(expected) = expected_parent_revision
&& expected != parent_revision
{
return Err(TranscriptEditError::RevisionConflict {
expected,
actual: parent_revision,
});
}
let (start, end) = selection.bounds();
let message_count = self.messages.len();
if start > end || end > message_count {
return Err(TranscriptEditError::InvalidRewriteRange {
start,
end,
message_count,
});
}
let replacement_len = replacement.len();
let mut rewritten = Vec::with_capacity(
start
.saturating_add(replacement_len)
.saturating_add(message_count.saturating_sub(end)),
);
rewritten.extend_from_slice(&self.messages[..start]);
rewritten.extend(replacement.iter().cloned());
rewritten.extend_from_slice(&self.messages[end..]);
validate_transcript_tool_result_shape(&rewritten)?;
let revision = transcript_messages_digest(&rewritten)
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?;
if let Some(expected) = expected_revision
&& expected != revision
{
return Err(TranscriptEditError::InvalidTranscriptShape(
"validated compaction witness does not authorize this exact transcript rebuild"
.to_string(),
));
}
if revision == parent_revision {
return Err(TranscriptEditError::NoOpRewrite { revision });
}
let original_span_digest = if start == 0 && end == message_count {
self.transcript_content_digest()
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?
} else {
transcript_messages_digest(&self.messages[start..end])
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?
};
let replacement_digest = if start == 0 && start + replacement_len == rewritten.len() {
revision.clone()
} else {
transcript_messages_digest(&rewritten[start..start + replacement_len])
.map_err(|err| TranscriptEditError::HistoryStateMalformed(err.to_string()))?
};
let realtime_rebase = self.prepare_realtime_transcript_rebase_after_rewrite(
&rewritten,
RealtimeTranscriptSnapshotReasonV1::TranscriptRewrite,
)?;
let prior_history = self.validated_transcript_history_state()?;
let rewrite_generation = prior_history
.as_ref()
.and_then(|history| history.last_commit())
.map_or(Some(1), |commit| commit.rewrite_generation.checked_add(1))
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"transcript rewrite generation exhausted u64".to_string(),
)
})?;
let committed_at = SystemTime::now();
let commit = TranscriptRewriteCommit {
rewrite_generation,
parent_revision,
revision,
selection,
original_span_digest,
replacement_digest,
messages_before: message_count,
messages_after: rewritten.len(),
reason,
actor,
committed_at,
};
self.finish_compact_transcript_rewrite(
prior_history,
commit,
replacement,
rewritten,
realtime_rebase,
)
}
fn finish_compact_transcript_rewrite(
&mut self,
prior_history: Option<ValidatedTranscriptHistory>,
commit: TranscriptRewriteCommit,
replacement: Vec<Message>,
rewritten: Vec<Message>,
realtime_rebase: PreparedRealtimeTranscriptRebase,
) -> Result<TranscriptRewriteCommit, TranscriptEditError> {
let parent_row_prefix = self
.exact_message_row_prefix_at(u64::try_from(self.messages.len()).map_err(|_| {
TranscriptEditError::HistoryStateMalformed(
"live transcript row count exceeds u64".to_string(),
)
})?)
.ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"live transcript has no exact row-lineage authority".to_string(),
)
})?;
let (start, end) = commit.selection.bounds();
let serialized_replacement = replacement
.iter()
.map(serde_json::to_vec)
.collect::<Result<Vec<_>, _>>()
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
let start = u64::try_from(start).map_err(|_| {
TranscriptEditError::HistoryStateMalformed(
"rewrite start exceeds durable row coordinates".to_string(),
)
})?;
let end = u64::try_from(end).map_err(|_| {
TranscriptEditError::HistoryStateMalformed(
"rewrite end exceeds durable row coordinates".to_string(),
)
})?;
let result_row_prefix = parent_row_prefix
.replace_serialized_range(start, end, &serialized_replacement)
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
let result_witness = TranscriptEndpointWitness::from_messages_with_row_prefix(
&rewritten,
result_row_prefix.clone(),
)
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
let state = match prior_history {
None => {
let parent = TranscriptRevisionBody {
revision: commit.parent_revision.clone(),
parent_revision: None,
messages: self.messages.to_vec(),
created_at: self.updated_at,
};
TranscriptHistoryState::from_authorized_first_rewrite(
parent,
parent_row_prefix,
&commit.revision,
&rewritten,
commit.committed_at,
result_row_prefix.clone(),
replacement,
commit.clone(),
)?
}
Some(history) => {
let mut state = history.state().clone();
let endpoint = state.final_endpoint_witness().ok_or_else(|| {
TranscriptEditError::HistoryStateMalformed(
"compact transcript graph has no final endpoint witness".to_string(),
)
})?;
if self.messages.len() < endpoint.message_count() {
return Err(TranscriptEditError::HistoryStateMalformed(
"live rewrite parent is shorter than the audited endpoint".to_string(),
));
}
let appended = self.messages[endpoint.message_count()..].to_vec();
let serialized_appended = appended
.iter()
.map(serde_json::to_vec)
.collect::<Result<Vec<_>, _>>()
.map_err(|error| {
TranscriptEditError::HistoryStateMalformed(error.to_string())
})?;
let exact_append_prefix = endpoint
.row_prefix()
.extend_serialized_rows(&serialized_appended)
.map_err(|error| {
TranscriptEditError::HistoryStateMalformed(error.to_string())
})?;
let parent_advance = if exact_append_prefix == parent_row_prefix {
TranscriptParentAdvance::ExactAppend { appended }
} else {
return Err(TranscriptEditError::HistoryStateMalformed(
"live rewrite parent is not an exact audited append".to_string(),
));
};
let messages_before_base = endpoint.message_count();
state.append_authorized_rewrite(
commit.clone(),
messages_before_base,
parent_advance,
parent_row_prefix,
replacement,
result_witness,
self.updated_at,
commit.committed_at,
)?;
state
}
};
self.install_validated_transcript_history_state(state)
.map_err(|error| TranscriptEditError::HistoryStateMalformed(error.to_string()))?;
self.realtime_transcript
.apply_prepared_rebase(realtime_rebase);
self.messages.replace(rewritten);
self.mark_content_mutated(commit.committed_at);
if !self.install_exact_message_row_prefix(result_row_prefix) {
return Err(TranscriptEditError::HistoryStateMalformed(
"failed to install rewrite result row-lineage authority".to_string(),
));
}
Ok(commit)
}
pub fn set_mob_tool_authority_context(
&mut self,
authority_context: Option<MobToolAuthorityContext>,
) -> Result<(), serde_json::Error> {
if let Some(authority_context) = authority_context.as_ref()
&& !authority_context.is_generated_authority_context()
{
return Err(<serde_json::Error as serde::de::Error>::custom(
"mob authority context was not minted by generated authority",
));
}
let mut build_state = self.build_state().ok_or_else(|| {
<serde_json::Error as serde::de::Error>::custom(format!(
"session {} is missing session build state",
self.id
))
})?;
build_state.mob_tool_authority_context = authority_context;
self.set_build_state(build_state)
}
pub fn mob_tool_authority_context(&self) -> Option<MobToolAuthorityContext> {
self.build_state()
.and_then(|state| state.mob_tool_authority_context)
.filter(MobToolAuthorityContext::is_generated_authority_context)
}
pub fn fork_at(&self, index: usize) -> Self {
let now = SystemTime::now();
let truncated = self.messages[..index.min(self.messages.len())].to_vec();
let id = SessionId::new();
Self {
version: session_version(),
realtime_transcript: Box::new(SessionRealtimeTranscriptProjection::empty(&id)),
id,
messages: TranscriptMessages::from_fresh_branch(truncated),
created_at: now,
updated_at: now,
metadata: self.fork_metadata_projection(),
history_caches: Box::default(),
transcript_history_metadata_validation: TranscriptHistoryMetadataValidation::Validated,
usage: self.usage.clone(),
}
}
pub fn fork_replacing(
&self,
message_index: usize,
replacement: TranscriptReplacement,
) -> Result<Self, TranscriptEditError> {
let Some(original) = self.messages.get(message_index) else {
return Err(TranscriptEditError::MessageIndexOutOfBounds {
message_index,
message_count: self.messages.len(),
});
};
let replacement_message = match replacement {
TranscriptReplacement::Message { message } => message,
TranscriptReplacement::UserContentBlock { block_index, block } => {
let Message::User(user) = original else {
return Err(TranscriptEditError::MessageRoleMismatch {
message_index,
expected: "user",
actual: message_role_name(original),
});
};
if block_index >= user.content.len() {
return Err(TranscriptEditError::BlockIndexOutOfBounds {
block_kind: "user content block",
block_index,
block_count: user.content.len(),
});
}
let mut edited = user.clone();
edited.content[block_index] = block;
Message::User(edited)
}
TranscriptReplacement::AssistantBlock { block_index, block } => {
let Message::BlockAssistant(assistant) = original else {
return Err(TranscriptEditError::MessageRoleMismatch {
message_index,
expected: "block_assistant",
actual: message_role_name(original),
});
};
if block_index >= assistant.blocks.len() {
return Err(TranscriptEditError::BlockIndexOutOfBounds {
block_kind: "assistant block",
block_index,
block_count: assistant.blocks.len(),
});
}
let mut edited = assistant.clone();
edited.blocks[block_index] = block;
Message::BlockAssistant(edited)
}
TranscriptReplacement::ToolResultContentBlock {
result_index,
block_index,
block,
} => {
let Message::ToolResults {
results,
created_at,
} = original
else {
return Err(TranscriptEditError::MessageRoleMismatch {
message_index,
expected: "tool_results",
actual: message_role_name(original),
});
};
let Some(result) = results.get(result_index) else {
return Err(TranscriptEditError::BlockIndexOutOfBounds {
block_kind: "tool result",
block_index: result_index,
block_count: results.len(),
});
};
if block_index >= result.content.len() {
return Err(TranscriptEditError::BlockIndexOutOfBounds {
block_kind: "tool result content block",
block_index,
block_count: result.content.len(),
});
}
let mut edited_results = results.clone();
edited_results[result_index].content[block_index] = block;
Message::ToolResults {
results: edited_results,
created_at: *created_at,
}
}
};
let mut forked = self.fork_at(message_index);
forked.push(replacement_message);
Ok(forked)
}
pub fn fork(&self) -> Self {
let now = SystemTime::now();
let id = SessionId::new();
Self {
version: session_version(),
realtime_transcript: Box::new(SessionRealtimeTranscriptProjection::empty(&id)),
id,
messages: self.messages.clone(),
created_at: now,
updated_at: now,
metadata: self.fork_metadata_projection(),
history_caches: Box::default(),
transcript_history_metadata_validation: TranscriptHistoryMetadataValidation::Validated,
usage: self.usage.clone(),
}
}
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SessionMeta {
pub id: SessionId,
pub created_at: SystemTime,
pub updated_at: SystemTime,
pub message_count: usize,
pub total_tokens: u64,
#[serde(default)]
pub metadata: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SessionMetadata {
pub schema_version: u32,
pub model: String,
pub max_tokens: u32,
#[serde(default = "crate::config::default_structured_output_retries")]
pub structured_output_retries: u32,
pub provider: Provider,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub self_hosted_server_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_params: Option<crate::lifecycle::run_primitive::ProviderParamsOverride>,
pub tooling: SessionTooling,
#[serde(default)]
pub keep_alive: bool,
pub comms_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_meta: Option<PeerMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub realm_id: Option<crate::RealmId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instance_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_generation: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_binding: Option<crate::AuthBindingRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mob_member_binding: Option<crate::MobMemberBinding>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct SessionLlmIdentity {
pub model: String,
pub provider: Provider,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub self_hosted_server_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_params: Option<crate::lifecycle::run_primitive::ProviderParamsOverride>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_binding: Option<crate::AuthBindingRef>,
}
pub struct SessionLlmIdentityOverride<'a> {
pub model: Option<&'a str>,
pub provider: Option<Provider>,
pub self_hosted_server_id: Option<&'a str>,
pub provider_params:
Option<TurnMetadataOverride<&'a crate::lifecycle::run_primitive::ProviderParamsOverride>>,
pub auth_binding: Option<TurnMetadataOverride<&'a crate::AuthBindingRef>>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SessionLlmIdentityOverrideError {
#[error("provider override requires model on an existing session")]
ProviderRequiresModel,
#[error("{0}")]
ProviderModelMismatch(String),
#[error("self-hosted provider requires a registered model alias; '{model}' is not configured")]
MissingSelfHostedAlias { model: String },
#[error("self_hosted_server_id requires provider 'self_hosted'")]
SelfHostedServerRequiresSelfHostedProvider,
#[error("self_hosted_server_id must not be empty")]
EmptySelfHostedServerId,
#[error(
"self-hosted model '{model}' is configured on server '{configured}', not requested server '{requested}'"
)]
SelfHostedServerMismatch {
model: String,
requested: String,
configured: String,
},
}
pub fn resolve_session_llm_identity_override(
current: &SessionLlmIdentity,
registry: &crate::ModelRegistry,
overrides: SessionLlmIdentityOverride<'_>,
) -> Result<SessionLlmIdentity, SessionLlmIdentityOverrideError> {
if overrides.provider.is_some() && overrides.model.is_none() {
return Err(SessionLlmIdentityOverrideError::ProviderRequiresModel);
}
let model = overrides
.model
.map(str::to_string)
.unwrap_or_else(|| current.model.clone());
let provider = if let Some(provider) = overrides.provider {
provider
} else if overrides.model.is_some() {
registry
.entry(&model)
.map_or(current.provider, |entry| entry.provider)
} else {
current.provider
};
if (overrides.model.is_some() || overrides.provider.is_some())
&& let Some(reason) = registry.provider_override_mismatch_reason(provider, &model)
{
return Err(SessionLlmIdentityOverrideError::ProviderModelMismatch(
reason,
));
}
let provider_params = match overrides.provider_params {
Some(TurnMetadataOverride::Clear) => None,
Some(TurnMetadataOverride::Set(value)) => Some(value.clone()),
None => current.provider_params.clone(),
};
if overrides.self_hosted_server_id.is_some() && provider != Provider::SelfHosted {
return Err(SessionLlmIdentityOverrideError::SelfHostedServerRequiresSelfHostedProvider);
}
let self_hosted_server_id = if provider == Provider::SelfHosted {
if let Some(requested_server_id) = overrides.self_hosted_server_id {
if requested_server_id.trim().is_empty() {
return Err(SessionLlmIdentityOverrideError::EmptySelfHostedServerId);
}
let entry = registry
.entry_for_provider(Provider::SelfHosted, &model)
.ok_or_else(|| SessionLlmIdentityOverrideError::MissingSelfHostedAlias {
model: model.clone(),
})?;
let configured_server_id = entry
.self_hosted
.as_ref()
.map(|server| server.server_id.as_str())
.ok_or_else(|| SessionLlmIdentityOverrideError::MissingSelfHostedAlias {
model: model.clone(),
})?;
if configured_server_id != requested_server_id {
return Err(SessionLlmIdentityOverrideError::SelfHostedServerMismatch {
model,
requested: requested_server_id.to_string(),
configured: configured_server_id.to_string(),
});
}
Some(requested_server_id.to_string())
} else if overrides.model.is_none() {
current.self_hosted_server_id.clone().or_else(|| {
registry
.entry_for_provider(Provider::SelfHosted, &model)
.and_then(|entry| entry.self_hosted.as_ref())
.map(|server| server.server_id.clone())
})
} else {
let entry = registry
.entry_for_provider(Provider::SelfHosted, &model)
.ok_or_else(|| SessionLlmIdentityOverrideError::MissingSelfHostedAlias {
model: model.clone(),
})?;
entry
.self_hosted
.as_ref()
.map(|server| server.server_id.clone())
}
} else {
None
};
let auth_binding = match overrides.auth_binding {
Some(TurnMetadataOverride::Clear) => None,
Some(TurnMetadataOverride::Set(value)) => Some(value.clone()),
None if provider != current.provider => None,
None => current.auth_binding.clone(),
};
Ok(SessionLlmIdentity {
model,
provider,
self_hosted_server_id,
provider_params,
auth_binding,
})
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct SessionLlmRequestPolicy {
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_params: Option<crate::lifecycle::run_primitive::ProviderParamsOverride>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_tool_defaults: Option<crate::lifecycle::run_primitive::ProviderTag>,
}
impl SessionMetadata {
pub fn llm_identity(&self) -> SessionLlmIdentity {
SessionLlmIdentity {
model: self.model.clone(),
provider: self.provider,
self_hosted_server_id: self.self_hosted_server_id.clone(),
provider_params: self.provider_params.clone(),
auth_binding: self.auth_binding.clone(),
}
}
pub fn apply_llm_identity(&mut self, identity: &SessionLlmIdentity) {
self.model = identity.model.clone();
self.provider = identity.provider;
self.self_hosted_server_id = identity.self_hosted_server_id.clone();
self.provider_params = identity.provider_params.clone();
self.auth_binding = identity.auth_binding.clone();
}
}
pub const SESSION_METADATA_KEY: &str = "session_metadata";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCategoryOverride {
#[default]
Inherit,
Enable,
Disable,
}
impl ToolCategoryOverride {
#[must_use]
pub fn resolve(self, runtime_default: bool) -> bool {
match self {
Self::Enable => true,
Self::Disable => false,
Self::Inherit => runtime_default,
}
}
#[must_use]
pub fn to_override(self) -> Option<bool> {
match self {
Self::Enable => Some(true),
Self::Disable => Some(false),
Self::Inherit => None,
}
}
#[must_use]
pub fn from_effective(enabled: bool) -> Self {
if enabled { Self::Enable } else { Self::Disable }
}
#[must_use]
pub fn from_override(value: Option<bool>) -> Self {
match value {
Some(true) => Self::Enable,
Some(false) => Self::Disable,
None => Self::Inherit,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub struct SessionTooling {
#[serde(default)]
pub builtins: ToolCategoryOverride,
#[serde(default)]
pub shell: ToolCategoryOverride,
#[serde(default)]
pub comms: ToolCategoryOverride,
#[serde(default)]
pub mob: ToolCategoryOverride,
#[serde(default)]
pub memory: ToolCategoryOverride,
#[serde(default)]
pub schedule: ToolCategoryOverride,
#[serde(default)]
pub workgraph: ToolCategoryOverride,
#[serde(default)]
pub image_generation: ToolCategoryOverride,
#[serde(default)]
pub web_search: ToolCategoryOverride,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_access_policy: Option<crate::ops::ToolAccessPolicy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_skills: Option<Vec<crate::skills::SkillKey>>,
}
impl From<&Session> for SessionMeta {
fn from(session: &Session) -> Self {
Self {
id: session.id.clone(),
created_at: session.created_at,
updated_at: session.updated_at,
message_count: session.messages.len(),
total_tokens: session.total_tokens(),
metadata: session.metadata.clone(),
}
}
}
pub fn try_session_metadata_from_map(
metadata: &serde_json::Map<String, serde_json::Value>,
) -> Result<Option<SessionMetadata>, serde_json::Error> {
let Some(value) = metadata.get(SESSION_METADATA_KEY) else {
return Ok(None);
};
let mut metadata = serde_json::from_value::<SessionMetadata>(value.clone())?;
metadata.schema_version =
session_persistence_version_authority::restore_session_metadata_schema_version(
metadata.schema_version,
)
.map_err(<serde_json::Error as serde::de::Error>::custom)?;
session_durable_config_authority::restore_session_metadata(metadata)
.map(Some)
.map_err(<serde_json::Error as serde::de::Error>::custom)
}
pub fn try_lifecycle_terminal_from_map(
metadata: &serde_json::Map<String, serde_json::Value>,
) -> Result<Option<SessionLifecycleTerminal>, serde_json::Error> {
match metadata.get(SESSION_LIFECYCLE_TERMINAL_KEY) {
Some(value) => serde_json::from_value(value.clone()).map(Some),
None => Ok(None),
}
}
#[derive(Debug, Clone)]
pub struct PersistedSessionMetadataView {
pub session_id: SessionId,
pub session_metadata: Option<SessionMetadata>,
pub lifecycle_terminal: Option<SessionLifecycleTerminal>,
}
impl PersistedSessionMetadataView {
pub fn try_from_metadata_map(
session_id: SessionId,
metadata: &serde_json::Map<String, serde_json::Value>,
) -> Result<Self, serde_json::Error> {
Ok(Self {
session_id,
session_metadata: try_session_metadata_from_map(metadata)?,
lifecycle_terminal: try_lifecycle_terminal_from_map(metadata)?,
})
}
pub fn try_from_session(session: &Session) -> Result<Self, serde_json::Error> {
Ok(Self {
session_id: session.id().clone(),
session_metadata: session.try_session_metadata()?,
lifecycle_terminal: session.try_lifecycle_terminal()?,
})
}
pub fn mob_member_binding(&self) -> Option<&crate::MobMemberBinding> {
self.session_metadata.as_ref()?.mob_member_binding.as_ref()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
#[test]
fn ordinary_append_graph_is_independent_of_validation_cache_state()
-> Result<(), Box<dyn std::error::Error>> {
fn seeded() -> Result<Session, Box<dyn std::error::Error>> {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("A".to_string())));
session.push(Message::User(UserMessage::text("B".to_string())));
session.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::User(UserMessage::text("B2".to_string()))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)?;
Ok(session)
}
let mut subject = seeded()?;
let mut control = subject.clone();
for index in 0..4 {
let message = Message::User(UserMessage::text(format!("append {index}")));
subject.push(message.clone());
let value = serde_json::to_value(
control
.transcript_history_state()?
.ok_or_else(|| std::io::Error::other("control history missing"))?,
)?;
control.set_metadata_unchecked_for_test(SESSION_TRANSCRIPT_HISTORY_STATE_KEY, value);
control.transcript_history_metadata_validation =
TranscriptHistoryMetadataValidation::RequiresValidation;
control.push(message);
let subject_state = subject
.transcript_history_state()?
.ok_or_else(|| std::io::Error::other("subject history missing"))?;
let control_state = control
.transcript_history_state()?
.ok_or_else(|| std::io::Error::other("control history missing"))?;
assert_eq!(
subject_state.head(),
control_state.head(),
"head at {index}"
);
assert_eq!(
subject_state.commits().collect::<Vec<_>>(),
control_state.commits().collect::<Vec<_>>(),
"commits at {index}"
);
let project = |state: &TranscriptHistoryState| {
let mut bodies = state
.materialize_revision_bodies()
.expect("audited bodies should materialize")
.into_iter()
.map(|body| (body.revision, body.messages))
.collect::<Vec<_>>();
bodies.sort_by(|left, right| left.0.cmp(&right.0));
bodies
};
assert_eq!(
project(&subject_state),
project(&control_state),
"retained bodies at {index}"
);
subject.validate_transcript_history_state()?;
}
Ok(())
}
use super::*;
use crate::realtime_transcript::RealtimeTranscriptRole;
use crate::types::{
AssistantBlock, BlockAssistantMessage, ContentBlock, StopReason, SystemMessage, Usage,
UserMessage,
};
use std::sync::Arc;
fn rewrite_record_at(
state: &TranscriptHistoryState,
edge_index: usize,
) -> TranscriptRewriteRecord {
let commit = state
.commit(edge_index)
.unwrap_or_else(|| panic!("rewrite occurrence {edge_index} should exist"))
.clone();
let parent_body = state
.materialize_occurrence_parent(edge_index)
.unwrap_or_else(|error| {
panic!("rewrite occurrence {edge_index} parent should materialize: {error}")
});
let revision_body = state
.materialize_occurrence_child(edge_index)
.unwrap_or_else(|error| {
panic!("rewrite occurrence {edge_index} child should materialize: {error}")
});
TranscriptRewriteRecord::new(commit, parent_body, revision_body).unwrap_or_else(|error| {
panic!("rewrite occurrence {edge_index} should validate: {error}")
})
}
fn replace_all_bytes(haystack: &mut Vec<u8>, needle: &[u8], replacement: &[u8]) -> usize {
let mut replaced = 0;
let mut cursor = 0;
while let Some(offset) = haystack[cursor..]
.windows(needle.len())
.position(|candidate| candidate == needle)
{
let start = cursor + offset;
haystack.splice(start..start + needle.len(), replacement.iter().copied());
cursor = start + replacement.len();
replaced += 1;
}
replaced
}
fn released_0810_transcript_messages_digest_with_raw_spelling(
messages: &[Message],
canonical_raw: &str,
released_raw: &str,
) -> String {
let canonical = canonicalize_released_0810_messages_for_digest(messages);
let mut bytes = serde_json::to_vec(&canonical).expect("released digest form serializes");
replace_all_bytes(
&mut bytes,
canonical_raw.as_bytes(),
released_raw.as_bytes(),
);
format!("sha256:{:x}", Sha256::digest(bytes))
}
fn released_0810_document(
session: &Session,
head: String,
revisions: Vec<TranscriptRevisionBody>,
) -> serde_json::Value {
let state = session
.transcript_history_state()
.expect("current history should decode")
.expect("current history should exist");
let mut commits = serde_json::to_value(state.commits().cloned().collect::<Vec<_>>())
.expect("commits should serialize");
for commit in commits.as_array_mut().expect("commit vector") {
let fields = commit.as_object_mut().expect("commit object");
fields.remove("rewrite_generation");
let selection = fields
.get_mut("selection")
.and_then(serde_json::Value::as_object_mut)
.expect("selection object");
if matches!(
selection.get("type").and_then(serde_json::Value::as_str),
Some("edit_message_range" | "compaction_message_range")
) {
let range = selection
.remove("range")
.and_then(|value| value.as_object().cloned())
.expect("typed range object");
*selection = serde_json::Map::from_iter([
(
"type".to_string(),
serde_json::Value::String("message_range".to_string()),
),
(
"start".to_string(),
range.get("start").cloned().expect("range start"),
),
(
"end".to_string(),
range.get("end").cloned().expect("range end"),
),
]);
}
}
let mut document = serde_json::to_value(session).expect("current session should serialize");
document["version"] = serde_json::json!(2);
let metadata = document["metadata"]
.as_object_mut()
.expect("metadata object");
metadata.remove(SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY);
metadata.insert(
SESSION_TRANSCRIPT_HISTORY_STATE_KEY.to_string(),
serde_json::json!({
"head": head,
"commits": commits,
"revisions": revisions,
"digest_format": TRANSCRIPT_DIGEST_FORMAT_RELEASED_0810,
}),
);
document
}
fn transient_context(text: &str) -> TurnRequestContext {
TurnRequestContext::new(text.to_string()).expect("non-empty transient context")
}
async fn wait_for_transient_boundary_request(handle: &TransientTurnContextStateHandle) {
for _ in 0..1_000 {
let registered = matches!(
&handle.boundary.lock().window,
TransientTurnContextBoundaryWindow::Open {
request: Some(_),
..
}
);
if registered {
return;
}
tokio::task::yield_now().await;
}
panic!("transient boundary request did not register");
}
#[test]
fn prepared_transient_boundary_authority_is_send() {
fn assert_send<T: Send>() {}
assert_send::<PreparedTransientTurnContextBoundary>();
assert_send::<crate::lifecycle::CoreBoundaryStageOutput>();
}
#[tokio::test]
async fn transient_boundary_runner_first_consumes_no_context() {
let state = TransientTurnContextStateHandle::new();
let run_id = RunId::new();
let _guard = state
.begin_boundary_run(run_id.clone())
.expect("open boundary");
let contexts = state
.take_pending_at_exact_boundary(&run_id)
.await
.expect("consume empty boundary");
assert!(contexts.is_empty());
let error = state
.prepare_active_turn_boundary(&run_id, vec![transient_context("late")])
.await
.expect_err("runner-first boundary is closed");
assert!(error.is_unavailable());
}
#[tokio::test]
async fn transient_boundary_prepare_commit_publishes_exact_order_once() {
let state = TransientTurnContextStateHandle::new();
let run_id = RunId::new();
let _guard = state
.begin_boundary_run(run_id.clone())
.expect("open boundary");
let prepare_state = state.clone();
let prepare_run_id = run_id.clone();
let prepare = tokio::spawn(async move {
prepare_state
.prepare_active_turn_boundary(
&prepare_run_id,
vec![transient_context(" first "), transient_context("second")],
)
.await
});
wait_for_transient_boundary_request(&state).await;
let runner_state = state.clone();
let runner_run_id = run_id.clone();
let runner = tokio::spawn(async move {
runner_state
.take_pending_at_exact_boundary(&runner_run_id)
.await
});
let prepared = prepare
.await
.expect("prepare task")
.expect("parked preparation");
prepared
.into_stage_output(None)
.commit()
.expect("publish transient context");
let contexts = runner.await.expect("runner task").expect("runner consume");
assert_eq!(
contexts
.iter()
.map(TurnRequestContext::as_str)
.collect::<Vec<_>>(),
vec![" first ", "second"]
);
}
#[tokio::test]
async fn transient_boundary_prepare_abort_releases_runner_without_context() {
let state = TransientTurnContextStateHandle::new();
let run_id = RunId::new();
let _guard = state
.begin_boundary_run(run_id.clone())
.expect("open boundary");
let prepare_state = state.clone();
let prepare_run_id = run_id.clone();
let prepare = tokio::spawn(async move {
prepare_state
.prepare_active_turn_boundary(
&prepare_run_id,
vec![transient_context("must not publish")],
)
.await
});
wait_for_transient_boundary_request(&state).await;
let runner_state = state.clone();
let runner_run_id = run_id.clone();
let runner = tokio::spawn(async move {
runner_state
.take_pending_at_exact_boundary(&runner_run_id)
.await
});
let prepared = prepare
.await
.expect("prepare task")
.expect("parked preparation");
prepared
.into_stage_output(None)
.abort()
.expect("abort transient context");
assert!(
runner
.await
.expect("runner task")
.expect("runner released")
.is_empty()
);
}
fn block_assistant_text(message: &BlockAssistantMessage) -> String {
message
.blocks
.iter()
.filter_map(|block| match block {
AssistantBlock::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect()
}
fn append_staged_user_image(
session: &mut Session,
event: &RealtimeTranscriptEvent,
) -> RealtimeTranscriptApplyOutcome {
let RealtimeTranscriptEvent::UserContentFinal {
idempotency_key,
item_id,
previous_item_id,
content_index,
content,
} = event
else {
panic!("test helper requires user content final")
};
let [ContentBlock::Image { media_type, data }] = content.as_slice() else {
panic!("test helper requires exactly one image")
};
let media_type = crate::image_generation::MediaType::canonical_str(media_type);
let blob_id = match data {
crate::types::ImageData::Inline { data } => {
crate::blob::content_blob_id(&media_type, data)
}
crate::types::ImageData::Blob { blob_id } => blob_id.clone(),
};
let pending = crate::PendingRealtimeUserContentBlob {
idempotency_key: idempotency_key.clone(),
item_id: item_id.clone(),
previous_item_id: previous_item_id.clone(),
content_index: *content_index,
blob_id,
media_type,
};
assert_eq!(
session
.stage_pending_realtime_user_content_blob(pending.clone())
.expect("test pending anchor should stage"),
crate::generated::session_document::RealtimeUserContentBlobStageDisposition::StageNew
);
session.append_realtime_transcript_event(pending.canonical_event())
}
#[test]
fn transcript_digest_is_content_addressed() {
let base_time = crate::types::message_timestamp_now();
let stamped = vec![
Message::User(UserMessage::text("turn one".to_string())),
Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "answer one".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity {
interaction_id: None,
run_id: Some(crate::lifecycle::RunId::new()),
objective_id: None,
},
created_at: base_time,
}),
];
let mut restamped = stamped.clone();
for message in &mut restamped {
match message {
Message::User(user) => {
user.created_at = base_time + chrono::Duration::hours(2);
}
Message::BlockAssistant(assistant) => {
assistant.identity = crate::types::TranscriptMessageIdentity {
interaction_id: None,
run_id: Some(crate::lifecycle::RunId::new()),
objective_id: None,
};
assistant.created_at = base_time + chrono::Duration::hours(2);
}
_ => {}
}
}
assert_eq!(
transcript_messages_digest(&stamped).expect("digest"),
transcript_messages_digest(&restamped).expect("digest"),
"bookkeeping variance must not fork the transcript revision"
);
let mut content_changed = stamped.clone();
if let Message::User(user) = &mut content_changed[0] {
user.content = vec![ContentBlock::Text {
text: "a different turn".to_string(),
}];
}
assert_ne!(
transcript_messages_digest(&stamped).expect("digest"),
transcript_messages_digest(&content_changed).expect("digest"),
"content changes must fork the transcript revision"
);
}
#[test]
fn public_generic_rewrite_api_rejects_typed_compaction_semantic() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("old context")));
let error = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::typed_compaction_for_test(0, 1),
vec![Message::User(UserMessage::compaction_summary("summary"))],
TranscriptRewriteReason::new("anything"),
None,
None,
)
.unwrap_err();
assert!(matches!(
error,
TranscriptEditError::InvalidTranscriptShape(_)
));
assert_eq!(session.messages().len(), 1);
}
#[test]
fn compaction_witness_authorizes_only_the_exact_validated_rebuild() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("old context one")));
session.push(Message::User(UserMessage::text("old context two")));
let validated = vec![Message::User(UserMessage::compaction_summary(
"validated summary",
))];
let authority = crate::agent::compact::ValidatedCompactionRewrite::for_test(
session.messages(),
&validated,
)
.unwrap();
let error = session
.replace_messages_for_compaction_internal(
vec![Message::User(UserMessage::compaction_summary(
"substituted summary",
))],
&authority,
)
.unwrap_err();
assert!(matches!(
error,
TranscriptEditError::InvalidTranscriptShape(_)
));
assert_eq!(session.messages().len(), 2);
}
#[test]
fn partial_span_rewrite_records_span_digests_not_whole_document_digests() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("m-0")));
session.push(Message::User(UserMessage::text("m-1")));
session.push(Message::User(UserMessage::text("m-2")));
let original = session.messages().to_vec();
let whole_before = session.transcript_content_digest().unwrap();
let replacement = vec![Message::User(UserMessage::text("m-1-rewritten"))];
let commit = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
replacement.clone(),
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.unwrap();
assert_eq!(
commit.original_span_digest,
transcript_messages_digest(&original[1..2]).unwrap(),
"partial-span original digest must cover exactly the selected span"
);
assert_ne!(commit.original_span_digest, whole_before);
assert_eq!(
commit.replacement_digest,
transcript_messages_digest(&replacement).unwrap(),
"partial-span replacement digest must cover exactly the replacement"
);
assert_ne!(commit.replacement_digest, commit.revision);
let state = session.transcript_history_state().unwrap().unwrap();
validate_transcript_history_state(&state).unwrap();
}
#[test]
fn synthetic_notice_refresh_inside_audited_prefix_fails_before_mutation() {
use crate::types::{SystemNoticeKind, SystemNoticeMessage};
let mut session = Session::new();
session.push(Message::User(UserMessage::text("u-0")));
session.push(Message::User(UserMessage::text("u-1")));
session
.replace_synthetic_notices(
SystemNoticeKind::McpPending,
vec![Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"pending v1",
))],
)
.expect("initial notice install");
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("u-0-rewritten"))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.expect("first rewrite commits");
session.push(Message::User(UserMessage::text("u-2")));
let before = session.messages().to_vec();
let error = session
.replace_synthetic_notices(
SystemNoticeKind::McpPending,
vec![Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"pending v2",
))],
)
.expect_err("refresh must not rewrite an audited prefix");
assert!(
matches!(error, TranscriptEditError::InvalidTranscriptShape(_)),
"expected the atomic audited-prefix refusal, got: {error:?}"
);
assert_eq!(session.messages(), before.as_slice());
let bytes = serde_json::to_vec(&session).expect("session serializes");
let decoded: Session = serde_json::from_slice(&bytes)
.expect("the failed rewrite must leave a graph every cold reader accepts");
assert_eq!(decoded.messages().len(), session.messages().len());
}
#[test]
fn current_envelope_rejects_non_append_live_rows_at_ingress() {
let mut session = Session::new();
session.append_system_message("original system");
session.push(Message::User(UserMessage::text("m-1")));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::User(UserMessage::text("m-1-rewritten"))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.expect("seed rewrite commits");
session.push(Message::User(UserMessage::text("m-2")));
let mut document = serde_json::to_value(&session).expect("session serializes");
let divergent_message =
serde_json::to_value(Message::System(SystemMessage::new("replacement system")))
.expect("message serializes");
let messages = document
.get_mut("messages")
.and_then(serde_json::Value::as_array_mut)
.expect("messages array");
messages[0] = divergent_message;
let error = serde_json::from_value::<Session>(document)
.expect_err("a current non-append live tail must fail closed at ingress");
assert!(
error
.to_string()
.contains("live transcript does not preserve the graph-proved audited endpoint"),
"unexpected error: {error}"
);
}
#[test]
fn current_rewrite_refuses_non_append_parent_divergence() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("original row")));
session.push(Message::User(UserMessage::text("question")));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::User(UserMessage::text("edited question"))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.expect("seed audited endpoint");
let generation_before = session
.transcript_rewrite_generation()
.expect("rewrite generation");
let mut divergent = session.messages().to_vec();
divergent[0] = Message::User(UserMessage::text("replacement row"));
session.messages.replace(divergent);
let current_prefix =
crate::SessionMessageRowPrefixAccumulator::from_messages(session.messages())
.expect("current row prefix");
assert!(session.install_exact_message_row_prefix(current_prefix));
let error = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::User(UserMessage::text("edited again"))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.expect_err("current writer must not infer a non-append parent splice");
assert!(
matches!(error, TranscriptEditError::HistoryStateMalformed(ref message)
if message.contains("not an exact audited append")),
"unexpected error: {error}"
);
assert_eq!(
session
.transcript_rewrite_generation()
.expect("rewrite generation"),
generation_before,
"failed current write must not append a graph edge"
);
}
#[test]
fn semantic_marker_prevents_new_generic_compaction_forgery_and_heals_prior_data() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("old context one")));
session.push(Message::User(UserMessage::text("old context two")));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 2 },
vec![Message::User(UserMessage::compaction_summary("summary"))],
TranscriptRewriteReason::new("compaction"),
None,
None,
)
.unwrap();
let session: Session =
serde_json::from_value(serde_json::to_value(&session).unwrap()).unwrap();
let history = session.transcript_history_state().unwrap().unwrap();
let current_commit = history.commit(0).expect("current rewrite commit");
assert_eq!(
current_commit.selection.semantic(),
TranscriptRewriteSemantic::Edit,
"new generic rewrites retain an explicit typed edit marker after roundtrip"
);
assert_eq!(current_commit.reason.kind, "compaction");
let mut legacy_value =
serde_json::to_value(rewrite_record_at(&history, 0)).expect("record wire");
let legacy = legacy_value.as_object_mut().expect("record object");
legacy.remove("digest_format");
legacy.get_mut("commit").expect("legacy commit")["selection"] = serde_json::json!({
"type": "message_range",
"start": 0,
"end": 2,
});
let legacy: TranscriptRewriteRecord =
serde_json::from_value(legacy_value).expect("legacy record should heal");
assert_eq!(
legacy.commit.selection.semantic(),
TranscriptRewriteSemantic::Compaction,
"marker-absent prior data derives compaction from typed transcript evidence"
);
let mut ordinary = Session::new();
ordinary.push(Message::User(UserMessage::text("ordinary old one")));
ordinary.push(Message::User(UserMessage::text("ordinary old two")));
ordinary
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 2 },
vec![Message::User(UserMessage::text("ordinary replacement"))],
TranscriptRewriteReason::new("compaction"),
None,
None,
)
.unwrap();
let history = ordinary.transcript_history_state().unwrap().unwrap();
assert_eq!(
history
.commit(0)
.expect("ordinary rewrite commit")
.selection
.semantic(),
TranscriptRewriteSemantic::Edit,
"free-form reason must not upgrade an ordinary edit"
);
}
#[test]
fn snapshot_compaction_returns_the_proof_of_the_installed_graph() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("seam proof before")));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("seam proof after"))],
TranscriptRewriteReason::new("edit"),
None,
None,
)
.unwrap();
let document = serde_json::to_value(&session).unwrap();
let mut metadata = serde_json::Map::new();
metadata.insert(
SESSION_TRANSCRIPT_HISTORY_STATE_KEY.to_string(),
document["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY].clone(),
);
let graph_wire = metadata[SESSION_TRANSCRIPT_HISTORY_STATE_KEY].clone();
let sealed = compact_transcript_history_metadata_for_snapshot(&mut metadata)
.expect("valid graph compacts")
.expect("graph value present");
assert_eq!(
serde_json::to_value(sealed.as_ref()).unwrap(),
graph_wire,
"the returned proof must cover exactly the consumed graph value"
);
assert!(
!metadata.contains_key(SESSION_TRANSCRIPT_HISTORY_STATE_KEY),
"the transient wire graph must not remain beside the typed authority"
);
let mut empty = serde_json::Map::new();
assert!(
compact_transcript_history_metadata_for_snapshot(&mut empty)
.expect("empty metadata compacts")
.is_none()
);
}
#[test]
fn decode_seeds_shared_transcript_graph_with_the_proven_parse() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("seed shared parse")));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("seed shared parse two"))],
TranscriptRewriteReason::new("edit"),
None,
None,
)
.unwrap();
let document = serde_json::to_value(&session).unwrap();
let serialized_graph = document["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY].clone();
let decoded: Session = serde_json::from_value(document).unwrap();
let seeded = decoded
.history_caches
.shared_state
.get()
.expect("decode must seed the shared graph parse");
assert_eq!(
serde_json::to_value(&*seeded).unwrap(),
serialized_graph,
"the seeded graph must be the value the wire carried"
);
assert!(
!decoded
.metadata
.contains_key(SESSION_TRANSCRIPT_HISTORY_STATE_KEY),
"the typed graph cache is the singular in-memory authority"
);
let sealed = decoded
.validated_transcript_history_state()
.expect("validated read")
.expect("graph present");
assert!(
std::sync::Arc::ptr_eq(&seeded, &sealed.shared()),
"validated_transcript_history_state must serve the decode-seeded parse"
);
let bare: Session =
serde_json::from_value(serde_json::to_value(Session::new()).unwrap()).unwrap();
assert!(bare.history_caches.shared_state.get().is_none());
}
#[test]
fn sealed_transcript_history_refuses_unverified_corrupt_graph() {
let mut source = Session::new();
source.push(Message::User(UserMessage::text("hello".to_string())));
source
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("hello again".to_string()))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.expect("consistent rewrite should commit");
let mut source_document = serde_json::to_value(&source).expect("source serializes");
source_document["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]["anchor"]["messages"]
[0] = serde_json::to_value(Message::User(UserMessage::text("tampered".to_string())))
.expect("tampered message");
assert!(
serde_json::from_value::<Session>(source_document).is_err(),
"decode must not mint a proof for a digest-inconsistent transcript graph"
);
}
#[test]
fn sealed_transcript_history_verifies_and_seals_consistent_graph() {
let mut source = Session::new();
source.push(Message::User(UserMessage::text("hello".to_string())));
let commit = source
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("hello again".to_string()))],
TranscriptRewriteReason::new("unit-test"),
Some("unit-test".to_string()),
None,
)
.expect("consistent rewrite should commit");
let source_document = serde_json::to_value(&source).expect("source serializes");
let session: Session =
serde_json::from_value(source_document).expect("consistent graph decodes");
let sealed = session
.validated_transcript_history_state()
.expect("a consistent graph must seal")
.expect("history metadata is present");
assert_eq!(sealed.state().head(), commit.revision);
}
#[test]
fn replace_synthetic_notices_leaves_only_replacements_of_kind() {
use crate::types::{SystemNoticeKind, SystemNoticeMessage};
let mut session = Session::new();
session.push(Message::User(UserMessage::text("hello".to_string())));
session.push(Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"stale one",
)));
session.push(Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"stale two",
)));
session.push(Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::BackgroundJob,
"other-kind",
)));
session
.replace_synthetic_notices(
SystemNoticeKind::McpPending,
vec![Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"fresh",
))],
)
.expect("notice refresh succeeds");
let mcp_pending: Vec<&SystemNoticeMessage> = session
.messages()
.iter()
.filter_map(|message| match message {
Message::SystemNotice(notice) if notice.kind == SystemNoticeKind::McpPending => {
Some(notice)
}
_ => None,
})
.collect();
assert_eq!(mcp_pending.len(), 1, "exactly one notice of the kind");
assert_eq!(mcp_pending[0].body.as_deref(), Some("fresh"));
assert!(
session.messages().iter().any(|message| matches!(
message,
Message::SystemNotice(notice) if notice.kind == SystemNoticeKind::BackgroundJob
)),
"other-kind notices are untouched"
);
session
.replace_synthetic_notices(SystemNoticeKind::McpPending, Vec::new())
.expect("pure strip succeeds");
assert!(
!session.messages().iter().any(|message| matches!(
message,
Message::SystemNotice(notice) if notice.kind == SystemNoticeKind::McpPending
)),
"empty replacement clears the kind"
);
}
#[test]
fn ordinary_appends_after_rewrite_leave_audited_graph_untouched() {
let mut session = Session::new();
for message in 0..133 {
session.push(Message::User(UserMessage::text(format!(
"seed message {message}"
))));
}
let parent = session.transcript_revision().expect("parent revision");
let commit = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange {
start: 132,
end: 133,
},
vec![Message::User(UserMessage::text("edited question"))],
TranscriptRewriteReason::new("unit-test-edit"),
Some("unit-test".to_string()),
Some(parent),
)
.expect("rewrite should commit");
let graph_before = session
.validated_transcript_history_state()
.expect("history validation")
.expect("rewrite graph");
for turn in 0..762 {
session.push(Message::User(UserMessage::text(format!("turn {turn}"))));
}
let graph_after = session
.validated_transcript_history_state()
.expect("history validation")
.expect("rewrite graph");
assert!(
graph_before.shares_exact_state_with(&graph_after),
"ordinary appends must preserve the exact audited graph authority"
);
let state = session
.transcript_history_state()
.expect("history state should decode")
.expect("rewrite should create history state");
assert_eq!(session.messages().len(), 895);
assert_eq!(state.commit_count(), 1, "ordinary appends are not rewrites");
let retained_bodies = state
.materialize_revision_bodies()
.expect("audited bodies should materialize");
assert_eq!(
retained_bodies.len(),
2,
"one real rewrite retains only its two audited endpoints"
);
assert_eq!(state.head(), commit.revision);
assert_ne!(
session.transcript_revision().expect("live revision"),
state.head(),
"the live append tail is Session authority, not a mechanical graph head"
);
let retained_message_entries = retained_bodies
.iter()
.map(|body| body.messages.len())
.sum::<usize>();
assert!(retained_message_entries <= 2 * session.messages().len());
let live_bytes = serde_json::to_vec(session.messages())
.expect("live transcript should serialize")
.len();
let snapshot_bytes = serde_json::to_vec(&session)
.expect("session snapshot should serialize")
.len();
assert!(
snapshot_bytes <= live_bytes.saturating_mul(5).saturating_add(64 * 1024),
"snapshot must remain linear in the live transcript: {snapshot_bytes} bytes for {live_bytes} live bytes"
);
}
#[test]
fn repeated_synthetic_notice_refreshes_do_not_mint_rewrite_commits() {
use crate::types::{SystemNoticeKind, SystemNoticeMessage};
let mut session = Session::new();
session.push(Message::User(UserMessage::text("before".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("after".to_string()))],
TranscriptRewriteReason::new("unit-test-edit"),
Some("unit-test".to_string()),
None,
)
.expect("seed rewrite");
let graph_before = session
.validated_transcript_history_state()
.expect("history validation")
.expect("audited graph");
for refresh in 0..64 {
session
.replace_synthetic_notices(
SystemNoticeKind::McpPending,
vec![Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
format!("refresh {refresh}"),
))],
)
.expect("mechanical refresh");
}
let graph_after = session
.validated_transcript_history_state()
.expect("history validation")
.expect("audited graph");
assert!(
graph_before.shares_exact_state_with(&graph_after),
"tail-only synthetic refreshes must preserve the exact audited graph authority"
);
let state = session
.transcript_history_state()
.expect("history state")
.expect("seed rewrite history");
assert_eq!(state.commit_count(), 1);
assert_eq!(session.transcript_rewrite_generation().unwrap(), 1);
assert_eq!(
state
.materialize_revision_bodies()
.expect("audited bodies should materialize")
.len(),
2,
"mechanical refreshes do not mint retained live-head bodies"
);
}
#[test]
fn snapshot_compaction_does_not_launder_corrupt_old_body() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("seed".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("rewritten".to_string()))],
TranscriptRewriteReason::new("unit-test-edit"),
Some("unit-test".to_string()),
None,
)
.expect("seed rewrite");
let state = session
.transcript_history_state()
.expect("state")
.expect("history");
let mut state = serde_json::to_value(&state).expect("current history value");
state["anchor"]["messages"][0] =
serde_json::to_value(Message::User(UserMessage::text("tampered".to_string())))
.expect("tampered message");
session.set_metadata_unchecked_for_test(SESSION_TRANSCRIPT_HISTORY_STATE_KEY, state);
assert!(
serde_json::to_vec(&session).is_err(),
"serialization must fail before pruning a corrupt old body"
);
}
#[test]
fn released_0_8_10_mechanical_history_is_compacted_at_import_boundary() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("seed".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("rewritten".to_string()))],
TranscriptRewriteReason::new("unit-test-edit"),
Some("unit-test".to_string()),
None,
)
.expect("seed rewrite");
let state = session
.transcript_history_state()
.expect("state")
.expect("history");
let mut revisions = state
.materialize_revision_bodies()
.expect("released audit bodies should materialize");
let mut parent = state.head().to_string();
for index in 0..8 {
session.push(Message::User(UserMessage::text(format!(
"0.8.10 ordinary append {index}"
))));
let revision = transcript_messages_digest(session.messages()).expect("revision digest");
revisions.push(TranscriptRevisionBody {
revision: revision.clone(),
parent_revision: Some(parent),
messages: session.messages().to_vec(),
created_at: SystemTime::now(),
});
parent = revision;
}
let released = released_0810_document(&session, parent, revisions);
let released = serde_json::to_vec(&released).expect("released document bytes");
let imported =
import_released_0810_session(&released).expect("released document should import");
assert_eq!(
imported.receipt().evidence(),
Released0810ImportEvidence::StoreAuthorizationRequired
);
let compact = imported
.session()
.transcript_history_state()
.expect("imported history")
.expect("imported graph");
assert_eq!(
compact
.materialize_revision_bodies()
.expect("compact bodies should materialize")
.len(),
2,
"import boundary should retain only the two audited endpoints"
);
assert_eq!(
compact.head(),
compact.last_commit().expect("rewrite commit").revision
);
validate_transcript_history_state(&compact).expect("compacted history remains valid");
}
#[test]
fn released_0_8_10_lost_raw_json_spelling_is_authorized_then_rebound_once() {
let opaque = r#"{"z":1,"a":{"y":2,"x":3}}"#;
let canonical_opaque = r#"{"a":{"x":3,"y":2},"z":1}"#;
let mut session = Session::new();
session.append_system_message("system".to_string());
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::ToolUse {
id: "tool-1".to_string(),
name: "opaque".to_string(),
args: serde_json::value::RawValue::from_string(opaque.to_string())
.expect("valid tool args"),
meta: None,
}],
StopReason::ToolUse,
)));
session.push(Message::tool_results(vec![
crate::types::ToolResult::with_blocks(
"tool-1".to_string(),
vec![ContentBlock::Structured {
data: serde_json::value::RawValue::from_string(opaque.to_string())
.expect("valid structured result"),
}],
false,
),
]));
session.push(Message::User(UserMessage::text("tail".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 2, end: 3 },
vec![Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::ToolUse {
id: "tool-1".to_string(),
name: "opaque-revised".to_string(),
args: serde_json::value::RawValue::from_string(opaque.to_string())
.expect("valid revised tool args"),
meta: None,
}],
StopReason::ToolUse,
))],
TranscriptRewriteReason::new("released-rich-content"),
Some("unit-test".to_string()),
None,
)
.expect("seed current rewrite");
let state = session
.transcript_history_state()
.expect("current state")
.expect("current graph");
let revisions = state
.materialize_revision_bodies()
.expect("released bodies materialize");
let released_ids = revisions
.iter()
.map(|body| {
(
body.revision.clone(),
released_0810_transcript_messages_digest_with_raw_spelling(
&body.messages,
canonical_opaque,
opaque,
),
)
})
.collect::<std::collections::BTreeMap<_, _>>();
assert!(
released_ids
.iter()
.any(|(current, released)| current != released),
"fixture must reproduce a real pre-buffer format-2 identity mismatch"
);
let released_span_ids = (0..state.commit_count())
.map(|index| {
let record = rewrite_record_at(&state, index);
let (start, end) = record.commit.selection.bounds();
let removed = end - start;
let retained = record.commit.messages_before - removed;
let replacement_len = record.commit.messages_after - retained;
(
released_0810_transcript_messages_digest_with_raw_spelling(
&record.parent_body.messages[start..end],
canonical_opaque,
opaque,
),
released_0810_transcript_messages_digest_with_raw_spelling(
&record.revision_body.messages[start..start + replacement_len],
canonical_opaque,
opaque,
),
)
})
.collect::<Vec<_>>();
for (index, (released_original, released_replacement)) in
released_span_ids.iter().enumerate()
{
let record = rewrite_record_at(&state, index);
assert_ne!(
released_original, &record.commit.original_span_digest,
"fixture must reproduce a real pre-buffer format-2 original-span mismatch"
);
assert_ne!(
released_replacement, &record.commit.replacement_digest,
"fixture must reproduce a real pre-buffer format-2 replacement-span mismatch"
);
}
let mut released = released_0810_document(&session, state.head().to_string(), revisions);
let history = released["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]
.as_object_mut()
.expect("released history object");
let revisions = history["revisions"]
.as_array_mut()
.expect("released revision vector");
for body in revisions {
body["revision"] = serde_json::Value::String(
released_ids
.get(body["revision"].as_str().expect("body revision"))
.expect("body remap")
.clone(),
);
if let Some(parent) = body["parent_revision"].as_str() {
body["parent_revision"] = serde_json::Value::String(
released_ids.get(parent).expect("parent remap").clone(),
);
}
}
history["head"] = serde_json::Value::String(
released_ids
.get(history["head"].as_str().expect("released head"))
.expect("head remap")
.clone(),
);
for (index, commit) in history["commits"]
.as_array_mut()
.expect("released commits")
.iter_mut()
.enumerate()
{
commit["parent_revision"] = serde_json::Value::String(
released_ids
.get(commit["parent_revision"].as_str().expect("commit parent"))
.expect("commit parent remap")
.clone(),
);
commit["revision"] = serde_json::Value::String(
released_ids
.get(commit["revision"].as_str().expect("commit revision"))
.expect("commit revision remap")
.clone(),
);
commit["original_span_digest"] =
serde_json::Value::String(released_span_ids[index].0.clone());
commit["replacement_digest"] =
serde_json::Value::String(released_span_ids[index].1.clone());
}
let mut inconsistent = released.clone();
inconsistent["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]["commits"][0]["messages_before"] =
serde_json::json!(999);
let inconsistent = serde_json::to_vec(&inconsistent).expect("inconsistent released bytes");
assert!(
import_released_0810_session(&inconsistent).is_err(),
"source authorization must not launder contradictory commit/body topology"
);
let released = serde_json::to_vec(&released).expect("released bytes");
let imported = import_released_0810_session(&released)
.expect("store-authorized 0.8.10 lost-spelling graph imports");
assert_eq!(
imported.receipt().evidence(),
Released0810ImportEvidence::StoreAuthorizationRequired
);
let imported_state = imported
.session()
.transcript_history_state()
.expect("imported state")
.expect("imported graph");
assert_eq!(
imported_state.digest_format(),
transcript_history::graph::TRANSCRIPT_DIGEST_FORMAT_CURRENT
);
for body in imported_state
.materialize_revision_bodies()
.expect("current bodies materialize")
{
assert_eq!(
body.revision,
transcript_messages_digest(&body.messages).expect("current body digest"),
"every released format-2 label must be replaced by current identity"
);
}
let current = serde_json::to_vec(imported.session()).expect("current session serializes");
let restored: Session =
serde_json::from_slice(¤t).expect("current session round-trips");
assert_eq!(
restored.transcript_revision().expect("restored revision"),
imported
.session()
.transcript_revision()
.expect("imported revision")
);
}
#[test]
fn released_0_8_10_collapsed_raw_json_rewrite_is_squashed_and_tail_rebased() {
let canonical_raw = r#"{"a":2,"z":1}"#;
let released_raw = r#"{"z":1,"a":2}"#;
let tool_message = |name: &str, raw: &str| {
Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::ToolUse {
id: "tool-1".to_string(),
name: name.to_string(),
args: serde_json::value::RawValue::from_string(raw.to_string())
.expect("valid tool args"),
meta: None,
}],
StopReason::ToolUse,
))
};
let parent_messages = vec![tool_message("opaque", released_raw)];
let collapsed_messages = vec![tool_message("opaque", canonical_raw)];
let final_messages = vec![tool_message("opaque-revised", released_raw)];
let old_parent = released_0810_transcript_messages_digest_with_raw_spelling(
&parent_messages,
canonical_raw,
released_raw,
);
let old_collapsed = released_0810_transcript_messages_digest_with_raw_spelling(
&collapsed_messages,
canonical_raw,
canonical_raw,
);
let old_final = released_0810_transcript_messages_digest_with_raw_spelling(
&final_messages,
canonical_raw,
released_raw,
);
assert_ne!(old_parent, old_collapsed);
assert_eq!(
transcript_messages_digest(&parent_messages).expect("current parent digest"),
transcript_messages_digest(&collapsed_messages).expect("current collapsed digest"),
"only released RawValue spelling may distinguish the squashed endpoints"
);
let body = |revision: String, parent_revision: Option<String>, messages: Vec<Message>| {
TranscriptRevisionBody {
revision,
parent_revision,
messages,
created_at: SystemTime::UNIX_EPOCH,
}
};
let parent_body = body(old_parent.clone(), None, parent_messages.clone());
let collapsed_body = body(
old_collapsed.clone(),
Some(old_parent.clone()),
collapsed_messages.clone(),
);
let final_body = body(
old_final.clone(),
Some(old_collapsed.clone()),
final_messages.clone(),
);
let commit = |parent_revision: String,
revision: String,
original_span_digest: String,
replacement_digest: String| TranscriptRewriteCommit {
rewrite_generation: 0,
parent_revision,
revision,
selection: TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
original_span_digest,
replacement_digest,
messages_before: 1,
messages_after: 1,
reason: TranscriptRewriteReason::new("released-rich-content"),
actor: Some("unit-test".to_string()),
committed_at: SystemTime::UNIX_EPOCH,
};
let collapsed_commit = commit(
old_parent,
old_collapsed.clone(),
released_0810_transcript_messages_digest_with_raw_spelling(
&parent_messages,
canonical_raw,
released_raw,
),
released_0810_transcript_messages_digest_with_raw_spelling(
&collapsed_messages,
canonical_raw,
canonical_raw,
),
);
let final_commit = commit(
old_collapsed.clone(),
old_final.clone(),
released_0810_transcript_messages_digest_with_raw_spelling(
&collapsed_messages,
canonical_raw,
canonical_raw,
),
released_0810_transcript_messages_digest_with_raw_spelling(
&final_messages,
canonical_raw,
released_raw,
),
);
let mut session = Session::new();
session.push(final_messages[0].clone());
let mut released = serde_json::to_value(&session).expect("released session serializes");
released["version"] = serde_json::json!(2);
released["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY] = serde_json::json!({
"head": old_final,
"commits": [collapsed_commit, final_commit],
"revisions": [parent_body, collapsed_body, final_body],
"digest_format": TRANSCRIPT_DIGEST_FORMAT_RELEASED_0810,
});
for invalid_format in [None, Some(1), Some(3)] {
let mut invalid = released.clone();
match invalid_format {
Some(format) => {
invalid["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]["digest_format"] =
serde_json::json!(format);
}
None => {
invalid["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]
.as_object_mut()
.expect("history object")
.remove("digest_format");
}
}
assert!(
import_released_0810_session(
&serde_json::to_vec(&invalid).expect("invalid released bytes")
)
.is_err(),
"released history digest format {invalid_format:?} must fail closed"
);
}
let mut retired_stamp = released.clone();
retired_stamp["metadata"]["session_checkpoint_stamp_v1"] =
serde_json::json!("untrusted-retired-metadata");
let retired_stamp_import = import_released_0810_session(
&serde_json::to_vec(&retired_stamp).expect("released fixture with retired stamp"),
)
.expect("retired checkpoint metadata does not claim physical authority");
assert_eq!(
retired_stamp_import.receipt().evidence(),
Released0810ImportEvidence::StoreAuthorizationRequired
);
assert!(
!retired_stamp_import
.session()
.metadata()
.contains_key("session_checkpoint_stamp_v1")
);
let mut all_collapsed = released.clone();
all_collapsed["messages"] =
serde_json::to_value(&collapsed_messages).expect("collapsed live messages");
all_collapsed["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]["head"] =
serde_json::Value::String(old_collapsed);
all_collapsed["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]["commits"]
.as_array_mut()
.expect("released commits")
.truncate(1);
all_collapsed["metadata"][SESSION_TRANSCRIPT_HISTORY_STATE_KEY]["revisions"]
.as_array_mut()
.expect("released bodies")
.truncate(2);
let all_collapsed = import_released_0810_session(
&serde_json::to_vec(&all_collapsed).expect("all-collapsed fixture"),
)
.expect("fully collapsed released graph imports");
assert!(
all_collapsed
.session()
.transcript_history_state()
.expect("current history query")
.is_none(),
"a graph containing only semantic no-ops disappears at migration"
);
let imported = import_released_0810_session(
&serde_json::to_vec(&released).expect("released collapse fixture"),
)
.expect("source-proven collapsed rewrite imports");
let state = imported
.session()
.transcript_history_state()
.expect("current graph decodes")
.expect("retained tail keeps one graph");
assert_eq!(state.commit_count(), 1);
assert_eq!(
state.commit(0).expect("retained tail").rewrite_generation,
1,
"collapsed occurrences do not consume current generations"
);
assert_eq!(
state
.materialize_revision_bodies()
.expect("current bodies materialize")
.len(),
2,
"collapsed body identity is deduplicated"
);
assert_eq!(
state.commit(0).expect("retained tail").parent_revision,
transcript_messages_digest(&parent_messages).expect("current parent digest")
);
assert_eq!(
state.head(),
transcript_messages_digest(&final_messages).expect("current final digest")
);
}
#[test]
fn transcript_history_rejects_stale_branch_after_digest_recurrence() {
let mut restored = Session::new();
restored.push(Message::User(UserMessage::text("A".to_string())));
restored
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("B".to_string()))],
TranscriptRewriteReason::new("to-b"),
Some("unit-test".to_string()),
None,
)
.expect("A to B");
let mut stale_branch = restored.clone();
restored
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("A".to_string()))],
TranscriptRewriteReason::new("restore-a"),
Some("unit-test".to_string()),
None,
)
.expect("B back to A");
stale_branch
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("C".to_string()))],
TranscriptRewriteReason::new("stale-b-to-c"),
Some("unit-test".to_string()),
None,
)
.expect("stale B to C is locally valid");
let stale_state = stale_branch
.transcript_history_state()
.expect("stale state")
.expect("stale history");
let restored_state = restored
.transcript_history_state()
.expect("restored state")
.expect("restored history");
let mut records = (0..restored_state.commit_count())
.map(|index| rewrite_record_at(&restored_state, index))
.collect::<Vec<_>>();
let mut stale_record = rewrite_record_at(&stale_state, 1);
stale_record.commit.rewrite_generation = 3;
records.push(stale_record);
assert!(
TranscriptHistoryState::from_rewrite_records(records).is_err(),
"an old B<-A body edge cannot authorize stale B->C after B->A restored A"
);
}
#[test]
fn zero_generation_0_8_10_cycle_normalizes_from_proved_vector_order() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("A".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("B".to_string()))],
TranscriptRewriteReason::new("to-b"),
Some("unit-test".to_string()),
None,
)
.expect("A to B");
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("A".to_string()))],
TranscriptRewriteReason::new("restore-a"),
Some("unit-test".to_string()),
None,
)
.expect("B back to A");
let current = session
.transcript_history_state()
.expect("current history decodes")
.expect("current history exists");
let released = released_0810_document(
&session,
current.head().to_string(),
current
.materialize_revision_bodies()
.expect("released bodies should materialize"),
);
let released = serde_json::to_vec(&released).expect("released document bytes");
let imported =
import_released_0810_session(&released).expect("0.8.10 cyclic graph remains supported");
assert_eq!(
imported.receipt().evidence(),
Released0810ImportEvidence::StoreAuthorizationRequired
);
let state = imported
.session()
.transcript_history_state()
.expect("history decodes")
.expect("history exists");
assert_eq!(
state
.commits()
.map(|commit| commit.rewrite_generation)
.collect::<Vec<_>>(),
vec![1, 2],
"content recurrence must not rotate or refuse the proved commit-vector order"
);
validate_transcript_history_state(&state).expect("normalized cycle remains valid");
}
#[test]
fn transcript_history_rejects_cyclic_edge_base() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("P".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("Q".to_string()))],
TranscriptRewriteReason::new("valid"),
Some("unit-test".to_string()),
None,
)
.expect("valid seed rewrite");
let state = session
.transcript_history_state()
.expect("state")
.expect("history");
let mut state = serde_json::to_value(&state).expect("current history value");
let child_revision = state["edges"][0]["commit"]["revision"]
.as_str()
.expect("edge child revision")
.to_string();
state["edges"][0]["base_revision"] = serde_json::Value::String(child_revision);
session.set_metadata_unchecked_for_test(SESSION_TRANSCRIPT_HISTORY_STATE_KEY, state);
assert!(
serde_json::to_vec(&session).is_err(),
"a cyclic compact-edge base must fail instead of looping"
);
}
#[test]
fn live_append_can_recur_to_an_audited_digest_without_moving_audited_head() {
let a = Message::User(UserMessage::text("A".to_string()));
let b = Message::User(UserMessage::text("B".to_string()));
let mut session = Session::new();
session.push(Message::User(UserMessage::text("X".to_string())));
let first = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![a.clone(), b.clone()],
TranscriptRewriteReason::new("to-a-b"),
Some("unit-test".to_string()),
None,
)
.expect("X to [A,B]");
let h_parent = session
.transcript_revision_body(&first.revision)
.expect("H body")
.expect("H retained")
.parent_revision;
let second = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 2 },
vec![a],
TranscriptRewriteReason::new("to-a"),
Some("unit-test".to_string()),
None,
)
.expect("[A,B] to [A]");
session.push(b);
let state = session
.transcript_history_state()
.expect("state")
.expect("history");
assert_eq!(
state.head(),
second.revision,
"graph head remains the latest audited endpoint"
);
assert_eq!(session.transcript_revision().unwrap(), first.revision);
let recurred_body = state
.materialize_revision(&first.revision)
.expect("recurred H body");
assert_eq!(
recurred_body.parent_revision, h_parent,
"reusing an audited digest must not rewrite its occurrence metadata"
);
validate_transcript_history_state(&state).expect("audited graph remains valid");
}
#[test]
fn replace_synthetic_notices_rejects_mismatched_kind_without_mutation() {
use crate::types::{SystemNoticeKind, SystemNoticeMessage};
let mut session = Session::new();
session.push(Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"stale",
)));
let before = session.messages().to_vec();
let err = session
.replace_synthetic_notices(
SystemNoticeKind::McpPending,
vec![Message::User(UserMessage::text("not a notice".to_string()))],
)
.expect_err("mismatched replacement must fail typed");
assert!(
matches!(err, TranscriptEditError::InvalidTranscriptShape(_)),
"expected InvalidTranscriptShape, got {err:?}"
);
assert_eq!(
session.messages(),
before.as_slice(),
"fault must leave the transcript unchanged (no partial strip)"
);
}
#[test]
fn replace_synthetic_notices_rejects_malformed_history_atomically() {
use crate::types::{SystemNoticeKind, SystemNoticeMessage};
let mut session = Session::new();
session.push(Message::User(UserMessage::text("before".to_string())));
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("after".to_string()))],
TranscriptRewriteReason::new("unit-test-edit"),
Some("unit-test".to_string()),
None,
)
.expect("seed rewrite");
session.push(Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"stale",
)));
let state = session
.transcript_history_state()
.expect("state")
.expect("history");
let mut state = serde_json::to_value(&state).expect("current history value");
state["anchor"]["messages"][0] =
serde_json::to_value(Message::User(UserMessage::text("tampered".to_string())))
.expect("tampered message");
session.set_metadata_unchecked_for_test(SESSION_TRANSCRIPT_HISTORY_STATE_KEY, state);
let before_messages = session.messages.clone();
let before_metadata = session.metadata.clone();
let before_updated_at = session.updated_at;
assert!(
session
.replace_synthetic_notices(SystemNoticeKind::McpPending, Vec::new())
.is_err()
);
assert_eq!(session.messages(), before_messages.as_slice());
assert_eq!(session.metadata, before_metadata);
assert_eq!(session.updated_at, before_updated_at);
}
#[test]
fn replace_synthetic_notices_rejects_durable_notice_kinds() {
use crate::types::SystemNoticeKind;
let mut session = Session::new();
let before = session.messages().to_vec();
assert!(
session
.replace_synthetic_notices(SystemNoticeKind::Comms, Vec::new())
.is_err()
);
assert_eq!(session.messages(), before);
}
#[test]
fn replace_synthetic_notices_preserves_persisted_mcp_pending_notice() {
use crate::types::{SystemNoticeBlock, SystemNoticeKind, SystemNoticeMessage};
let mut session = Session::new();
session.push(Message::SystemNotice(SystemNoticeMessage::with_block(
SystemNoticeKind::McpPending,
Some("persisted pending fact".to_string()),
SystemNoticeBlock::Mcp {
server_id: Some("server".to_string()),
operation: None,
phase: None,
persisted: true,
detail: None,
pending_sources: Vec::new(),
},
)));
let before = session.messages().to_vec();
session
.replace_synthetic_notices(SystemNoticeKind::McpPending, Vec::new())
.expect("synthetic refresh must coexist with a durable notice of the same kind");
assert_eq!(session.messages(), before);
}
#[test]
fn replace_synthetic_notices_replaces_projection_beside_persisted_mcp_fact() {
use crate::types::{SystemNoticeBlock, SystemNoticeKind, SystemNoticeMessage};
let durable = Message::SystemNotice(SystemNoticeMessage::with_block(
SystemNoticeKind::McpPending,
Some("persisted pending fact".to_string()),
SystemNoticeBlock::Mcp {
server_id: Some("server".to_string()),
operation: None,
phase: None,
persisted: true,
detail: None,
pending_sources: Vec::new(),
},
));
let stale = Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"stale synthetic projection",
));
let fresh = Message::SystemNotice(SystemNoticeMessage::new(
SystemNoticeKind::McpPending,
"fresh synthetic projection",
));
let mut session = Session::new();
session.push(durable.clone());
session.push(stale);
session
.replace_synthetic_notices(SystemNoticeKind::McpPending, vec![fresh.clone()])
.expect("synthetic refresh beside durable fact");
assert_eq!(session.messages(), &[durable, fresh]);
}
#[test]
fn transcript_rewrite_preserves_full_assistant_block_trace() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text(
"run the trace".to_string(),
)));
session.push(Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::Text {
text: "original assistant trace".to_string(),
meta: None,
}],
StopReason::EndTurn,
)));
let parent_revision = session.transcript_revision().expect("parent revision");
let replacement = vec![
Message::BlockAssistant(BlockAssistantMessage::new(
vec![
AssistantBlock::Text {
text: "compacted assistant trace".to_string(),
meta: None,
},
AssistantBlock::ToolUse {
id: "toolu_trace".to_string(),
name: "trace_probe".to_string(),
args: serde_json::value::RawValue::from_string(
r#"{"path":"N-3"}"#.to_string(),
)
.expect("valid tool args"),
meta: None,
},
],
StopReason::ToolUse,
)),
Message::tool_results(vec![ToolResult::new(
"toolu_trace".to_string(),
"trace complete".to_string(),
false,
)]),
];
let commit = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
replacement,
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision.clone()),
)
.expect("rewrite should commit");
assert_eq!(commit.parent_revision, parent_revision);
let current = session
.transcript_revision_messages(&commit.revision)
.expect("history state should decode")
.expect("current revision should be retained");
let Message::BlockAssistant(assistant) = ¤t[1] else {
panic!("replacement should remain a block assistant message");
};
assert!(assistant.blocks.iter().any(|block| matches!(
block,
AssistantBlock::ToolUse { name, args, .. }
if name == "trace_probe" && args.get().contains("\"N-3\"")
)));
let parent = session
.transcript_revision_messages(&parent_revision)
.expect("history state should decode")
.expect("parent revision should remain retained");
assert!(matches!(
&parent[1],
Message::BlockAssistant(assistant)
if block_assistant_text(assistant).contains("original assistant trace")
));
}
#[test]
fn transcript_rewrite_rejects_trailing_block_assistant_tool_call() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "plain answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent_revision = session.transcript_revision().expect("parent revision");
let err = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::ToolUse {
id: "toolu_1".to_string(),
name: "lookup".to_string(),
args: serde_json::value::RawValue::from_string("{}".to_string())
.expect("valid args"),
meta: None,
}],
StopReason::ToolUse,
))],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision),
)
.expect_err("rewrite should reject trailing unresolved block-assistant tool call");
assert!(matches!(
err,
TranscriptEditError::InvalidTranscriptShape(_)
));
}
#[test]
fn transcript_rewrite_rejects_no_op_self_edge() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text(
"keep this exact transcript".to_string(),
)));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "unchanged".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent_revision = session.transcript_revision().expect("parent revision");
let err = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![session.messages()[1].clone()],
TranscriptRewriteReason::new("retry"),
Some("unit-test".to_string()),
Some(parent_revision.clone()),
)
.expect_err("same-content rewrite should not emit a self-edge commit");
assert!(matches!(
err,
TranscriptEditError::NoOpRewrite { revision } if revision == parent_revision
));
assert!(
session
.transcript_history_state()
.expect("history state should decode")
.is_none()
);
}
#[test]
fn transcript_rewrite_run_boundary_guard_accepts_rewrite_then_append() {
let mut original = Session::new();
original.push(Message::User(UserMessage::text("question".to_string())));
original.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent_revision = original.transcript_revision().expect("parent revision");
let mut incoming = original.clone();
incoming
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision),
)
.expect("rewrite should commit");
incoming.push(Message::User(UserMessage::text("follow-up".to_string())));
incoming.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "follow-up answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
crate::session_store::run_boundary_snapshot_save_guard(&incoming, Some(&original))
.expect("rewrite plus appended turn should be a valid run-boundary commit");
}
#[test]
fn transcript_rewrite_rejects_orphaned_tool_results() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("use a tool".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::ToolUse {
id: "toolu_1".to_string(),
name: "lookup".to_string(),
args: serde_json::value::RawValue::from_string("{}".to_string())
.expect("valid args"),
meta: None,
}],
StopReason::ToolUse,
)));
session.push(Message::tool_results(vec![ToolResult::new(
"toolu_1".to_string(),
"done".to_string(),
false,
)]));
let parent_revision = session.transcript_revision().expect("parent revision");
let err = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "no tool after all".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision),
)
.expect_err("rewrite should reject stranded tool results");
assert!(matches!(
err,
TranscriptEditError::InvalidTranscriptShape(_)
));
}
#[test]
fn transcript_rewrite_rejects_trailing_assistant_tool_call() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "plain answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent_revision = session.transcript_revision().expect("parent revision");
let err = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::ToolUse {
id: "toolu_1".to_string(),
name: "lookup".to_string(),
args: serde_json::value::RawValue::from_string("{}".to_string())
.expect("valid args"),
meta: None,
}],
stop_reason: StopReason::ToolUse,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision),
)
.expect_err("rewrite should reject trailing unresolved tool call");
assert!(matches!(
err,
TranscriptEditError::InvalidTranscriptShape(_)
));
}
#[test]
fn transcript_rewrite_rejects_duplicate_tool_results() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("use a tool".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "plain answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent_revision = session.transcript_revision().expect("parent revision");
let err = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![
Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::ToolUse {
id: "toolu_1".to_string(),
name: "lookup".to_string(),
args: serde_json::value::RawValue::from_string("{}".to_string())
.expect("valid args"),
meta: None,
}],
StopReason::ToolUse,
)),
Message::tool_results(vec![
ToolResult::new("toolu_1".to_string(), "one".to_string(), false),
ToolResult::new("toolu_1".to_string(), "two".to_string(), false),
]),
],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision),
)
.expect_err("rewrite should reject duplicate tool results");
assert!(matches!(
err,
TranscriptEditError::InvalidTranscriptShape(_)
));
}
#[test]
fn transcript_rewrite_record_rejects_prefix_or_suffix_tampering() {
let mut session = Session::new();
session.push(Message::System(SystemMessage::new("keep prefix")));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
session.push(Message::User(UserMessage::text("keep suffix".to_string())));
let parent_revision = session.transcript_revision().expect("parent revision");
let commit = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent_revision),
)
.expect("rewrite should commit");
let state = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
let record = rewrite_record_at(&state, 0);
let parent_body = record.parent_body;
let revision_body = record.revision_body;
let mut forged_body = revision_body;
forged_body.messages[0] = Message::System(SystemMessage::new("tampered prefix"));
forged_body.revision =
transcript_messages_digest(&forged_body.messages).expect("forged digest");
let mut forged_commit = commit;
forged_commit.revision = forged_body.revision.clone();
let err = TranscriptRewriteRecord::new(forged_commit, parent_body, forged_body)
.expect_err("record validation must reject changes outside selected span");
assert!(
err.to_string().contains("before the selected span"),
"unexpected error: {err}"
);
}
#[test]
fn transcript_rewrite_replay_allows_normal_turn_revisions_between_rewrites() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("first".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose first answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let first_parent = session.transcript_revision().expect("first parent");
let first_commit = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact first answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(first_parent),
)
.expect("first rewrite");
session.push(Message::User(UserMessage::text("normal turn".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose second answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let bridge_parent = session
.transcript_revision()
.expect("normal turn should advance transcript head");
assert_ne!(bridge_parent, first_commit.revision);
validate_transcript_history_state(
&session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist"),
)
.expect("normal turn head may legitimately differ from last rewrite commit");
let second_commit = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 3, end: 4 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact second answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(bridge_parent.clone()),
)
.expect("second rewrite");
let state = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
let records =
(0..state.commit_count()).map(|edge_index| rewrite_record_at(&state, edge_index));
let replayed = TranscriptHistoryState::from_rewrite_records(records)
.expect("rewrite replay should accept normal-turn bridge revisions")
.expect("rewrite records should exist");
assert_eq!(replayed.head(), second_commit.revision);
assert!(replayed.contains_revision(&bridge_parent));
}
#[test]
fn transcript_rewrite_replay_rejects_branched_rewrite_records() {
let mut base = Session::new();
base.push(Message::User(UserMessage::text("question".to_string())));
base.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent = base.transcript_revision().expect("parent revision");
let mut first = base.clone();
first
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "first compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent.clone()),
)
.expect("first rewrite");
let first_state = first
.transcript_history_state()
.expect("first state decodes")
.expect("first state exists");
let mut second = base;
second
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "second compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent),
)
.expect("second rewrite");
let second_state = second
.transcript_history_state()
.expect("second state decodes")
.expect("second state exists");
let err = TranscriptHistoryState::from_rewrite_records(vec![
rewrite_record_at(&first_state, 0),
rewrite_record_at(&second_state, 0),
])
.expect_err("branched rewrite records must not replay as a linear source history");
assert!(
err.to_string()
.contains("not expected contiguous generation"),
"unexpected error: {err}"
);
}
#[test]
fn internal_message_rewrites_refresh_transcript_history_head() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent = session.transcript_revision().expect("parent revision");
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent),
)
.expect("rewrite should commit");
session.push(Message::User(UserMessage::text(
"notice-bearing turn".to_string(),
)));
let retained = session
.messages()
.iter()
.filter(|message| {
!matches!(
message,
Message::User(user)
if user.content.iter().any(|block| matches!(
block,
ContentBlock::Text { text } if text.contains("notice-bearing")
))
)
})
.cloned()
.collect();
session
.replace_messages_internal(
retained,
TranscriptRewriteReason::new("synthetic_notice_cleanup"),
)
.expect("retain should commit internal rewrite");
let retained_digest =
transcript_messages_digest(session.messages()).expect("retained digest");
assert_eq!(
session.transcript_revision().expect("retained head"),
retained_digest
);
session
.replace_messages_internal(
vec![
Message::User(UserMessage::text("compacted question".to_string())),
Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compacted answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}),
],
TranscriptRewriteReason::new("compaction"),
)
.expect("replace should commit internal rewrite");
let replaced_digest =
transcript_messages_digest(session.messages()).expect("replaced digest");
assert_eq!(
session.transcript_revision().expect("replaced head"),
replaced_digest
);
let state = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
assert!(state.contains_revision(&replaced_digest));
validate_transcript_history_state(&state).expect("history state remains valid");
}
#[test]
fn append_system_message_preserves_exact_prefix_without_rewriting_history() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let parent = session.transcript_revision().expect("parent revision");
let rewrite = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent),
)
.expect("rewrite should commit");
let graph_before = session
.validated_transcript_history_state()
.expect("history validation")
.expect("audited graph");
let messages_before = session.messages().to_vec();
session.append_system_message("durable system prompt".to_string());
assert_eq!(
&session.messages()[..messages_before.len()],
messages_before.as_slice(),
"setting a System prompt must preserve every existing message as an exact prefix"
);
assert!(matches!(
session.messages().last(),
Some(Message::System(system)) if system.content == "durable system prompt"
));
let head = session
.transcript_revision()
.expect("live system prompt digest");
assert_ne!(head, rewrite.revision);
assert_eq!(
head,
transcript_messages_digest(session.messages()).expect("current digest")
);
let graph_after = session
.validated_transcript_history_state()
.expect("history validation")
.expect("audited graph");
assert!(
graph_before.shares_exact_state_with(&graph_after),
"mechanical prompt mutation must preserve the exact audited graph authority"
);
assert!(
session
.transcript_revision_messages(&head)
.expect("history state should decode")
.is_none(),
"live message digests are not retained graph revisions"
);
let state = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
assert_eq!(state.head(), rewrite.revision);
validate_transcript_history_state(&state).expect("audited graph remains valid");
}
#[test]
fn apply_transcript_history_state_uses_latest_commit_time_for_restored_head() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let original_messages = session.messages().to_vec();
let parent = session.transcript_revision().expect("parent revision");
let compact = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(parent.clone()),
)
.expect("rewrite should commit");
let restore = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange {
start: 0,
end: session.messages().len(),
},
original_messages.clone(),
TranscriptRewriteReason::new("restore"),
Some("unit-test".to_string()),
Some(compact.revision),
)
.expect("restore should commit");
assert_eq!(restore.revision, parent);
let state = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
let restored_body_created_at = state
.materialize_revision(&restore.revision)
.expect("restored body should be retained")
.created_at;
assert_eq!(
restored_body_created_at, restore.committed_at,
"restoring a repeated revision selects its latest occurrence timestamp"
);
let mut replayed = Session::new();
replayed
.apply_transcript_history_state(state)
.expect("replay should materialize restored head");
assert_eq!(
serde_json::to_value(replayed.messages()).expect("replayed serializes"),
serde_json::to_value(&original_messages).expect("original serializes")
);
assert_eq!(replayed.updated_at(), restore.committed_at);
}
#[test]
fn validated_bridge_parent_materialization_preserves_its_selected_head() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("question".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let first_parent = session.transcript_revision().expect("first parent");
let _first = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact answer".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(first_parent),
)
.expect("first rewrite should commit");
session.push(Message::User(UserMessage::text("follow up".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "verbose follow-up".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let bridge_messages = session.messages().to_vec();
let bridge_revision = session.transcript_revision().expect("bridge revision");
let second = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 3, end: 4 },
vec![Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "compact follow-up".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
})],
TranscriptRewriteReason::new("compaction"),
Some("unit-test".to_string()),
Some(bridge_revision.clone()),
)
.expect("second rewrite should commit");
assert_ne!(second.revision, bridge_revision);
let full = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
assert_eq!(full.head(), second.revision);
let bridge_body = ValidatedTranscriptHistory::seal_owned(full)
.expect("full graph should seal")
.materialize_rewrite_parent(&second)
.expect("the exact rewrite occurrence must materialize its bridge parent");
assert_eq!(
bridge_body.revision, bridge_revision,
"explicit parent materialization must preserve the selected bridge revision"
);
assert_eq!(
serde_json::to_value(&bridge_body.messages).expect("projection serializes"),
serde_json::to_value(&bridge_messages).expect("bridge serializes")
);
}
#[test]
fn exact_rewrite_occurrence_projection_orders_digest_recurrence() {
let message_a = Message::User(UserMessage::text("A".to_string()));
let message_b = Message::User(UserMessage::text("B".to_string()));
let mut session = Session::new();
session.push(message_a.clone());
let revision_a = session.transcript_revision().expect("A revision");
let first_b = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![message_b.clone()],
TranscriptRewriteReason::new("A-to-B"),
Some("unit-test".to_string()),
Some(revision_a.clone()),
)
.expect("first B occurrence should commit");
let back_to_a = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![message_a.clone()],
TranscriptRewriteReason::new("B-to-A"),
Some("unit-test".to_string()),
Some(first_b.revision.clone()),
)
.expect("second A occurrence should commit");
let second_b = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![message_b.clone()],
TranscriptRewriteReason::new("A-to-B-again"),
Some("unit-test".to_string()),
Some(back_to_a.revision.clone()),
)
.expect("second B occurrence should commit");
assert_eq!(back_to_a.revision, revision_a);
assert_eq!(second_b.revision, first_b.revision);
let graph = session
.transcript_history_state()
.expect("history state should decode")
.expect("history state should exist");
let sealed =
ValidatedTranscriptHistory::seal_owned(graph).expect("recurrence graph should seal");
for (generation, commit, parent_message, revision_message) in [
(1_u64, &first_b, &message_a, &message_b),
(2_u64, &back_to_a, &message_b, &message_a),
(3_u64, &second_b, &message_a, &message_b),
] {
assert_eq!(commit.rewrite_generation, generation);
let before = sealed
.materialize_rewrite_parent(commit)
.expect("exact parent occurrence should materialize");
assert_eq!(before.messages, std::slice::from_ref(parent_message));
let mut after = Session::new();
after
.apply_validated_transcript_history_state(
sealed
.project_at_rewrite_commit(commit)
.expect("exact rewrite occurrence should project"),
)
.expect("exact rewrite occurrence should materialize");
assert_eq!(after.messages(), std::slice::from_ref(revision_message));
let after_graph = after
.transcript_history_state()
.expect("rewrite graph should decode")
.expect("rewrite graph should exist");
assert_eq!(
after_graph.commit_count(),
usize::try_from(generation).expect("test generation fits usize")
);
assert_eq!(
after_graph.last_commit(),
Some(commit),
"the projection must end at this occurrence, not a later equal digest"
);
}
let latest_b = sealed
.project_at_revision(&first_b.revision)
.expect("content lookup should remain available");
assert_eq!(
latest_b.commit_count(),
3,
"digest-only lookup intentionally selects the latest matching occurrence"
);
}
#[test]
fn proved_rewrite_parent_projection_preserves_appends_and_prior_graph() {
let message_a = Message::User(UserMessage::text("A".to_string()));
let message_b = Message::User(UserMessage::text("B".to_string()));
let message_c = Message::User(UserMessage::text("C".to_string()));
let message_d = Message::User(UserMessage::text("D".to_string()));
let message_e = Message::User(UserMessage::text("E".to_string()));
let mut session = Session::new();
session.push(message_a);
session.push(message_b.clone());
let first_parent = session.messages().to_vec();
let first_parent_revision = session
.transcript_revision()
.expect("first parent revision");
let first = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![message_c.clone()],
TranscriptRewriteReason::new("first rewrite"),
Some("unit-test".to_string()),
Some(first_parent_revision),
)
.expect("first rewrite should commit");
session.push(message_d.clone());
let second_parent = session.messages().to_vec();
let second_parent_revision = session
.transcript_revision()
.expect("second parent revision");
let second = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![message_e],
TranscriptRewriteReason::new("second rewrite"),
Some("unit-test".to_string()),
Some(second_parent_revision),
)
.expect("second rewrite should commit");
let sealed = session
.validated_transcript_history_state()
.expect("history should validate")
.expect("rewritten session should carry history");
let projected_first_parent = session
.with_validated_transcript_rewrite_parent_projection(&sealed, &first)
.expect("first parent should project");
assert_eq!(projected_first_parent.messages(), first_parent);
assert_eq!(
projected_first_parent
.transcript_rewrite_generation()
.expect("first parent generation"),
0
);
assert_eq!(
projected_first_parent
.transcript_revision()
.expect("first projected parent revision"),
first.parent_revision
);
let projected_second_parent = session
.with_validated_transcript_rewrite_parent_projection(&sealed, &second)
.expect("second parent should project");
assert_eq!(projected_second_parent.messages(), second_parent);
assert_eq!(
projected_second_parent
.transcript_rewrite_generation()
.expect("second parent generation"),
1
);
assert_eq!(
projected_second_parent
.transcript_revision()
.expect("second projected parent revision"),
second.parent_revision
);
let preceding_graph = projected_second_parent
.validated_transcript_history_state()
.expect("preceding graph should validate")
.expect("second parent should retain the first occurrence");
assert_eq!(preceding_graph.commit_count(), 1);
assert_eq!(preceding_graph.last_commit(), Some(&first));
assert_eq!(
projected_second_parent.messages(),
&[message_c, message_b, message_d]
);
}
#[test]
fn test_session_new() {
let session = Session::new();
assert_eq!(session.version(), SESSION_VERSION);
assert!(session.messages().is_empty());
assert!(session.created_at() <= session.updated_at());
}
#[test]
fn llm_identity_model_override_switches_to_catalog_provider() {
let registry = crate::ModelRegistry::from_config(
&crate::Config::default(),
*crate::model_profile::test_catalog::TEST_CATALOG,
)
.unwrap();
let current = SessionLlmIdentity {
model: "test-anthropic-default".to_string(),
provider: Provider::Anthropic,
self_hosted_server_id: None,
provider_params: None,
auth_binding: Some(crate::AuthBindingRef {
realm: crate::RealmId::parse("tenant_a").unwrap(),
binding: crate::BindingId::parse("anthropic_default").unwrap(),
profile: None,
origin: crate::BindingOrigin::Configured,
}),
};
let resolved = resolve_session_llm_identity_override(
¤t,
®istry,
SessionLlmIdentityOverride {
model: Some("test-openai-default"),
provider: None,
self_hosted_server_id: None,
provider_params: None,
auth_binding: None,
},
)
.unwrap();
assert_eq!(resolved.model, "test-openai-default");
assert_eq!(resolved.provider, Provider::OpenAI);
assert!(
resolved.auth_binding.is_none(),
"provider switches must not inherit a binding from the previous provider"
);
}
#[test]
fn llm_identity_model_override_keeps_uncatalogued_model_on_current_provider() {
let registry = crate::ModelRegistry::from_config(
&crate::Config::default(),
*crate::model_profile::test_catalog::TEST_CATALOG,
)
.unwrap();
let current = SessionLlmIdentity {
model: "custom-model".to_string(),
provider: Provider::Anthropic,
self_hosted_server_id: None,
provider_params: None,
auth_binding: None,
};
let resolved = resolve_session_llm_identity_override(
¤t,
®istry,
SessionLlmIdentityOverride {
model: Some("uncatalogued-custom-model"),
provider: None,
self_hosted_server_id: None,
provider_params: None,
auth_binding: None,
},
)
.unwrap();
assert_eq!(resolved.model, "uncatalogued-custom-model");
assert_eq!(resolved.provider, Provider::Anthropic);
}
fn self_hosted_registry_with_shared_remote_model() -> crate::ModelRegistry {
use crate::config::{
SelfHostedApiStyle, SelfHostedModelConfig, SelfHostedServerConfig, SelfHostedTransport,
};
use crate::model_profile::catalog::ModelTier;
let mut config = crate::Config::default();
for server_id in ["local-a", "local-b"] {
config.self_hosted.servers.insert(
server_id.to_string(),
SelfHostedServerConfig {
transport: SelfHostedTransport::OpenAiCompatible,
base_url: format!("http://{server_id}.test"),
api_style: SelfHostedApiStyle::Responses,
},
);
config.self_hosted.models.insert(
format!("shared-local-{server_id}"),
SelfHostedModelConfig {
server: server_id.to_string(),
remote_model: "shared-local-model".to_string(),
display_name: "Shared local model".to_string(),
family: "shared-local".to_string(),
tier: ModelTier::Supported,
..Default::default()
},
);
}
config.self_hosted.default_model = Some("shared-local-local-a".to_string());
crate::ModelRegistry::from_config(
&config,
*crate::model_profile::test_catalog::TEST_CATALOG,
)
.expect("shared local registry")
}
#[test]
fn llm_identity_override_preserves_exact_self_hosted_server_route() {
let registry = self_hosted_registry_with_shared_remote_model();
let current = SessionLlmIdentity {
model: "shared-local-local-a".to_string(),
provider: Provider::SelfHosted,
self_hosted_server_id: Some("local-a".to_string()),
provider_params: None,
auth_binding: None,
};
let resolved = resolve_session_llm_identity_override(
¤t,
®istry,
SessionLlmIdentityOverride {
model: Some("shared-local-local-b"),
provider: Some(Provider::SelfHosted),
self_hosted_server_id: Some("local-b"),
provider_params: None,
auth_binding: None,
},
)
.expect("exact configured local route should resolve");
assert_eq!(resolved.model, "shared-local-local-b");
assert_eq!(resolved.provider, Provider::SelfHosted);
assert_eq!(resolved.self_hosted_server_id.as_deref(), Some("local-b"));
}
#[test]
fn llm_identity_override_rejects_self_hosted_server_model_mismatch() {
let registry = self_hosted_registry_with_shared_remote_model();
let current = SessionLlmIdentity {
model: "shared-local-local-a".to_string(),
provider: Provider::SelfHosted,
self_hosted_server_id: Some("local-a".to_string()),
provider_params: None,
auth_binding: None,
};
let error = resolve_session_llm_identity_override(
¤t,
®istry,
SessionLlmIdentityOverride {
model: Some("shared-local-local-b"),
provider: Some(Provider::SelfHosted),
self_hosted_server_id: Some("local-a"),
provider_params: None,
auth_binding: None,
},
)
.expect_err("server id must match the requested model alias route");
assert!(matches!(
error,
SessionLlmIdentityOverrideError::SelfHostedServerMismatch {
requested,
configured,
..
} if requested == "local-a" && configured == "local-b"
));
}
#[test]
fn realtime_transcript_append_is_idempotent_by_provider_item_and_delta_id() {
let mut session = Session::new();
let user = RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "hello".to_string(),
};
assert!(
!session
.append_realtime_transcript_event(user.clone())
.is_inert()
);
assert!(session.append_realtime_transcript_event(user).is_inert());
let delta = RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_assistant".to_string(),
delta_id: "evt_delta_1".to_string(),
item_id: "item_assistant".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "hi".to_string(),
};
assert!(
session
.append_realtime_transcript_event(delta.clone())
.is_inert()
);
assert!(session.append_realtime_transcript_event(delta).is_inert());
let terminal = RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_assistant".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
};
assert!(
!session
.append_realtime_transcript_event(terminal.clone())
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(terminal)
.is_inert()
);
assert_eq!(session.messages().len(), 2);
assert!(matches!(
&session.messages()[0],
Message::User(user) if user.text_content() == "hello"
));
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "hi"
));
}
#[test]
fn realtime_legacy_inline_activation_is_failure_atomic_and_preserves_whole_blob() {
let mut malformed = Session::new();
malformed.set_metadata_unchecked_for_test(
SESSION_REALTIME_TRANSCRIPT_STATE_KEY,
serde_json::json!("not-a-realtime-state"),
);
let pristine_prefix = malformed
.realtime_component_event_prefix()
.expect("pristine realtime prefix");
assert!(matches!(
malformed.activate_realtime_component_sidecar(),
Err(RealtimeTranscriptSidecarError::Serialization(_))
));
assert_eq!(
malformed
.metadata()
.get(SESSION_REALTIME_TRANSCRIPT_STATE_KEY),
Some(&serde_json::json!("not-a-realtime-state")),
"failed activation must leave the exact legacy value in place"
);
assert_eq!(
malformed
.realtime_component_event_prefix()
.expect("unchanged realtime prefix"),
pristine_prefix,
"failed activation must not advance component authority"
);
let mut session = Session::new();
let state = SessionRealtimeTranscriptState::default();
let inline = serde_json::to_value(&state).expect("inline projection");
session
.set_metadata_unchecked_for_test(SESSION_REALTIME_TRANSCRIPT_STATE_KEY, inline.clone());
session
.activate_realtime_component_sidecar()
.expect("supported inline activation");
assert!(
!session
.metadata()
.contains_key(SESSION_REALTIME_TRANSCRIPT_STATE_KEY),
"successful activation removes raw shadow authority"
);
let suffix = session
.prepare_realtime_component_event_suffix()
.expect("prepare activation suffix")
.expect("SnapshotV1 suffix");
assert_eq!(suffix.events().len(), 1);
assert!(matches!(
suffix.events()[0]
.decode_payload::<crate::RealtimeTranscriptSidecarRecord>(
crate::REALTIME_TRANSCRIPT_SIDECAR_EVENT_SCHEMA_V1
)
.expect("decode activation record"),
crate::RealtimeTranscriptSidecarRecord::SnapshotV1 { .. }
));
let whole_blob =
serde_json::to_value(&session).expect("WholeBlob projection after activation");
assert_eq!(
whole_blob
.get("metadata")
.and_then(serde_json::Value::as_object)
.and_then(|metadata| metadata.get(SESSION_REALTIME_TRANSCRIPT_STATE_KEY)),
Some(&inline),
"activation changes storage authority, not the WholeBlob projection"
);
}
#[test]
fn realtime_user_image_materializes_once_and_unblocks_causal_assistant() {
let mut session = Session::new();
let image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB".to_string();
let image = RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "image-request-1".to_string(),
item_id: "item_image".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline {
data: image_data.clone(),
},
}],
};
assert!(
!append_staged_user_image(&mut session, &image).is_inert(),
"first image final must materialize canonical user content"
);
let replay = session
.preflight_realtime_user_content_event(&image)
.expect("exact retry should preflight as committed");
assert!(matches!(
replay,
crate::RealtimeUserContentApplyOutcome::AlreadyCommitted(_)
));
assert!(
!session
.metadata()
.contains_key(SESSION_REALTIME_TRANSCRIPT_STATE_KEY),
"ordinary operation must keep the full realtime projection out of raw metadata"
);
let whole_blob =
serde_json::to_value(&session).expect("WholeBlob projection should serialize");
let staged_state = whole_blob
.get("metadata")
.and_then(serde_json::Value::as_object)
.and_then(|metadata| metadata.get(SESSION_REALTIME_TRANSCRIPT_STATE_KEY))
.expect("WholeBlob compatibility projection must include realtime state");
assert!(
!staged_state.to_string().contains(&image_data),
"materialized image bytes must not remain duplicated in transcript metadata"
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_image".to_string(),
delta_id: "delta_image".to_string(),
item_id: "item_assistant".to_string(),
previous_item_id: Some("item_image".to_string()),
content_index: 0,
delta: "I see red.".to_string(),
})
.is_inert()
);
assert!(
!session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_image".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},)
.is_inert(),
"materialized image predecessor must unblock the assistant response"
);
assert_eq!(session.messages().len(), 2);
assert!(matches!(
&session.messages()[0],
Message::User(user)
if matches!(
user.content.as_slice(),
[ContentBlock::Image {
media_type,
data: crate::types::ImageData::Blob { blob_id },
}] if media_type == "image/png"
&& blob_id == &crate::blob::content_blob_id("image/png", &image_data)
)
));
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "I see red."
));
}
#[test]
fn realtime_user_image_identity_is_durable_canonical_and_conflict_safe() {
let mut session = Session::new();
let data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB".to_string();
let initial = RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "stable-image-key".to_string(),
item_id: "canonical-image-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: " image/PNG; charset=binary ".to_string(),
data: crate::types::ImageData::Inline { data: data.clone() },
}],
};
let committed = append_staged_user_image(&mut session, &initial);
let Some(crate::RealtimeUserContentApplyOutcome::Committed(identity)) =
committed.user_content
else {
panic!("first image must commit its durable identity");
};
assert_eq!(identity.item_id, "canonical-image-item");
assert_eq!(identity.media_type, "image/png");
let encoded = serde_json::to_string(&session).expect("session should serialize");
let restored: Session =
serde_json::from_str(&encoded).expect("committed identity should restore");
let replay_event = RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "stable-image-key".to_string(),
item_id: "ignored-retry-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline { data: data.clone() },
}],
};
let replay = restored
.preflight_realtime_user_content_event(&replay_event)
.expect("exact retry should preflight");
assert!(matches!(
replay,
crate::RealtimeUserContentApplyOutcome::AlreadyCommitted(
crate::RealtimeUserContentIdentity { ref item_id, .. }
) if item_id == "canonical-image-item"
));
let conflict = restored
.preflight_realtime_user_content_event(&RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "stable-image-key".to_string(),
item_id: "conflicting-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline {
data: "different-payload".to_string(),
},
}],
})
.expect("conflicting retry should preflight");
assert!(matches!(
conflict,
crate::RealtimeUserContentApplyOutcome::RejectedConflict { .. }
));
let item_collision = restored
.preflight_realtime_user_content_event(&RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "another-key".to_string(),
item_id: "canonical-image-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline { data },
}],
})
.expect("item collision should preflight");
assert!(matches!(
item_collision,
crate::RealtimeUserContentApplyOutcome::RejectedConflict { .. }
));
assert_eq!(restored.messages().len(), 1);
serde_json::to_string(&restored).expect("rejections must not corrupt durable state");
}
#[test]
fn realtime_user_image_reducer_never_receipts_without_pending_blob_proof() {
for data in [
crate::types::ImageData::Inline {
data: "iVBORw0KGgo=".to_string(),
},
crate::types::ImageData::Blob {
blob_id: crate::blob::content_blob_id("image/png", "iVBORw0KGgo="),
},
] {
let mut session = Session::new();
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "unstaged-image-key".to_string(),
item_id: "unstaged-image-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data,
}],
},
);
assert!(matches!(
outcome.user_content,
Some(crate::RealtimeUserContentApplyOutcome::RejectedInvalidIdentity { .. })
));
assert!(session.messages().is_empty());
assert!(session.realtime_user_content_identities().is_empty());
}
}
#[test]
fn realtime_user_image_pending_slot_is_generated_bounded_and_recovery_typed() {
use crate::generated::session_document::{
RealtimeUserContentBlobRecoveryDisposition, RealtimeUserContentBlobStageDisposition,
};
let mut session = Session::new();
let pending = crate::PendingRealtimeUserContentBlob {
idempotency_key: "pending-key-a".to_string(),
item_id: "pending-item-a".to_string(),
previous_item_id: None,
content_index: 0,
blob_id: crate::blob::content_blob_id("image/png", "iVBORw0KGgo="),
media_type: "image/png".to_string(),
};
let different = crate::PendingRealtimeUserContentBlob {
idempotency_key: "pending-key-b".to_string(),
item_id: "pending-item-b".to_string(),
previous_item_id: None,
content_index: 0,
blob_id: crate::blob::content_blob_id("image/png", "iVBORw0KGgoB"),
media_type: "image/png".to_string(),
};
assert_eq!(
session
.stage_pending_realtime_user_content_blob(pending.clone())
.expect("empty slot stages"),
RealtimeUserContentBlobStageDisposition::StageNew
);
assert_eq!(
session
.stage_pending_realtime_user_content_blob(pending.clone())
.expect("exact stage retry is idempotent"),
RealtimeUserContentBlobStageDisposition::ReuseExact
);
assert_eq!(
session
.stage_pending_realtime_user_content_blob(different.clone())
.expect("occupied decision is typed"),
RealtimeUserContentBlobStageDisposition::RejectOccupied
);
assert_eq!(
session.pending_realtime_user_content_blob(),
Some(pending.clone())
);
assert_eq!(
session
.resolve_pending_realtime_user_content_blob_recovery(Some(&pending), false)
.expect("exact recovery decision"),
RealtimeUserContentBlobRecoveryDisposition::RetryExact
);
assert_eq!(
session
.resolve_pending_realtime_user_content_blob_recovery(Some(&different), true)
.expect("verified older recovery decision"),
RealtimeUserContentBlobRecoveryDisposition::CommitVerifiedBeforeCurrent
);
assert_eq!(
session
.resolve_pending_realtime_user_content_blob_recovery(Some(&different), false)
.expect("invalid older recovery decision"),
RealtimeUserContentBlobRecoveryDisposition::ClearInvalidBeforeCurrent
);
session
.clear_invalid_pending_realtime_user_content_blob(Some(&different))
.expect("generated clear-invalid disposition authorizes clear");
assert!(session.pending_realtime_user_content_blob().is_none());
}
#[test]
fn transcript_rewrite_tombstones_removed_image_key_and_accepts_new_key() {
let mut session = Session::new();
let data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB".to_string();
let original = RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "removed-image-key".to_string(),
item_id: "removed-image-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline { data: data.clone() },
}],
};
assert!(matches!(
append_staged_user_image(&mut session, &original).user_content,
Some(crate::RealtimeUserContentApplyOutcome::Committed(_))
));
let parent = session.transcript_revision().expect("parent revision");
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("image removed"))],
TranscriptRewriteReason::new("remove-image"),
None,
Some(parent),
)
.expect("rewrite should tombstone removed image identity");
assert!(session.realtime_user_content_identities().is_empty());
assert_eq!(
session.realtime_user_content_tombstones(),
vec![crate::RealtimeUserContentTombstone {
idempotency_key: "removed-image-key".to_string(),
}]
);
assert!(matches!(
session.preflight_realtime_user_content_event(&original),
Some(crate::RealtimeUserContentApplyOutcome::RejectedConflict { .. })
));
assert!(matches!(
session
.append_realtime_transcript_event(original)
.user_content,
Some(crate::RealtimeUserContentApplyOutcome::RejectedConflict { .. })
));
assert_eq!(
session.messages().len(),
1,
"stale retry emits no receipt content"
);
let new_image = RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "new-image-key".to_string(),
item_id: "new-image-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline { data },
}],
};
assert!(matches!(
append_staged_user_image(&mut session, &new_image).user_content,
Some(crate::RealtimeUserContentApplyOutcome::Committed(_))
));
assert_eq!(session.messages().len(), 2);
let restored: Session = serde_json::from_str(
&serde_json::to_string(&session).expect("serialize rewritten session"),
)
.expect("cold restore rewritten session");
assert_eq!(restored.realtime_user_content_identities().len(), 1);
assert_eq!(restored.realtime_user_content_tombstones().len(), 1);
}
#[test]
fn transcript_rewrite_retains_only_canonical_image_occurrence_for_exact_replay() {
let mut session = Session::new();
let data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB".to_string();
let original = RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: "retained-image-key".to_string(),
item_id: "retained-image-item".to_string(),
previous_item_id: None,
content_index: 0,
content: vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline { data },
}],
};
assert!(matches!(
append_staged_user_image(&mut session, &original).user_content,
Some(crate::RealtimeUserContentApplyOutcome::Committed(_))
));
let retained_message = session.messages()[0].clone();
let parent = session.transcript_revision().expect("parent revision");
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![
retained_message,
Message::User(UserMessage::text("new canonical neighbor")),
],
TranscriptRewriteReason::new("retain-image"),
None,
Some(parent),
)
.expect("rewrite retaining exact inline image should reconcile");
assert!(session.realtime_user_content_tombstones().is_empty());
let replay = session
.preflight_realtime_user_content_event(&original)
.expect("retained image should preflight as exact replay");
assert!(matches!(
replay,
crate::RealtimeUserContentApplyOutcome::AlreadyCommitted(_)
));
assert_eq!(session.messages().len(), 2);
}
#[test]
fn transcript_rewrite_rejects_atomically_while_image_blob_anchor_is_pending() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("before rewrite")));
let pending = crate::PendingRealtimeUserContentBlob {
idempotency_key: "pending-rewrite-key".to_string(),
item_id: "pending-rewrite-item".to_string(),
previous_item_id: None,
content_index: 0,
blob_id: crate::blob::content_blob_id("image/png", "pending-bytes"),
media_type: "image/png".to_string(),
};
session
.stage_pending_realtime_user_content_blob(pending.clone())
.expect("stage durable pending anchor");
let parent = session.transcript_revision().expect("parent revision");
let error = session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text("after rewrite"))],
TranscriptRewriteReason::new("blocked-pending-image"),
None,
Some(parent),
)
.expect_err("rewrite must not cross an unresolved image anchor");
assert!(
error
.to_string()
.contains("history_rewrite_pending_user_content_blob")
);
assert!(matches!(
&session.messages()[0],
Message::User(user) if user.text_content() == "before rewrite"
));
assert_eq!(session.pending_realtime_user_content_blob(), Some(pending));
}
#[test]
fn realtime_user_image_rejects_noncanonical_blob_and_multiblock_shape() {
let mut session = Session::new();
for (key, content) in [
(
"invalid-blob",
vec![ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Blob {
blob_id: crate::BlobId::new("sha256:not-a-digest"),
},
}],
),
(
"multi-block",
vec![
ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline {
data: "payload".to_string(),
},
},
ContentBlock::Text {
text: "smuggled".to_string(),
},
],
),
] {
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: key.to_string(),
item_id: format!("item-{key}"),
previous_item_id: None,
content_index: 0,
content,
},
);
assert!(matches!(
outcome.user_content,
Some(crate::RealtimeUserContentApplyOutcome::RejectedInvalidIdentity { .. })
));
}
assert!(session.messages().is_empty());
let encoded = serde_json::to_string(&session).expect("session should serialize");
serde_json::from_str::<Session>(&encoded).expect("rejections must leave restorable state");
}
#[test]
fn realtime_restore_rejects_malformed_causal_graphs_and_accepts_waiting_dag() {
fn restore(
items: serde_json::Value,
first_seen_order: Vec<&str>,
) -> Result<
crate::realtime_transcript_revision::SessionRealtimeTranscriptState,
crate::realtime_transcript_revision::RealtimeTranscriptShellError,
> {
let state = serde_json::from_value(serde_json::json!({
"items": items,
"first_seen_order": first_seen_order,
}))
.expect("test state shape should deserialize");
crate::realtime_transcript_revision::restore_realtime_transcript_state(state)
}
assert!(
restore(
serde_json::json!({
"child": { "role": "user", "previous_item_id": "missing" }
}),
vec!["child"],
)
.is_ok(),
"an unmaterialized out-of-order item must survive cold restore until its predecessor arrives"
);
assert!(
restore(
serde_json::json!({
"child": {
"role": "user",
"previous_item_id": "missing",
"ready": true,
"materialized": true
}
}),
vec!["child"],
)
.is_err(),
"a materialized item cannot reference a missing predecessor"
);
assert!(
restore(
serde_json::json!({
"self": { "role": "user", "previous_item_id": "self" }
}),
vec!["self"],
)
.is_err(),
"self edge must fail cold restore"
);
assert!(
restore(
serde_json::json!({
"a": { "role": "user", "previous_item_id": "b" },
"b": { "role": "user", "previous_item_id": "a" }
}),
vec!["a", "b"],
)
.is_err(),
"cycle must fail cold restore"
);
assert!(
restore(
serde_json::json!({
"root": { "role": "user" },
"materialized_child": {
"role": "user",
"previous_item_id": "root",
"ready": true,
"materialized": true
}
}),
vec!["root", "materialized_child"],
)
.is_err(),
"materialized child cannot have unmaterialized ancestry"
);
assert!(
restore(
serde_json::json!({
"root": { "role": "user" },
"waiting_child": { "role": "user", "previous_item_id": "root" }
}),
vec!["waiting_child", "root"],
)
.is_ok(),
"valid acyclic waiting graph should restore even when first-seen order is child-first"
);
}
#[test]
fn realtime_restore_handles_long_waiting_chain_with_bounded_graph_walk() {
const ITEM_COUNT: usize = 4_096;
let mut items = serde_json::Map::new();
let mut order = Vec::with_capacity(ITEM_COUNT);
for index in 0..ITEM_COUNT {
let item_id = format!("item-{index:04}");
let value = if index == 0 {
serde_json::json!({ "role": "user" })
} else {
serde_json::json!({
"role": "user",
"previous_item_id": format!("item-{:04}", index - 1),
})
};
order.push(item_id.clone());
items.insert(item_id, value);
}
let state = serde_json::from_value(serde_json::json!({
"items": items,
"first_seen_order": order,
}))
.expect("long-chain fixture should deserialize");
crate::realtime_transcript_revision::restore_realtime_transcript_state(state)
.expect("long valid waiting DAG should restore in one bounded graph walk");
}
#[test]
fn realtime_transcript_final_text_overrides_partial_delta_and_promotes_to_spoken_lane() {
let mut session = Session::new();
assert!(
session
.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_a".to_string(),
delta_id: "evt_1".to_string(),
item_id: "item_a".to_string(),
previous_item_id: None,
content_index: 0,
delta: "incom".to_string(),
}
)
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptFinalText {
response_id: "resp_a".to_string(),
item_id: "item_a".to_string(),
content_index: 0,
text: "complete answer".to_string(),
}
)
.is_inert()
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert!(!outcome.is_inert());
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
let mut found_transcript = false;
for block in &assistant.blocks {
if let AssistantBlock::Transcript { text, .. } = block {
assert_eq!(text, "complete answer");
found_transcript = true;
}
}
assert!(
found_transcript,
"AssistantTranscriptFinalText must promote to the Spoken lane and \
materialize as AssistantBlock::Transcript"
);
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn realtime_transcript_final_text_creates_item_when_no_delta_staged() {
let mut session = Session::new();
assert!(
session
.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptFinalText {
response_id: "resp_a".to_string(),
item_id: "item_a".to_string(),
content_index: 0,
text: "spoken-final-only".to_string(),
}
)
.is_inert()
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert!(!outcome.is_inert());
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
let has_transcript = assistant.blocks.iter().any(|b| {
matches!(b, AssistantBlock::Transcript { text, .. } if text == "spoken-final-only")
});
assert!(
has_transcript,
"final-only provider path must materialize as Transcript on the Spoken lane"
);
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn realtime_transcript_append_orders_causally_equivalent_out_of_order_items() {
let mut session = Session::new();
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_assistant".to_string(),
delta_id: "evt_delta_1".to_string(),
item_id: "item_assistant".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "answer".to_string(),
})
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_assistant".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
})
.is_inert()
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "question".to_string(),
},
);
assert_eq!(outcome.materialized_messages.len(), 2);
assert_eq!(session.messages().len(), 2);
assert!(matches!(
&session.messages()[0],
Message::User(user) if user.text_content() == "question"
));
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "answer"
));
}
#[test]
fn realtime_transcript_replay_of_seen_provider_items_is_inert() {
let mut session = Session::new();
let events = vec![
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "hello".to_string(),
},
RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_assistant".to_string(),
delta_id: "evt_delta_1".to_string(),
item_id: "item_assistant".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "world".to_string(),
},
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_assistant".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
];
for event in events.iter().cloned() {
let _ = session.append_realtime_transcript_event(event);
}
let first_messages = serde_json::to_value(session.messages()).unwrap();
for event in events {
assert!(session.append_realtime_transcript_event(event).is_inert());
}
assert_eq!(
serde_json::to_value(session.messages()).unwrap(),
first_messages
);
}
#[test]
fn realtime_transcript_user_final_replay_cannot_erase_existing_segment() {
let mut session = Session::new();
let user = RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "remember amber lantern".to_string(),
};
assert!(
!session
.append_realtime_transcript_event(user.clone())
.is_inert()
);
let first_messages = serde_json::to_value(session.messages()).unwrap();
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: String::new(),
})
.is_inert()
);
assert!(session.append_realtime_transcript_event(user).is_inert());
assert_eq!(
serde_json::to_value(session.messages()).unwrap(),
first_messages
);
}
#[test]
fn realtime_transcript_empty_user_final_can_be_filled_by_later_nonempty_replay() {
let mut session = Session::new();
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: String::new(),
})
.is_inert()
);
assert!(session.messages().is_empty());
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "remember amber lantern".to_string(),
},
);
assert_eq!(outcome.materialized_messages.len(), 1);
assert_eq!(session.messages().len(), 1);
assert!(matches!(
&session.messages()[0],
Message::User(user) if user.text_content() == "remember amber lantern"
));
}
#[test]
fn realtime_transcript_skipped_provider_items_preserve_causal_order_without_content() {
let mut session = Session::new();
let assistant_delta = RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_assistant".to_string(),
delta_id: "evt_delta_1".to_string(),
item_id: "item_assistant".to_string(),
previous_item_id: Some("item_tool".to_string()),
content_index: 0,
delta: "done".to_string(),
};
assert!(
session
.append_realtime_transcript_event(assistant_delta.clone())
.is_inert()
);
let assistant_complete = RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_assistant".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
};
assert!(
session
.append_realtime_transcript_event(assistant_complete.clone())
.is_inert()
);
let skipped = RealtimeTranscriptEvent::ItemSkipped {
item_id: "item_tool".to_string(),
previous_item_id: Some("item_user".to_string()),
};
assert!(
session
.append_realtime_transcript_event(skipped.clone())
.is_inert(),
"a skipped provider item must not append transcript content"
);
assert!(session.messages().is_empty());
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "please use the tool".to_string(),
},
);
assert_eq!(outcome.materialized_messages.len(), 2);
assert_eq!(session.messages().len(), 2);
assert!(matches!(
&session.messages()[0],
Message::User(user) if user.text_content() == "please use the tool"
));
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "done"
));
let first_messages = serde_json::to_value(session.messages()).unwrap();
assert!(session.append_realtime_transcript_event(skipped).is_inert());
assert!(
session
.append_realtime_transcript_event(assistant_delta)
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(assistant_complete)
.is_inert()
);
assert_eq!(
serde_json::to_value(session.messages()).unwrap(),
first_messages
);
}
#[test]
fn realtime_transcript_interrupted_assistant_item_unblocks_later_provider_items() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_repeat".to_string(),
previous_item_id: None,
content_index: 0,
text: "repeat until stop".to_string(),
},
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_loop".to_string(),
delta_id: "evt_loop_1".to_string(),
item_id: "item_loop".to_string(),
previous_item_id: Some("item_repeat".to_string()),
content_index: 0,
delta: "Looping now".to_string(),
})
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_stop".to_string(),
previous_item_id: Some("item_loop".to_string()),
content_index: 0,
text: "Stop.".to_string(),
})
.is_inert(),
"the stop turn waits until the interrupted assistant provider item is resolved"
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_loop".to_string(),
},
);
assert_eq!(outcome.materialized_messages.len(), 2);
assert_eq!(session.messages().len(), 3);
assert!(matches!(
&session.messages()[0],
Message::User(user) if user.text_content() == "repeat until stop"
));
match &session.messages()[1] {
Message::BlockAssistant(assistant) => {
let text = block_assistant_text(assistant);
assert_eq!(text, "Looping now");
}
other => unreachable!(
"Display lane assistant item must be retained on Interrupted, got {other:?}"
),
}
assert!(matches!(
&session.messages()[2],
Message::User(user) if user.text_content() == "Stop."
));
}
#[test]
fn realtime_transcript_late_interrupted_assistant_delta_stays_noncanonical() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_repeat".to_string(),
previous_item_id: None,
content_index: 0,
text: "repeat until stop".to_string(),
},
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::ItemObserved {
item_id: "item_loop".to_string(),
previous_item_id: Some("item_repeat".to_string()),
role: RealtimeTranscriptRole::Assistant,
response_id: None,
})
.is_inert(),
"provider can observe an assistant item before the adapter learns its response id"
);
assert!(
session
.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_loop".to_string(),
}
)
.is_inert(),
"an interruption can arrive before delayed transcript deltas for the response"
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_stop".to_string(),
previous_item_id: Some("item_loop".to_string()),
content_index: 0,
text: "Stop.".to_string(),
})
.is_inert(),
"the stop turn waits for the provider's interrupted assistant item anchor"
);
let late_delta_outcome =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_loop".to_string(),
delta_id: "evt_loop_late".to_string(),
item_id: "item_loop".to_string(),
previous_item_id: Some("item_repeat".to_string()),
content_index: 0,
delta: "Looping now".to_string(),
});
assert_eq!(late_delta_outcome.materialized_messages.len(), 1);
assert!(matches!(
&session.messages()[1],
Message::User(user) if user.text_content() == "Stop."
));
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_loop".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
})
.is_inert(),
"late completion for an interrupted response must not resurrect its deltas"
);
assert!(
session
.messages()
.iter()
.filter_map(|message| match message {
Message::BlockAssistant(assistant) => Some(block_assistant_text(assistant)),
_ => None,
})
.all(|text| !text.contains("Looping now")),
"late interrupted assistant text must remain non-canonical"
);
}
#[test]
fn realtime_transcript_completion_only_finalizes_matching_response() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "question".to_string(),
},
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "evt_a".to_string(),
item_id: "item_a".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "answer a".to_string(),
})
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_b".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
})
.is_inert(),
"a completion for another response must not finalize buffered assistant text"
);
assert_eq!(session.messages().len(), 1);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(outcome.materialized_messages.len(), 1);
assert_eq!(session.messages().len(), 2);
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "answer a"
));
}
#[test]
fn realtime_transcript_completion_before_later_delta_is_response_scoped() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "question".to_string(),
},
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
})
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_b".to_string(),
delta_id: "evt_b".to_string(),
item_id: "item_b".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "wrong response".to_string(),
})
.is_inert(),
"a later delta for another response must not be finalized by resp_a's pending completion"
);
let outcome =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "evt_a".to_string(),
item_id: "item_a".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "right response".to_string(),
});
assert_eq!(outcome.materialized_messages.len(), 1);
assert_eq!(session.messages().len(), 2);
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "right response"
));
}
#[test]
fn realtime_transcript_late_duplicate_completion_cannot_finalize_unrelated_response() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "question".to_string(),
},
);
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "evt_a".to_string(),
item_id: "item_a".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "first".to_string(),
});
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(session.messages().len(), 2);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_b".to_string(),
delta_id: "evt_b".to_string(),
item_id: "item_b".to_string(),
previous_item_id: Some("item_a".to_string()),
content_index: 0,
delta: "second".to_string(),
})
.is_inert()
);
assert!(
session
.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
})
.is_inert(),
"a duplicate late terminal for resp_a must not finalize resp_b"
);
assert_eq!(session.messages().len(), 2);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_b".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(outcome.materialized_messages.len(), 1);
assert_eq!(session.messages().len(), 3);
}
#[test]
fn realtime_transcript_interruption_discards_only_matching_response() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "item_user".to_string(),
previous_item_id: None,
content_index: 0,
text: "question".to_string(),
},
);
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "evt_a".to_string(),
item_id: "item_a".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "interrupted display".to_string(),
});
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_b".to_string(),
delta_id: "evt_b".to_string(),
item_id: "item_b".to_string(),
previous_item_id: Some("item_user".to_string()),
content_index: 0,
delta: "keep me".to_string(),
});
let interrupt_outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_a".to_string(),
},
);
assert_eq!(
interrupt_outcome.materialized_messages.len(),
1,
"resp_a's Display item commits on Interrupted"
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_b".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(
outcome.materialized_messages.len(),
1,
"resp_b commits on its TurnCompleted, untouched by resp_a's Interrupted"
);
assert_eq!(session.messages().len(), 3);
assert!(matches!(
&session.messages()[1],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "interrupted display"
));
assert!(matches!(
&session.messages()[2],
Message::BlockAssistant(assistant) if block_assistant_text(assistant) == "keep me"
));
}
#[test]
fn test_fork_shares_arc_no_clone() {
let mut session = Session::new();
for i in 0..100 {
session.push(Message::User(UserMessage::text(format!("Message {i}"))));
}
let forked = session.fork();
assert!(Arc::ptr_eq(session.messages.arc(), forked.messages.arc()));
assert_eq!(forked.messages().len(), 100);
}
#[test]
fn test_fork_at_shares_arc_prefix() {
let mut session = Session::new();
for i in 0..100 {
session.push(Message::User(UserMessage::text(format!("Message {i}"))));
}
let forked = session.fork_at(50);
assert_eq!(forked.messages().len(), 50);
assert_eq!(session.messages().len(), 100);
}
#[test]
fn test_fork_at_resets_transcript_history_state_for_branch_identity() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text(
"summarize this".to_string(),
)));
session.push(Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::Text {
text: "long assistant trace".to_string(),
meta: None,
}],
StopReason::EndTurn,
)));
let parent_revision = session.transcript_revision().expect("parent revision");
session
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 1, end: 2 },
vec![Message::BlockAssistant(BlockAssistantMessage::new(
vec![AssistantBlock::Text {
text: "compact trace".to_string(),
meta: None,
}],
StopReason::EndTurn,
))],
TranscriptRewriteReason::new("compaction"),
Some("test".to_string()),
Some(parent_revision),
)
.expect("rewrite should commit");
let source_head = session.transcript_revision().expect("source head");
let mut forked = session.fork_at(1);
assert_ne!(forked.id(), session.id());
assert!(
!forked
.metadata()
.contains_key(SESSION_TRANSCRIPT_HISTORY_STATE_KEY)
);
assert_eq!(
forked.transcript_revision().expect("fork head"),
transcript_messages_digest(forked.messages()).expect("fork digest")
);
assert!(
forked
.transcript_revision_messages(&source_head)
.expect("fork history lookup")
.is_none()
);
let fork_parent = forked.transcript_revision().expect("fork parent");
let commit = forked
.commit_transcript_rewrite(
TranscriptRewriteSelection::MessageRange { start: 0, end: 1 },
vec![Message::User(UserMessage::text(
"branch prompt".to_string(),
))],
TranscriptRewriteReason::new("branch_edit"),
Some("test".to_string()),
Some(fork_parent.clone()),
)
.expect("fork rewrite should use fork-local parent");
assert_eq!(commit.parent_revision, fork_parent);
}
#[test]
fn test_push_cow_behavior() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("First".to_string())));
let forked = session.fork();
assert!(Arc::ptr_eq(session.messages.arc(), forked.messages.arc()));
session.push(Message::User(UserMessage::text("Second".to_string())));
assert!(!Arc::ptr_eq(session.messages.arc(), forked.messages.arc()));
assert_eq!(session.messages().len(), 2);
assert_eq!(forked.messages().len(), 1);
}
#[test]
fn test_push_batch_single_timestamp() {
let mut session = Session::new();
let initial_updated = session.updated_at();
session.push_batch(vec![
Message::User(UserMessage::text("First".to_string())),
Message::User(UserMessage::text("Second".to_string())),
Message::User(UserMessage::text("Third".to_string())),
]);
assert_eq!(session.messages().len(), 3);
assert!(session.updated_at() >= initial_updated);
}
#[test]
fn test_touch_updates_timestamp() {
let mut session = Session::new();
let initial = session.updated_at();
std::thread::sleep(std::time::Duration::from_millis(10));
session.touch();
assert!(session.updated_at() > initial);
}
#[test]
fn test_session_push() {
let mut session = Session::new();
let initial_updated = session.updated_at();
std::thread::sleep(std::time::Duration::from_millis(10));
session.push(Message::User(UserMessage::text("Hello".to_string())));
assert_eq!(session.messages().len(), 1);
assert!(session.updated_at() > initial_updated);
}
#[test]
fn test_session_fork() {
let mut session = Session::new();
session.push(Message::System(SystemMessage::new("System prompt")));
session.push(Message::User(UserMessage::text("Hello".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "Hi!".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
let forked = session.fork_at(2);
assert_eq!(forked.messages().len(), 2);
assert_ne!(forked.id(), session.id());
let full_fork = session.fork();
assert_eq!(full_fork.messages().len(), 3);
}
#[test]
fn test_session_forks_drop_generated_authority_metadata() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("original")));
session.set_metadata("ordinary", serde_json::json!("keep"));
session
.set_build_state(SessionBuildState::default())
.expect("build state should serialize");
session
.set_deferred_turn_state(SessionDeferredTurnState::default())
.expect("deferred-turn state should serialize");
session
.set_tool_visibility_state(
AuthorizedSessionToolVisibilityState::from_generated_authority(
SessionToolVisibilityState::default(),
),
)
.expect("visibility state should serialize");
let _ = session.append_realtime_transcript_event(RealtimeTranscriptEvent::ItemObserved {
item_id: "rt-item".to_string(),
previous_item_id: None,
role: RealtimeTranscriptRole::User,
response_id: None,
});
session.metadata.insert(
crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY.to_string(),
serde_json::json!([{"sealed_projection": "must-not-fork"}]),
);
assert!(
!session
.metadata()
.contains_key(SESSION_REALTIME_TRANSCRIPT_STATE_KEY),
"typed realtime authority must not leak into the raw metadata map"
);
assert_eq!(
session
.realtime_component_event_prefix()
.expect("realtime component prefix")
.event_count(),
1,
"test setup should park one typed realtime event"
);
let forked_at = session.fork_at(1);
let full_fork = session.fork();
let replaced = session
.fork_replacing(
0,
TranscriptReplacement::Message {
message: Message::User(UserMessage::text("replacement")),
},
)
.expect("replacement fork should succeed");
for forked in [&forked_at, &full_fork, &replaced] {
assert_eq!(forked.metadata().get("ordinary").unwrap(), "keep");
assert!(
!forked.metadata().contains_key(SESSION_BUILD_STATE_KEY),
"forked sessions must not raw-copy durable build-state authority"
);
assert!(
!forked
.metadata()
.contains_key(SESSION_DEFERRED_TURN_STATE_KEY),
"forked sessions must not raw-copy deferred-turn authority state"
);
assert!(
!forked
.metadata()
.contains_key(SESSION_TOOL_VISIBILITY_STATE_KEY),
"forked sessions must not raw-copy tool-visibility authority state"
);
assert!(
!forked
.metadata()
.contains_key(SESSION_REALTIME_TRANSCRIPT_STATE_KEY),
"forked sessions must not raw-copy realtime transcript authority state"
);
assert_eq!(
forked
.realtime_component_event_prefix()
.expect("fork realtime component prefix")
.event_count(),
0,
"forked sessions must start a new empty realtime component lineage"
);
assert!(
!forked
.metadata()
.contains_key(crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY),
"forked sessions must not raw-copy compaction outbox authority"
);
}
}
#[test]
fn test_session_metadata() {
let mut session = Session::new();
session.set_metadata("key", serde_json::json!("value"));
assert_eq!(session.metadata().get("key").unwrap(), "value");
}
#[test]
fn identical_metadata_projection_is_wire_idempotent() {
let mut session = Session::new();
session.set_metadata("key", serde_json::json!({ "value": 1 }));
let updated_at = session.updated_at;
let bytes = session
.to_persisted_bytes()
.expect("session bytes before identical projection");
session.set_metadata("key", serde_json::json!({ "value": 1 }));
session.remove_metadata("already_absent");
assert_eq!(
session.updated_at, updated_at,
"an identical durable projection must not manufacture a content mutation"
);
assert_eq!(
session
.to_persisted_bytes()
.expect("session bytes after identical projection"),
bytes,
"an identical durable projection must not rotate current Session bytes"
);
}
#[test]
fn session_metadata_realm_id_is_back_read_compatible_string() {
let metadata = SessionMetadata {
schema_version: SESSION_METADATA_SCHEMA_VERSION,
model: "test-model".to_string(),
max_tokens: 1024,
structured_output_retries: 2,
provider: Provider::Other,
self_hosted_server_id: None,
provider_params: None,
tooling: SessionTooling::default(),
keep_alive: false,
comms_name: None,
peer_meta: None,
realm_id: Some(crate::RealmId::parse("env_default").unwrap()),
instance_id: None,
backend: None,
config_generation: None,
auth_binding: None,
mob_member_binding: None,
};
let value = serde_json::to_value(&metadata).unwrap();
assert_eq!(
value.get("realm_id"),
Some(&serde_json::json!("env_default")),
"typed realm_id must serialize as a bare slug string"
);
let legacy = serde_json::json!({
"schema_version": SESSION_METADATA_SCHEMA_VERSION,
"model": "test-model",
"max_tokens": 1024,
"structured_output_retries": 2,
"provider": "other",
"tooling": SessionTooling::default(),
"keep_alive": false,
"comms_name": null,
"realm_id": "legacy_realm",
});
let restored: SessionMetadata = serde_json::from_value(legacy).unwrap();
assert_eq!(
restored.realm_id.as_ref().map(crate::RealmId::as_str),
Some("legacy_realm")
);
}
#[test]
fn session_tooling_tool_access_policy_round_trip_and_absent_default() {
let legacy = serde_json::json!({});
let restored: SessionTooling = serde_json::from_value(legacy).unwrap();
assert_eq!(restored.tool_access_policy, None);
let value = serde_json::to_value(SessionTooling::default()).unwrap();
assert!(
value.get("tool_access_policy").is_none(),
"None policy must not serialize"
);
let tooling = SessionTooling {
tool_access_policy: Some(crate::ops::ToolAccessPolicy::AllowList(
["read_file", "send_message"].into_iter().collect(),
)),
..SessionTooling::default()
};
let value = serde_json::to_value(&tooling).unwrap();
let restored: SessionTooling = serde_json::from_value(value).unwrap();
assert_eq!(restored.tool_access_policy, tooling.tool_access_policy);
}
#[test]
fn lifecycle_terminal_typed_round_trip() {
let mut session = Session::new();
assert_eq!(session.lifecycle_terminal(), None);
session
.set_lifecycle_terminal(SessionLifecycleTerminal::Archived)
.expect("typed terminal write should serialize");
assert_eq!(
session.lifecycle_terminal(),
Some(SessionLifecycleTerminal::Archived)
);
assert!(
session
.lifecycle_terminal()
.is_some_and(SessionLifecycleTerminal::is_archived)
);
assert_eq!(
session
.metadata()
.get(SESSION_LIFECYCLE_TERMINAL_KEY)
.unwrap(),
&serde_json::json!("archived")
);
}
#[test]
fn recovered_head_adoption_keeps_archived_absorbing_from_either_copy() {
let mut archived_recovery = Session::new();
archived_recovery
.set_lifecycle_terminal(SessionLifecycleTerminal::Archived)
.expect("archive terminal serializes");
let mut active_head = archived_recovery.clone();
active_head
.set_lifecycle_terminal(SessionLifecycleTerminal::Active)
.expect("active terminal serializes");
archived_recovery
.adopt_recovered_head_state(&active_head)
.expect("generated lifecycle merge resolves");
assert_eq!(
archived_recovery.lifecycle_terminal(),
Some(SessionLifecycleTerminal::Archived),
"a newer Active projection must not resurrect an Archived recovery base"
);
let mut active_recovery = Session::new();
active_recovery
.set_lifecycle_terminal(SessionLifecycleTerminal::Active)
.expect("active terminal serializes");
let mut archived_head = active_recovery.clone();
archived_head
.set_lifecycle_terminal(SessionLifecycleTerminal::Archived)
.expect("archive terminal serializes");
active_recovery
.adopt_recovered_head_state(&archived_head)
.expect("generated lifecycle merge resolves");
assert_eq!(
active_recovery.lifecycle_terminal(),
Some(SessionLifecycleTerminal::Archived),
"an Archived durable head must remain terminal after recovery adoption"
);
}
#[test]
fn lifecycle_terminal_key_rejects_raw_mutation() {
let mut session = Session::new();
assert!(
session
.try_set_metadata(
SESSION_LIFECYCLE_TERMINAL_KEY,
serde_json::json!("archived")
)
.is_err(),
"the typed lifecycle-terminal key is reserved for session authority"
);
}
#[test]
fn test_session_metadata_backfill_preserves_timestamp() {
let mut session = Session::new();
let initial_updated = session.updated_at();
std::thread::sleep(std::time::Duration::from_millis(10));
assert!(session.backfill_metadata_if_absent("key", serde_json::json!("value")));
assert_eq!(session.metadata().get("key").unwrap(), "value");
assert_eq!(session.updated_at(), initial_updated);
assert!(!session.backfill_metadata_if_absent("key", serde_json::json!("other")));
assert_eq!(session.metadata().get("key").unwrap(), "value");
assert_eq!(session.updated_at(), initial_updated);
}
#[test]
fn test_reserved_generated_authority_metadata_rejects_raw_mutation() {
let mut session = Session::new();
assert!(
session
.try_set_metadata(SESSION_METADATA_KEY, serde_json::json!({}))
.is_err()
);
assert!(
session
.try_set_metadata(SESSION_BUILD_STATE_KEY, serde_json::json!({}))
.is_err()
);
assert!(
session
.try_set_metadata(
SESSION_TRANSCRIPT_REWRITE_PREFIX_AUTHORITY_KEY,
serde_json::json!({
"occurrence_count": 0,
"digest": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
})
)
.is_err(),
"raw metadata must not forge rewrite-prefix authority"
);
let compaction_intents_key = crate::memory::SESSION_COMPACTION_PROJECTION_INTENTS_KEY;
let sealed_compaction_intents =
serde_json::json!([{"sealed_projection": "typed-owner-only"}]);
session.metadata.insert(
compaction_intents_key.to_string(),
sealed_compaction_intents.clone(),
);
assert!(
session
.try_set_metadata(compaction_intents_key, serde_json::json!([]))
.is_err(),
"raw metadata must not overwrite compaction outbox authority"
);
session.remove_metadata(compaction_intents_key);
assert_eq!(
session.metadata().get(compaction_intents_key),
Some(&sealed_compaction_intents),
"raw metadata removal must not erase compaction outbox authority"
);
let mut absent = Session::new();
assert!(
!absent.backfill_metadata_if_absent(
compaction_intents_key,
serde_json::json!([{"forged_projection": true}])
),
"compatibility backfill must not fabricate compaction outbox authority"
);
assert!(!absent.metadata().contains_key(compaction_intents_key));
session
.set_session_metadata(SessionMetadata {
schema_version: SESSION_METADATA_SCHEMA_VERSION,
model: "test-model".to_string(),
max_tokens: 1024,
structured_output_retries: 2,
provider: Provider::Other,
self_hosted_server_id: None,
provider_params: None,
tooling: SessionTooling::default(),
keep_alive: false,
comms_name: None,
peer_meta: None,
realm_id: None,
instance_id: None,
backend: None,
config_generation: None,
auth_binding: None,
mob_member_binding: None,
})
.expect("typed metadata setter should route through generated authority");
session
.set_build_state(SessionBuildState::default())
.expect("typed build-state setter should route through generated authority");
session.remove_metadata(SESSION_METADATA_KEY);
session.remove_metadata(SESSION_BUILD_STATE_KEY);
assert!(
session.metadata().contains_key(SESSION_METADATA_KEY),
"raw removal must not delete generated-authority session metadata"
);
assert!(
session.metadata().contains_key(SESSION_BUILD_STATE_KEY),
"raw removal must not delete generated-authority build state"
);
session.set_metadata(SESSION_DEFERRED_TURN_STATE_KEY, serde_json::json!({}));
assert!(
!session
.metadata()
.contains_key(SESSION_DEFERRED_TURN_STATE_KEY)
);
session.metadata.insert(
SESSION_METADATA_KEY.to_string(),
serde_json::json!("not-metadata"),
);
assert!(
session.try_session_metadata().is_err(),
"malformed session metadata must not decode as absent/default"
);
session.metadata.insert(
SESSION_BUILD_STATE_KEY.to_string(),
serde_json::json!("not-build-state"),
);
assert!(
session.try_build_state().is_err(),
"malformed build state must not decode as absent/default"
);
assert!(
session
.try_set_metadata(SESSION_TOOL_VISIBILITY_STATE_KEY, serde_json::json!({}))
.is_err()
);
session
.set_tool_visibility_state(
AuthorizedSessionToolVisibilityState::from_generated_authority(
SessionToolVisibilityState::default(),
),
)
.expect("typed visibility setter should route through typed authority handoff");
session.remove_metadata(SESSION_TOOL_VISIBILITY_STATE_KEY);
assert!(
session
.metadata()
.contains_key(SESSION_TOOL_VISIBILITY_STATE_KEY)
);
session.clear_tool_visibility_state();
assert!(
!session
.metadata()
.contains_key(SESSION_TOOL_VISIBILITY_STATE_KEY)
);
assert!(
session
.try_set_metadata(SESSION_REALTIME_TRANSCRIPT_STATE_KEY, serde_json::json!({}))
.is_err()
);
let _ = session.append_realtime_transcript_event(RealtimeTranscriptEvent::ItemObserved {
item_id: "rt-item".to_string(),
previous_item_id: None,
role: RealtimeTranscriptRole::User,
response_id: None,
});
assert!(
!session
.metadata()
.contains_key(SESSION_REALTIME_TRANSCRIPT_STATE_KEY),
"typed realtime transcript append must not recreate raw shadow authority"
);
assert_eq!(
session
.realtime_component_event_prefix()
.expect("typed realtime prefix")
.event_count(),
1,
"typed append must advance the authenticated component prefix"
);
session.metadata.insert(
SESSION_REALTIME_TRANSCRIPT_STATE_KEY.to_string(),
serde_json::json!("not-a-state"),
);
let whole_blob =
serde_json::to_value(&session).expect("typed projection must override a raw shadow");
let projected = whole_blob
.get("metadata")
.and_then(serde_json::Value::as_object)
.and_then(|metadata| metadata.get(SESSION_REALTIME_TRANSCRIPT_STATE_KEY))
.expect("WholeBlob projection");
assert!(
serde_json::from_value::<SessionRealtimeTranscriptState>(projected.clone()).is_ok(),
"WholeBlob encoding must derive from typed authority, never a raw metadata shadow"
);
}
#[test]
fn test_session_mob_tool_authority_context_persists_projection_without_authority_seal() {
let mut session = Session::new();
session
.set_build_state(SessionBuildState::default())
.expect("session build state should serialize");
let authority = MobToolAuthorityContext::generated_for_test(
crate::service::OpaquePrincipalToken::new("opaque-principal"),
false,
false,
false,
std::collections::BTreeSet::from(["mob-a".to_string()]),
std::collections::BTreeMap::new(),
None,
Some("audit-1".to_string()),
);
session
.set_mob_tool_authority_context(Some(authority))
.expect("authority should serialize");
assert!(session.mob_tool_authority_context().is_none());
let stored = session
.build_state()
.and_then(|state| state.mob_tool_authority_context)
.expect("stored projection should deserialize");
assert!(!stored.is_generated_authority_context());
assert!(!stored.can_manage_mob("mob-a"));
session
.set_mob_tool_authority_context(None)
.expect("authority should clear");
assert!(session.mob_tool_authority_context().is_none());
}
#[test]
fn test_session_build_state_rejects_forged_mob_authority_projection() {
let mut session = Session::new();
let authority = MobToolAuthorityContext::generated_for_test(
crate::service::OpaquePrincipalToken::new("opaque-principal"),
false,
false,
false,
std::collections::BTreeSet::from(["mob-a".to_string()]),
std::collections::BTreeMap::new(),
None,
Some("audit-1".to_string()),
);
let forged_projection: MobToolAuthorityContext =
serde_json::from_value(serde_json::to_value(authority).expect("serialize authority"))
.expect("deserialize projection");
assert!(!forged_projection.is_generated_authority_context());
let err = session
.set_build_state(SessionBuildState {
mob_tool_authority_context: Some(forged_projection),
..Default::default()
})
.expect_err("forged build state must be rejected by generated authority");
assert!(
err.to_string()
.contains("generated session document authority rejected"),
"unexpected error: {err}"
);
}
#[test]
fn test_session_tool_visibility_state_roundtrip() {
let mut session = Session::new();
let state = SessionToolVisibilityState {
inherited_base_filter: ToolFilter::Allow(["visible".to_string()].into_iter().collect()),
active_filter: ToolFilter::Allow(
["visible".to_string(), "missing".to_string()]
.into_iter()
.collect(),
),
staged_filter: ToolFilter::Allow(
["visible".to_string(), "missing".to_string()]
.into_iter()
.collect(),
),
active_revision: 1,
staged_revision: 2,
..Default::default()
};
session
.set_tool_visibility_state(
AuthorizedSessionToolVisibilityState::from_generated_authority(state.clone()),
)
.expect("tool visibility state should serialize");
assert_eq!(session.tool_visibility_state().unwrap(), Some(state));
}
#[test]
fn test_session_tool_visibility_state_malformed_returns_error() {
let mut session = Session::new();
session.metadata.insert(
SESSION_TOOL_VISIBILITY_STATE_KEY.to_string(),
serde_json::json!({
"active_filter": {
"unexpected_filter_kind": ["secret"]
}
}),
);
assert!(
session.tool_visibility_state().is_err(),
"malformed canonical visibility metadata must not decode as absent/default"
);
}
#[test]
fn test_session_serialization() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("Test".to_string())));
let json = serde_json::to_string(&session).unwrap();
let parsed: Session = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id(), session.id());
assert_eq!(parsed.messages().len(), 1);
assert_eq!(parsed.version(), SESSION_VERSION);
}
#[test]
fn test_session_meta_from_session() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("Hello".to_string())));
session.push(Message::BlockAssistant(BlockAssistantMessage {
blocks: vec![AssistantBlock::Text {
text: "Hi!".to_string(),
meta: None,
}],
stop_reason: StopReason::EndTurn,
identity: crate::types::TranscriptMessageIdentity::default(),
created_at: crate::types::message_timestamp_now(),
}));
session.record_usage(Usage {
input_tokens: 10,
output_tokens: 5,
cache_creation_tokens: None,
cache_read_tokens: None,
});
let meta = SessionMeta::from(&session);
assert_eq!(meta.id, *session.id());
assert_eq!(meta.message_count, 2);
assert_eq!(meta.total_tokens, 15);
}
#[test]
fn deferred_tool_result_redelivery_is_idempotent_per_exact_payload() {
let mut state = SessionDeferredTurnState::default();
let results = vec![
ToolResult::new("callback-a".to_string(), "a".to_string(), false),
ToolResult::new("callback-b".to_string(), "b".to_string(), false),
];
assert_eq!(
state.stage_tool_results(results.clone(), SystemTime::UNIX_EPOCH),
2
);
let before = serde_json::to_value(&state).expect("serialize staged state");
assert_eq!(
state.stage_tool_results(
results,
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1),
),
0,
"identical redelivery must coalesce without restaging"
);
assert_eq!(
serde_json::to_value(&state).expect("serialize redelivered state"),
before,
"duplicate ingress must be a byte-identical no-op"
);
}
#[test]
fn deferred_tool_result_conflict_and_wrong_id_fail_without_poison_after_replay() {
let mut state = SessionDeferredTurnState::default();
state
.try_stage_tool_results(
vec![ToolResult::new(
"callback-a".to_string(),
"approved".to_string(),
false,
)],
SystemTime::UNIX_EPOCH,
)
.expect("first callback payload should stage");
let mut replayed: SessionDeferredTurnState = serde_json::from_value(
serde_json::to_value(&state).expect("serialize deferred callback state"),
)
.expect("restore deferred callback state");
let before = serde_json::to_value(&replayed).expect("serialize replayed state");
assert!(matches!(
replayed.try_stage_tool_results(
vec![ToolResult::new(
"callback-a".to_string(),
"denied".to_string(),
false,
)],
SystemTime::UNIX_EPOCH,
),
Err(DeferredToolResultsIngressError::ConflictingRedelivery(id))
if id == "callback-a"
));
assert_eq!(serde_json::to_value(&replayed).unwrap(), before);
assert!(matches!(
replayed.try_stage_tool_results(
vec![ToolResult::new(
"callback-b".to_string(),
"wrong".to_string(),
false,
)],
SystemTime::UNIX_EPOCH,
),
Err(DeferredToolResultsIngressError::WrongToolUseId(id))
if id == "callback-b"
));
assert_eq!(
serde_json::to_value(&replayed).unwrap(),
before,
"typed ingress refusals must leave the valid pending continuation intact"
);
}
#[test]
fn persisted_round_trip_preserves_multiple_systems_anywhere_exactly() {
let mut session = Session::new();
session.append_system_message("first");
session.push(Message::User(UserMessage::text("hello")));
session.append_system_message(" second ");
session.append_system_message("");
session.append_system_message(" second ");
let expected = session.messages().to_vec();
let bytes = serde_json::to_vec(&session).expect("serialize session");
let resumed: Session = serde_json::from_slice(&bytes).expect("deserialize session");
assert_eq!(resumed.messages(), expected.as_slice());
assert_eq!(resumed.messages_for_model_boundary(), expected);
}
#[test]
fn system_control_idempotency_is_explicit_and_does_not_coalesce_keyless_rows() {
let mut session = Session::new();
let timestamp = crate::types::message_timestamp_now();
let first = session
.append_system_message_idempotent(
" exact ",
Some("host".to_string()),
Some("key".to_string()),
timestamp,
)
.expect("first append");
assert_eq!(first, crate::service::AppendSystemContextStatus::Applied);
let duplicate = session
.append_system_message_idempotent(
" exact ",
Some("host".to_string()),
Some("key".to_string()),
timestamp,
)
.expect("exact retry");
assert_eq!(
duplicate,
crate::service::AppendSystemContextStatus::Duplicate
);
session
.append_system_message_idempotent("", None, None, timestamp)
.expect("empty keyless System");
session
.append_system_message_idempotent("", None, None, timestamp)
.expect("duplicate keyless System");
assert_eq!(
session
.messages()
.iter()
.filter(|message| matches!(message, Message::System(system) if system.content.is_empty()))
.count(),
2
);
assert!(matches!(
session.append_system_message_idempotent(
"different",
Some("host".to_string()),
Some("key".to_string()),
timestamp,
),
Err(SystemMessageAppendError::Conflict { .. })
));
}
#[test]
fn realtime_transcript_assistant_transcript_delta_materializes_transcript_block() {
let mut session = Session::new();
let delta = RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_spoken".to_string(),
delta_id: "evt_delta_spoken_1".to_string(),
item_id: "item_spoken".to_string(),
previous_item_id: None,
content_index: 0,
delta: "I said hi".to_string(),
};
assert!(
session.append_realtime_transcript_event(delta).is_inert(),
"delta alone is inert until turn-completed flushes"
);
let terminal = RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_spoken".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
};
let outcome = session.append_realtime_transcript_event(terminal);
assert_eq!(outcome.materialized_messages.len(), 1);
let messages = session.messages();
assert_eq!(messages.len(), 1);
match &messages[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Transcript { text, source, .. } => {
assert_eq!(text, "I said hi");
assert_eq!(*source, crate::types::TranscriptSource::Spoken);
}
other => unreachable!(
"AssistantTranscriptDelta must materialize as AssistantBlock::Transcript, got {other:?}"
),
}
}
other => unreachable!("expected BlockAssistant message, got {other:?}"),
}
}
#[test]
fn round4_cc4_in_flight_response_ids_lists_distinct_unmaterialized_responses() {
let mut session = Session::new();
for (i, response_id) in [
("resp_a", "resp_a"),
("resp_a_extra", "resp_a"),
("resp_b", "resp_b"),
("resp_c", "resp_c"),
]
.iter()
.enumerate()
{
let event = RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: response_id.1.to_string(),
delta_id: format!("delta_{i}"),
item_id: response_id.0.to_string(),
previous_item_id: None,
content_index: 0,
delta: "x".to_string(),
};
let _ = session.append_realtime_transcript_event(event);
}
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_c".to_string(),
},
);
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::UserTranscriptFinal {
item_id: "u_item".to_string(),
previous_item_id: None,
content_index: 0,
text: "hi".to_string(),
},
);
let in_flight = session.in_flight_realtime_assistant_response_ids();
assert!(in_flight.contains(&"resp_a".to_string()), "{in_flight:?}");
assert!(in_flight.contains(&"resp_b".to_string()), "{in_flight:?}");
assert!(
!in_flight.contains(&"resp_c".to_string()),
"discarded response must not appear in in_flight: {in_flight:?}"
);
assert_eq!(
in_flight.iter().filter(|r| *r == "resp_a").count(),
1,
"distinct response_ids only: {in_flight:?}"
);
}
#[test]
fn round4_cc2_assistant_turn_completed_after_transcript_deltas_materializes_transcript() {
let mut session = Session::new();
let delta = RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_cc2".to_string(),
delta_id: "delta_cc2_1".to_string(),
item_id: "item_cc2".to_string(),
previous_item_id: None,
content_index: 0,
delta: "hello world".to_string(),
};
assert!(session.append_realtime_transcript_event(delta).is_inert());
assert_eq!(
session.in_flight_realtime_assistant_response_ids(),
vec!["resp_cc2".to_string()]
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_cc2".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(outcome.materialized_messages.len(), 1);
assert!(
session
.in_flight_realtime_assistant_response_ids()
.is_empty(),
"materialized items must not appear in in_flight_realtime_assistant_response_ids"
);
let messages = session.messages();
let assistant = messages.iter().find_map(|m| match m {
Message::BlockAssistant(a) => Some(a),
_ => None,
});
let assistant = assistant.expect("assistant block message expected");
assert_eq!(assistant.blocks.len(), 1);
assert!(matches!(
&assistant.blocks[0],
AssistantBlock::Transcript {
source: crate::types::TranscriptSource::Spoken,
..
}
));
}
#[test]
fn realtime_transcript_assistant_text_delta_still_materializes_text_block() {
let mut session = Session::new();
let delta = RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_display".to_string(),
delta_id: "evt_delta_display_1".to_string(),
item_id: "item_display".to_string(),
previous_item_id: None,
content_index: 0,
delta: "I wrote".to_string(),
};
let _ = session.append_realtime_transcript_event(delta);
let terminal = RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_display".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
};
let outcome = session.append_realtime_transcript_event(terminal);
assert_eq!(outcome.materialized_messages.len(), 1);
let messages = session.messages();
match &messages[0] {
Message::BlockAssistant(assistant) => match &assistant.blocks[0] {
AssistantBlock::Text { text, .. } => assert_eq!(text, "I wrote"),
other => unreachable!(
"AssistantTextDelta must keep materializing AssistantBlock::Text, got {other:?}"
),
},
other => unreachable!("expected BlockAssistant message, got {other:?}"),
}
}
#[test]
fn round4_cc7_mixed_response_persists_text_and_transcript_in_order() {
let mut session = Session::new();
let display_a = RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_mixed_1".to_string(),
delta_id: "delta_disp_1".to_string(),
item_id: "item_display".to_string(),
previous_item_id: None,
content_index: 0,
delta: "Here's the report:".to_string(),
};
assert!(
session
.append_realtime_transcript_event(display_a)
.is_inert()
);
let display_b = RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_mixed_1".to_string(),
delta_id: "delta_disp_2".to_string(),
item_id: "item_display".to_string(),
previous_item_id: None,
content_index: 0,
delta: " (still writing)".to_string(),
};
assert!(
session
.append_realtime_transcript_event(display_b)
.is_inert()
);
let spoken_a = RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_mixed_1".to_string(),
delta_id: "delta_spoken_1".to_string(),
item_id: "item_spoken".to_string(),
previous_item_id: Some("item_display".to_string()),
content_index: 0,
delta: "I'm reading the report aloud:".to_string(),
};
assert!(
session
.append_realtime_transcript_event(spoken_a)
.is_inert()
);
let spoken_b = RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_mixed_1".to_string(),
delta_id: "delta_spoken_2".to_string(),
item_id: "item_spoken".to_string(),
previous_item_id: Some("item_display".to_string()),
content_index: 0,
delta: " sentence two.".to_string(),
};
assert!(
session
.append_realtime_transcript_event(spoken_b)
.is_inert()
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_mixed_1".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage {
input_tokens: 11,
output_tokens: 22,
cache_creation_tokens: None,
cache_read_tokens: None,
},
},
);
assert_eq!(outcome.materialized_messages.len(), 2);
let messages = session.messages();
let assistants: Vec<&BlockAssistantMessage> = messages
.iter()
.filter_map(|m| match m {
Message::BlockAssistant(a) => Some(a),
_ => None,
})
.collect();
assert_eq!(
assistants.len(),
1,
"mixed display+spoken response under one response_id must produce exactly ONE BlockAssistant message, got: {assistants:?}"
);
let assistant = assistants[0];
assert_eq!(
assistant.blocks.len(),
2,
"mixed response message must carry both blocks: {:?}",
assistant.blocks
);
match &assistant.blocks[0] {
AssistantBlock::Text { text, .. } => {
assert_eq!(text, "Here's the report: (still writing)");
}
other => unreachable!(
"first block must be AssistantBlock::Text (display lane), got {other:?}"
),
}
match &assistant.blocks[1] {
AssistantBlock::Transcript { text, source, .. } => {
assert_eq!(text, "I'm reading the report aloud: sentence two.");
assert_eq!(*source, crate::types::TranscriptSource::Spoken);
}
other => unreachable!(
"second block must be AssistantBlock::Transcript {{ source: Spoken }}, got {other:?}"
),
}
assert_eq!(session.usage.input_tokens, 11);
assert_eq!(session.usage.output_tokens, 22);
}
#[test]
fn round5_r55_mixed_response_barge_in_preserves_display_drops_spoken() {
let mut session = Session::new();
let display = RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_mixed_2".to_string(),
delta_id: "delta_disp_1".to_string(),
item_id: "item_display_2".to_string(),
previous_item_id: None,
content_index: 0,
delta: "Working on the report...".to_string(),
};
let _ = session.append_realtime_transcript_event(display);
let spoken = RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_mixed_2".to_string(),
delta_id: "delta_spoken_1".to_string(),
item_id: "item_spoken_2".to_string(),
previous_item_id: Some("item_display_2".to_string()),
content_index: 0,
delta: "I'm reading the report".to_string(),
};
let _ = session.append_realtime_transcript_event(spoken);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_mixed_2".to_string(),
},
);
assert_eq!(
outcome.materialized_messages.len(),
1,
"Display lane item must materialize on Interrupted: {outcome:?}"
);
let late_completion = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_mixed_2".to_string(),
stop_reason: StopReason::Cancelled,
usage: Usage::default(),
},
);
assert_eq!(
late_completion.materialized_messages.len(),
0,
"post-barge-in TurnCompleted must not resurrect anything"
);
let messages = session.messages();
let assistants: Vec<&BlockAssistantMessage> = messages
.iter()
.filter_map(|m| match m {
Message::BlockAssistant(a) => Some(a),
_ => None,
})
.collect();
assert_eq!(
assistants.len(),
1,
"barge-in must commit exactly one BlockAssistant containing the Display lane: {assistants:?}"
);
let assistant = assistants[0];
assert_eq!(assistant.blocks.len(), 1, "blocks: {:?}", assistant.blocks);
match &assistant.blocks[0] {
AssistantBlock::Text { text, .. } => {
assert_eq!(text, "Working on the report...");
}
other => {
unreachable!("Display lane must materialize as AssistantBlock::Text, got {other:?}")
}
}
assert!(
!assistant
.blocks
.iter()
.any(|b| matches!(b, AssistantBlock::Transcript { .. })),
"Spoken lane must be dropped on barge-in"
);
assert!(
!session
.in_flight_realtime_assistant_response_ids()
.contains(&"resp_mixed_2".to_string()),
"barged-in response must not appear in in_flight_realtime_assistant_response_ids"
);
}
#[test]
fn round5_r55_barge_in_preserves_display_lane_drops_spoken() {
let mut session = Session::new();
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_d_1".to_string(),
item_id: "item_display".to_string(),
previous_item_id: None,
content_index: 0,
delta: "display-text".to_string(),
});
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_s_1".to_string(),
item_id: "item_spoken".to_string(),
previous_item_id: None,
content_index: 0,
delta: "spoken-transcript".to_string(),
},
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_a".to_string(),
},
);
assert_eq!(outcome.materialized_messages.len(), 1);
let messages = session.messages();
let assistants: Vec<&BlockAssistantMessage> = messages
.iter()
.filter_map(|m| match m {
Message::BlockAssistant(a) => Some(a),
_ => None,
})
.collect();
assert_eq!(assistants.len(), 1);
assert_eq!(assistants[0].blocks.len(), 1);
match &assistants[0].blocks[0] {
AssistantBlock::Text { text, .. } => assert_eq!(text, "display-text"),
other => unreachable!("expected Text, got {other:?}"),
}
}
#[test]
fn round5_r55_barge_in_finalizes_retained_display_into_committed_block() {
let mut session = Session::new();
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_d_1".to_string(),
item_id: "item_display".to_string(),
previous_item_id: None,
content_index: 0,
delta: "committed-display-text".to_string(),
});
assert!(session.messages().is_empty());
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnInterrupted {
response_id: "resp_a".to_string(),
},
);
assert_eq!(
outcome.materialized_messages.len(),
1,
"Interrupted must finalize retained Display lane immediately"
);
let messages = session.messages();
assert_eq!(messages.len(), 1);
match &messages[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Text { text, .. } => {
assert_eq!(text, "committed-display-text");
}
other => unreachable!("expected Text, got {other:?}"),
}
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn round5_r56_truncation_promotes_default_lane_item_to_spoken() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptTruncated {
response_id: "resp_a".to_string(),
item_id: "item_a".to_string(),
content_index: 0,
text: "what was actually heard".to_string(),
},
);
let outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(outcome.materialized_messages.len(), 1);
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Transcript { text, source, .. } => {
assert_eq!(text, "what was actually heard");
assert_eq!(*source, crate::types::TranscriptSource::Spoken);
}
other => unreachable!(
"truncation-only path must materialize as AssistantBlock::Transcript, got {other:?}"
),
}
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn round5_r56_truncation_after_display_delta_is_no_op_keeping_display_content() {
let mut session = Session::new();
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_d_1".to_string(),
item_id: "item_a".to_string(),
previous_item_id: None,
content_index: 0,
delta: "display-text-from-delta".to_string(),
});
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptTruncated {
response_id: "resp_a".to_string(),
item_id: "item_a".to_string(),
content_index: 0,
text: "spoken-truncation-text".to_string(),
},
);
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Text { text, .. } => {
assert_eq!(text, "display-text-from-delta");
}
other => unreachable!(
"Display content must survive misrouted truncation, got {other:?}"
),
}
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn round5_r56_sibling_display_delta_skipped_on_spoken_item() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptTruncated {
response_id: "resp_a".to_string(),
item_id: "item_a".to_string(),
content_index: 0,
text: "what was actually heard".to_string(),
},
);
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_d_1".to_string(),
item_id: "item_a".to_string(),
previous_item_id: None,
content_index: 0,
delta: "should-not-appear".to_string(),
});
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Transcript { text, source, .. } => {
assert_eq!(text, "what was actually heard");
assert_eq!(*source, crate::types::TranscriptSource::Spoken);
}
other => unreachable!(
"Spoken-locked item must materialize as Transcript, got {other:?}"
),
}
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn round5_r56_sibling_spoken_delta_skipped_on_display_item() {
let mut session = Session::new();
let _ =
session.append_realtime_transcript_event(RealtimeTranscriptEvent::AssistantTextDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_d_1".to_string(),
item_id: "item_a".to_string(),
previous_item_id: None,
content_index: 0,
delta: "display-locked-text".to_string(),
});
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_s_1".to_string(),
item_id: "item_a".to_string(),
previous_item_id: None,
content_index: 0,
delta: "should-not-appear".to_string(),
},
);
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Text { text, .. } => {
assert_eq!(text, "display-locked-text");
}
other => {
unreachable!("Display-locked item must materialize as Text, got {other:?}")
}
}
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
#[test]
fn round5_r57_late_final_text_after_turn_completed_warns_and_skips() {
let mut session = Session::new();
let _ = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptDelta {
response_id: "resp_a".to_string(),
delta_id: "delta_s_1".to_string(),
item_id: "item_a".to_string(),
previous_item_id: None,
content_index: 0,
delta: "delta-accumulated".to_string(),
},
);
let commit_outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTurnCompleted {
response_id: "resp_a".to_string(),
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
},
);
assert_eq!(commit_outcome.materialized_messages.len(), 1);
let late_outcome = session.append_realtime_transcript_event(
RealtimeTranscriptEvent::AssistantTranscriptFinalText {
response_id: "resp_a".to_string(),
item_id: "item_a".to_string(),
content_index: 0,
text: "authoritative-final-that-must-not-land".to_string(),
},
);
assert!(
late_outcome.is_inert(),
"late FinalText after materialization must produce inert outcome"
);
assert_eq!(session.messages().len(), 1);
match &session.messages()[0] {
Message::BlockAssistant(assistant) => {
assert_eq!(assistant.blocks.len(), 1);
match &assistant.blocks[0] {
AssistantBlock::Transcript { text, .. } => {
assert_eq!(
text, "delta-accumulated",
"canonical message must preserve delta-accumulated text; \
append-only history forbids late FinalText repair"
);
}
other => unreachable!("expected Transcript, got {other:?}"),
}
}
other => unreachable!("expected BlockAssistant, got {other:?}"),
}
}
fn metadata_seam_session_metadata() -> SessionMetadata {
SessionMetadata {
schema_version: SESSION_METADATA_SCHEMA_VERSION,
model: "test-model".to_string(),
max_tokens: 1024,
structured_output_retries: 2,
provider: Provider::Anthropic,
self_hosted_server_id: None,
provider_params: None,
tooling: SessionTooling::default(),
keep_alive: false,
comms_name: Some("team/reviewer/alice".to_string()),
peer_meta: None,
realm_id: None,
instance_id: None,
backend: None,
config_generation: None,
auth_binding: None,
mob_member_binding: Some(crate::MobMemberBinding {
mob_id: "team".to_string(),
role: "reviewer".to_string(),
member: "alice".to_string(),
}),
}
}
#[test]
fn session_metadata_document_lockstep_with_full_envelope() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("hello".to_string())));
session
.set_session_metadata(metadata_seam_session_metadata())
.expect("session metadata should persist");
session
.set_lifecycle_terminal(SessionLifecycleTerminal::Archived)
.expect("lifecycle terminal should persist");
let bytes = serde_json::to_vec(&session).expect("session should serialize");
let document = session_metadata_document_from_slice(&bytes)
.expect("partial decode must accept the canonical envelope");
assert_eq!(document.session_id(), session.id());
assert_eq!(
document.session_metadata_value(),
session.metadata().get(SESSION_METADATA_KEY),
"partial decode must project the identical raw session-metadata value"
);
assert_eq!(
document.lifecycle_terminal_value(),
session.metadata().get(SESSION_LIFECYCLE_TERMINAL_KEY),
"partial decode must project the identical raw lifecycle-terminal value"
);
let view = document
.try_into_view()
.expect("typed view must decode from the partial document");
let full_view =
PersistedSessionMetadataView::try_from_session(&session).expect("full-session view");
assert_eq!(view.session_id, full_view.session_id);
assert_eq!(
view.session_metadata.as_ref().map(|m| m.model.clone()),
full_view.session_metadata.as_ref().map(|m| m.model.clone())
);
assert_eq!(
view.mob_member_binding(),
full_view.mob_member_binding(),
"typed binding must be identical across the two decode paths"
);
assert_eq!(
view.lifecycle_terminal,
Some(SessionLifecycleTerminal::Archived)
);
assert_eq!(
full_view.lifecycle_terminal,
Some(SessionLifecycleTerminal::Archived)
);
}
#[test]
fn session_metadata_document_fails_closed_on_envelope_version() {
let session = Session::new();
let mut value = serde_json::to_value(&session).expect("session should serialize");
value["version"] = serde_json::json!(SESSION_VERSION + 999);
let bytes = serde_json::to_vec(&value).expect("mangled envelope should serialize");
session_metadata_document_from_slice(&bytes)
.expect_err("an unsupported envelope version must fail the partial decode closed");
}
#[test]
fn current_session_deserializer_rejects_released_envelope_version() {
let session = Session::new();
let mut value = serde_json::to_value(&session).expect("session should serialize");
value["version"] = serde_json::json!(2);
let bytes = serde_json::to_vec(&value).expect("released envelope should serialize");
Session::from_persisted_bytes(&bytes)
.expect_err("ordinary Session decode must accept only current envelope v3");
}
#[test]
fn persisted_session_metadata_view_fails_closed_on_corrupt_values() {
let session_id = SessionId::new();
let mut corrupt_metadata = serde_json::Map::new();
corrupt_metadata.insert(SESSION_METADATA_KEY.to_string(), serde_json::json!(42));
PersistedSessionMetadataView::try_from_metadata_map(session_id.clone(), &corrupt_metadata)
.expect_err("corrupt session_metadata must fail the view decode closed");
let mut corrupt_terminal = serde_json::Map::new();
corrupt_terminal.insert(
SESSION_LIFECYCLE_TERMINAL_KEY.to_string(),
serde_json::json!("definitely-not-a-terminal"),
);
PersistedSessionMetadataView::try_from_metadata_map(session_id, &corrupt_terminal)
.expect_err("corrupt lifecycle terminal must fail the view decode closed");
}
#[test]
fn persisted_session_metadata_view_reads_absent_facts_as_none() {
let view = PersistedSessionMetadataView::try_from_metadata_map(
SessionId::new(),
&serde_json::Map::new(),
)
.expect("empty metadata map must decode");
assert!(view.session_metadata.is_none());
assert!(view.lifecycle_terminal.is_none());
assert!(view.mob_member_binding().is_none());
}
}