blob: b35ade75a3387c847b3885a5bf38062d65ad35f2 [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.
*/
use super::{invalid, proof, read, safety, selection::Selection};
use crate::StoreError;
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::context::CompactOutcome;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use sqlx::SqliteConnection;
pub(crate) async fn validate_append(
connection: &mut SqliteConnection,
event: &RuntimeEvent,
verification: &mut super::cache::Verification,
) -> Result<(), StoreError> {
super::request::validate(connection, event, verification).await?;
match &event.fact {
Fact::ContextCheckpointRecorded { checkpoint } => {
safety::current_opening(
connection,
&event.invocation.session_id,
Some(&event.invocation.invocation_id),
)
.await?;
safety::require_safe(
connection,
&event.invocation.session_id,
Some(&event.invocation.invocation_id),
)
.await?;
safety::settled_boundary(connection, &event.invocation.invocation_id, i64::MAX as u64)
.await?;
let (_, opening) = proof::by_kind(
connection,
&event.invocation.invocation_id,
"invocation_opened",
None,
)
.await?;
let selection = Selection::for_opening(connection, &opening).await?;
let current = read::latest_record(connection, &selection, i64::MAX as u64)
.await?
.map(|record| record.event.id);
if current != checkpoint.previous_checkpoint_id {
return Err(invalid("checkpoint predecessor changed before adoption"));
}
verification.select(&selection);
chain_cached(
connection,
event,
i64::MAX as u64,
false,
None,
verification,
)
.await?;
}
Fact::InvocationEnded { outcome } => {
let compact: bool = sqlx::query_scalar(
"SELECT json_extract(event_json, '$.fact.input.kind') = 'context_compact'
FROM runtime_events WHERE invocation_id = ? AND kind = 'invocation_opened'",
)
.bind(&event.invocation.invocation_id)
.fetch_one(&mut *connection)
.await?;
match outcome {
InvocationOutcome::ContextCompactFinished { outcome } => {
if !compact {
return Err(invalid("compact terminal belongs to ordinary invocation"));
}
if let CompactOutcome::Compacted { checkpoint_id } = outcome {
let (_, checkpoint) = proof::by_id(connection, checkpoint_id).await?;
if checkpoint.invocation != event.invocation
|| !matches!(checkpoint.fact, Fact::ContextCheckpointRecorded { .. })
{
return Err(invalid("terminal references another checkpoint"));
}
}
let pending: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM runtime_events d WHERE invocation_id = ? AND kind IN ('tool_dispatched','model_requested')
AND NOT EXISTS(SELECT 1 FROM runtime_events t WHERE t.invocation_id = d.invocation_id AND t.operation_id = d.operation_id
AND t.kind IN ('tool_settled','model_completed','model_interrupted')))",
).bind(&event.invocation.invocation_id).fetch_one(connection).await?;
if pending {
return Err(invalid("compact terminal has unresolved execution"));
}
}
InvocationOutcome::Completed if compact => {
return Err(invalid("compact requires a domain terminal"));
}
_ => {}
}
}
_ => {}
}
Ok(())
}
/// Strictly decreasing coverage and record sequence make cycles impossible.
/// An optional current source cut must be at or after the first record.
pub(super) async fn chain_cached(
connection: &mut SqliteConnection,
first: &RuntimeEvent,
first_sequence: u64,
committed: bool,
source: Option<(&Selection, u64)>,
verification: &mut super::cache::Verification,
) -> Result<Option<String>, StoreError> {
let mut event = first.clone();
let mut sequence = first_sequence;
let mut needs_terminal = committed;
let mut sources: Vec<(Selection, Vec<(u64, String)>)> = Vec::new();
loop {
let Fact::ContextCheckpointRecorded { checkpoint } = &event.fact else {
return Err(invalid("predecessor is not a checkpoint"));
};
if verification.anchor(&event.id, sequence, checkpoint.covered_through) {
break;
}
#[cfg(test)]
verification.validated_link();
let selection = proof::validate(connection, &event, sequence, checkpoint).await?;
if needs_terminal && matches!(checkpoint.mode, CheckpointMode::Standalone) {
require_terminal(connection, &event).await?;
}
// A missing predecessor cannot silently discard the already adopted baseline.
let latest = read::latest_record(connection, &selection, checkpoint.covered_through)
.await?
.map(|r| r.event.id);
if latest != checkpoint.previous_checkpoint_id {
return Err(invalid("predecessor is not the selected source baseline"));
}
// Coverage decreases along the chain. Only identical selections share
// a scan; lineage predicates and copied-history ownership stay intact.
let expected = (checkpoint.covered_through, checkpoint.source_digest.clone());
if let Some((prior, prefixes)) = sources.last_mut()
&& *prior == selection
{
prefixes.push(expected);
} else {
sources.push((selection, vec![expected]));
}
let Some(id) = &checkpoint.previous_checkpoint_id else {
break;
};
let (previous_sequence, previous) = proof::by_id(connection, id).await?;
let Fact::ContextCheckpointRecorded {
checkpoint: predecessor,
} = &previous.fact
else {
return Err(invalid("predecessor has wrong kind"));
};
if previous.invocation.session_id != event.invocation.session_id
|| previous_sequence >= sequence
|| previous_sequence > checkpoint.covered_through
|| predecessor.covered_through >= checkpoint.covered_through
{
return Err(invalid(
"checkpoint coverage must advance in the same Session",
));
}
event = previous;
sequence = previous_sequence;
needs_terminal = true;
}
let mut source_digest = None;
for (selection, prefixes) in sources {
let mut throughs: Vec<_> = prefixes.iter().rev().map(|(through, _)| *through).collect();
// The current source and checkpoint prefixes share raw-byte hashing
// only when their full selection (including lineage) is identical.
let current =
source.filter(|(selected, _)| **selected == selection && source_digest.is_none());
if let Some((_, through)) = current {
throughs.push(through);
}
let mut actual = verification
.digests(connection, &selection, &throughs)
.await?;
if current.is_some() {
// Current coverage follows the first checkpoint record, so its
// digest is after every checkpoint source boundary in the stream.
source_digest = actual.pop();
}
if actual
.iter()
.zip(prefixes.iter().rev())
.any(|(actual, (_, expected))| actual != expected)
{
return Err(invalid("source digest changed"));
}
}
if let Some((selection, through)) = source
&& source_digest.is_none()
{
source_digest = Some(
verification
.digests(connection, selection, &[through])
.await?
.pop()
.expect("one requested prefix"),
);
}
if committed {
verification.adopt(first, first_sequence);
}
Ok(source_digest)
}
pub(crate) async fn require_terminal(
connection: &mut SqliteConnection,
checkpoint: &RuntimeEvent,
) -> Result<(), StoreError> {
let (_, terminal) = proof::by_kind(
connection,
&checkpoint.invocation.invocation_id,
"invocation_ended",
None,
)
.await?;
if terminal.invocation != checkpoint.invocation
|| !matches!(&terminal.fact,
Fact::InvocationEnded { outcome: InvocationOutcome::ContextCompactFinished { outcome: CompactOutcome::Compacted { checkpoint_id } } } if checkpoint_id == &checkpoint.id)
{
return Err(invalid("checkpoint lacks its atomic compacted terminal"));
}
Ok(())
}
pub(crate) async fn validate_batch(
connection: &mut SqliteConnection,
events: &[EventWrite],
) -> Result<(), StoreError> {
for write in events {
if matches!(&write.event().fact, Fact::ContextCheckpointRecorded { checkpoint } if matches!(checkpoint.mode, CheckpointMode::Standalone))
{
require_terminal(connection, write.event()).await?;
}
}
Ok(())
}