blob: 405eaecb1909f29a40aa4a24869ca2f85e8559e8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Fixed Session history inherited before a fresh Run. This is source evidence,
//! not permission to resume or replay that Run's effects.
use super::{
ModelContextSource, SourceEvidence, evidence, invalid, latest_main, proof, read, safety,
selection::Selection,
};
use crate::{EventLog, StoreError};
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::LogScope;
use sqlx::{Connection, SqliteConnection};
impl EventLog {
/// Capture the complete closed Session base before this exact fresh opening.
/// The opening's own message belongs to its Run segment, not to this base.
pub async fn context_before_run(
&self,
invocation: &Invocation,
max_events: usize,
max_bytes: usize,
) -> Result<ModelContextSource, StoreError> {
self.validate_root()?;
for id in [
&invocation.session_id,
&invocation.turn_id,
&invocation.run_id,
&invocation.invocation_id,
] {
crate::sessions::validate_id(id)?;
}
let invocation = invocation.clone();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let selected = crate::turns::run_invocation(
&mut tx,
&invocation.session_id,
&invocation.run_id,
)
.await?;
if selected.as_deref() != Some(&invocation.invocation_id) {
return Err(invalid("base Run identity is missing or changed"));
}
let (sequence, opening) = proof::by_kind(
&mut tx,
&invocation.invocation_id,
"invocation_opened",
None,
)
.await?;
if opening.invocation != invocation
|| !matches!(
opening.fact,
Fact::InvocationOpened {
input: InvocationInput::Message { .. },
..
}
)
{
return Err(invalid(
"Session base requires an exact fresh Message opening",
));
}
let high_water =
evidence::high_water(&mut tx, &invocation.session_id, sequence as i64)
.await?;
let source = closed(
&mut tx,
&invocation.session_id,
high_water,
max_events,
max_bytes,
)
.await?;
tx.commit().await?;
Ok(source)
})
})
.await
}
/// Rebuild exactly the recorded closed Session base, including its then-current
/// checkpoint, archive projection and Main diagnostics. Later facts are excluded.
/// The expected raw digest authenticates the entire source, not only the visible tail.
pub async fn read_frozen_model_context(
&self,
expected: &SourceEvidence,
max_events: usize,
max_bytes: usize,
) -> Result<ModelContextSource, StoreError> {
self.validate_root()?;
let session = match &expected.scope {
LogScope::Session { id } | LogScope::Lineage { session_id: id, .. } => id,
LogScope::Root => {
return Err(invalid(
"frozen context requires a Session or lineage scope",
));
}
};
crate::sessions::validate_id(session)?;
if expected.high_water >= i64::MAX as u64 {
return Err(invalid("frozen context cut exceeds ledger capacity"));
}
let (scope, high_water, digest) = (
expected.scope.clone(),
expected.high_water,
expected.digest.clone(),
);
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let selection = Selection::resolve(&mut tx, &scope).await?;
let source =
closed_selected(&mut tx, &selection, high_water, max_events, max_bytes)
.await?;
if source.source_evidence.digest != digest {
return Err(invalid("frozen Session context source changed"));
}
tx.commit().await?;
Ok(source)
})
})
.await
}
}
async fn closed(
connection: &mut SqliteConnection,
session: &str,
high_water: u64,
max_events: usize,
max_bytes: usize,
) -> Result<ModelContextSource, StoreError> {
closed_selected(
connection,
&Selection::session(session),
high_water,
max_events,
max_bytes,
)
.await
}
pub(super) async fn closed_selected(
connection: &mut SqliteConnection,
selection: &Selection,
high_water: u64,
max_events: usize,
max_bytes: usize,
) -> Result<ModelContextSource, StoreError> {
let session = selection
.session
.as_deref()
.ok_or_else(|| invalid("context requires a Session"))?;
if high_water > 0 {
safety::closed_boundary(connection, session, high_water).await?;
}
safety::require_safe_through(connection, session, None, high_water).await?;
let latest_main = latest_main::read_selected(connection, selection, high_water).await?;
read::materialize_selected(
connection,
selection,
high_water,
high_water + 1,
max_events,
max_bytes,
latest_main,
)
.await
}
pub(crate) async fn verify_base(
connection: &mut SqliteConnection,
invocation: &Invocation,
opening_sequence: u64,
expected: &maka_agent_facts::continuation::SessionBase,
) -> Result<(), StoreError> {
if fresh_base(connection, invocation, opening_sequence).await? != *expected {
return Err(invalid("continuation's fresh Session base changed"));
}
Ok(())
}
pub(crate) async fn fresh_base(
connection: &mut SqliteConnection,
invocation: &Invocation,
opening_sequence: u64,
) -> Result<maka_agent_facts::continuation::SessionBase, StoreError> {
let session = &invocation.session_id;
let through = evidence::high_water(connection, session, opening_sequence as i64).await?;
Ok(maka_agent_facts::continuation::SessionBase {
high_water: through,
digest: evidence::evidence(connection, session, through)
.await?
.digest,
})
}