blob: 4d044a8699467e990c95a3aee5d25c5f9bd460bc [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, safety, selection::Selection};
use crate::StoreError;
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::RuntimeEvent;
use sqlx::SqliteConnection;
pub(super) async fn source_fence(
connection: &mut SqliteConnection,
selection: &Selection,
opening: Option<&(i64, RuntimeEvent)>,
mode: &CheckpointMode,
through: u64,
) -> Result<u64, StoreError> {
let session = selection
.session
.as_deref()
.ok_or_else(|| invalid("context requires a Session"))?;
let Some((opened, event)) = opening else {
if !matches!(mode, CheckpointMode::Standalone) {
return Err(invalid(
"automatic compaction requires a live model invocation",
));
}
let high = selection
.high_water(connection, i64::MAX as u64 - 1)
.await?;
if high > 0 {
safety::closed_boundary(connection, session, high).await?;
}
return Ok(high);
};
let Fact::InvocationOpened { input, .. } = &event.fact else {
return Err(invalid("missing canonical opening"));
};
let before = match (mode, input) {
(CheckpointMode::Standalone, InvocationInput::ContextCompact { .. })
| (
CheckpointMode::PreTurn,
InvocationInput::Message { .. }
| InvocationInput::Continuation { .. }
| InvocationInput::Handoff { .. },
) => *opened,
(
CheckpointMode::MidTurn { anchor_event_id },
InvocationInput::Message { .. }
| InvocationInput::Continuation { .. }
| InvocationInput::Handoff { .. },
) => {
if anchor_event_id != &event.id {
return Err(invalid(
"mid-turn anchor is not the exact canonical opening",
));
}
through as i64
}
_ => return Err(invalid("checkpoint mode does not match its opening")),
};
let high = selection.high_water(connection, before as u64 - 1).await?;
if matches!(mode, CheckpointMode::MidTurn { .. }) {
if high <= *opened as u64 {
return Err(invalid(
"mid-turn source has no completed work after its anchor",
));
}
safety::settled_boundary(connection, &event.invocation.invocation_id, high).await?;
} else if high > 0 {
safety::closed_boundary(connection, session, high).await?;
}
Ok(high)
}
/// The frozen source key owns a round. Route/digest changes cannot create a new
/// repair budget; the first request's existing step identity is the round root.
pub(super) async fn summary_start(
connection: &mut SqliteConnection,
invocation: &str,
scope: &maka_agent_facts::event::LogScope,
high: u64,
through: u64,
) -> Result<Option<u64>, StoreError> {
let first: Option<i64> = sqlx::query_scalar(
"SELECT MIN(sequence) FROM runtime_events WHERE invocation_id = ?1
AND kind = 'model_requested' AND sequence < ?2
AND json_extract(event_json, '$.fact.purpose') = 'summary'
AND json_extract(event_json, '$.fact.source_high_water') = ?3
AND json_extract(event_json, '$.fact.source_scope') = json(?4)",
)
.bind(invocation)
.bind(through as i64)
.bind(high as i64)
.bind(serde_json::to_string(scope)?)
.fetch_one(connection)
.await?;
first.map(crate::sequence_number).transpose()
}
/// Renewal must be Main progress covered by the new source. A fast Main can
/// finish before the prior Summary request commits; request arrival order is
/// not a progress fence, and late repairs must not roll that fence backward.
pub(super) async fn admit_summary(
connection: &mut SqliteConnection,
invocation: &str,
first: Option<u64>,
high: u64,
) -> Result<(), StoreError> {
let (pending, attempts, renewed): (bool, i64, bool) = sqlx::query_as(
"WITH summaries AS (
SELECT * FROM runtime_events WHERE invocation_id = ?1 AND kind = 'model_requested'
AND json_extract(event_json, '$.fact.purpose') = 'summary')
SELECT EXISTS(SELECT 1 FROM summaries s WHERE NOT EXISTS(
SELECT 1 FROM runtime_events t WHERE t.invocation_id=s.invocation_id
AND t.operation_id=s.operation_id AND t.kind IN ('model_completed','model_interrupted'))),
(SELECT COUNT(*) FROM summaries WHERE sequence >= ?2),
NOT EXISTS(SELECT 1 FROM summaries) OR EXISTS(
SELECT 1 FROM runtime_events c JOIN runtime_events r
ON r.invocation_id=c.invocation_id AND r.kind='model_requested'
AND r.operation_id=CASE WHEN c.kind='model_observed'
THEN json_extract(c.event_json,'$.fact.step_id') ELSE c.operation_id END
WHERE c.invocation_id=?1 AND (c.kind='model_completed' OR
(c.kind='model_observed' AND json_extract(r.event_json,'$.fact.item_acceptance')=1
AND json_extract(c.event_json,'$.fact.event.kind') IN
('part_finished','tool_call','provider_tool_result','source')))
AND json_extract(r.event_json, '$.fact.purpose')='main'
AND c.sequence <= ?3 AND c.sequence > (
SELECT MAX(json_extract(event_json,'$.fact.source_high_water')) FROM summaries))",
).bind(invocation).bind(first.map(|v| v as i64)).bind(high as i64).fetch_one(connection).await?;
if pending || (first.is_some() && attempts >= 3) || (first.is_none() && !renewed) {
return Err(invalid(
"summary requires settled work and a bounded repair budget",
));
}
Ok(())
}