blob: cc1fb6471c2f1762136e945367567af68596bef0 [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::MAX_SAFE_INTEGER;
use crate::StoreError;
use sqlx::{Connection, SqliteConnection};
use std::time::{SystemTime, UNIX_EPOCH};
mod partial;
pub(crate) fn register_functions(connection: &rusqlite::Connection) -> Result<(), StoreError> {
use rusqlite::functions::FunctionFlags;
let flags = FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC;
connection.create_scalar_function("catalog_time", 1, flags, |ctx| {
let time: SystemTime = serde_json::from_str(ctx.get_raw(0).as_str()?)
.map_err(|error| rusqlite::Error::UserFunctionError(Box::new(error)))?;
time.duration_since(UNIX_EPOCH)
.ok()
.and_then(|duration| u64::try_from(duration.as_millis()).ok())
.filter(|value| *value <= MAX_SAFE_INTEGER)
.map(|value| value as i64)
.ok_or(rusqlite::Error::InvalidQuery)
})?;
partial::register_function(connection)
}
pub(crate) async fn initialize_execution(
connection: &mut SqliteConnection,
) -> Result<(), StoreError> {
// Only data is disposable; SQLx owns the schema. Rebuild a cleared cache
// or watermark in the same transaction before exposing catalog readers.
let mut tx = connection.begin().await?;
let complete: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM catalog_messages)
AND EXISTS(SELECT 1 FROM catalog_message_watermark WHERE singleton = 1)",
)
.fetch_one(&mut *tx)
.await?;
if !complete {
sqlx::raw_sql(
"DELETE FROM catalog_messages;
DELETE FROM catalog_message_watermark;",
)
.execute(&mut *tx)
.await?;
}
sqlx::query("INSERT OR IGNORE INTO catalog_message_watermark VALUES (1, 0)")
.execute(&mut *tx)
.await?;
let mut through: i64 =
sqlx::query_scalar("SELECT sequence FROM catalog_message_watermark WHERE singleton = 1")
.fetch_one(&mut *tx)
.await?;
loop {
// Startup only: at most 128 small row identifiers, never event JSON in Rust.
let rows = sqlx::query_scalar::<_, i64>(
"SELECT sequence FROM runtime_events WHERE sequence > ?
AND kind IN ('message_imported', 'invocation_opened', 'message_steered', 'model_completed', 'model_interrupted', 'invocation_ended')
AND NOT EXISTS(SELECT 1 FROM session_retirements r
WHERE r.session_id=runtime_events.event_session AND r.remove_session=1 AND r.completed=1)
ORDER BY sequence LIMIT 128",
).bind(through).fetch_all(&mut *tx).await?;
if rows.is_empty() {
break;
}
for sequence in rows {
project_execution(&mut tx, sequence as u64).await?;
through = sequence;
}
}
sqlx::query(
"UPDATE catalog_message_watermark SET sequence =
(SELECT COALESCE(max(sequence), 0) FROM runtime_events)",
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
/// Called only for committed catalog boundaries, in the event append transaction.
/// This is a rebuildable index of message facts, not another execution authority.
pub(crate) async fn project_execution(
connection: &mut SqliteConnection,
sequence: u64,
) -> Result<(), StoreError> {
let sequence = sequence as i64;
let (session, invocation, step, kind): (String, String, Option<String>, String) =
sqlx::query_as(
"SELECT json_extract(event_json, '$.invocation.session_id'), invocation_id,
operation_id, kind FROM runtime_events WHERE sequence = ?",
)
.bind(sequence)
.fetch_one(&mut *connection)
.await?;
// Both accepted and interrupted summary text belong to context evidence,
// not the visible message tail used by previews and read markers.
let compact: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id = ?
AND kind = 'invocation_opened'
AND json_extract(event_json, '$.fact.input.kind') = 'context_compact')",
)
.bind(&invocation)
.fetch_one(&mut *connection)
.await?;
let hidden_step = if matches!(kind.as_str(), "model_completed" | "model_interrupted") {
!sqlx::query_scalar::<_, bool>(
"SELECT EXISTS (SELECT 1 FROM runtime_events request
JOIN runtime_events opening ON opening.invocation_id = request.invocation_id
AND opening.kind = 'invocation_opened'
WHERE request.kind = 'model_requested' AND request.invocation_id = ?1
AND request.operation_id = ?2
AND json_extract(opening.event_json, '$.fact.input.kind') IN ('message', 'continuation', 'handoff')
AND json_extract(request.event_json, '$.fact.purpose') = 'main')",
)
.bind(&invocation)
.bind(&step)
.fetch_one(&mut *connection)
.await?
} else {
false
};
if compact || hidden_step {
sqlx::query(
"UPDATE catalog_message_watermark SET sequence = max(sequence, ?) WHERE singleton = 1",
)
.bind(sequence)
.execute(connection)
.await?;
return Ok(());
}
let prior_clock: i64 = sqlx::query_scalar(
"SELECT message_at FROM catalog_messages INDEXED BY catalog_latest_message
WHERE session_id = ? ORDER BY message_at DESC, sequence DESC, ordinal DESC LIMIT 1",
)
.bind(&session)
.fetch_optional(&mut *connection)
.await?
.unwrap_or(0);
if kind == "message_imported" {
sqlx::query(
"INSERT OR IGNORE INTO catalog_messages
SELECT sequence,0,?2,catalog_time(json_extract(event_json,'$.recorded_at')),
catalog_preview(json_extract(event_json,'$.fact.record.content.text')),event_id
FROM runtime_events WHERE sequence=?1
AND json_extract(event_json,'$.fact.record.content.kind') IN ('user','assistant')",
)
.bind(sequence)
.bind(&session)
.execute(&mut *connection)
.await?;
} else if kind == "invocation_opened" {
sqlx::query(
"INSERT OR IGNORE INTO catalog_messages
SELECT sequence, 0, ?2, catalog_time(json_extract(event_json, '$.recorded_at')),
catalog_preview(COALESCE(
json_extract(event_json, '$.fact.input.content.display_text'),
json_extract(event_json, '$.fact.input.content.text'))),
CASE WHEN json_array_length(event_json, '$.fact.input.source_messages') = 1
THEN json_extract(event_json, '$.fact.input.source_messages[0].message_id') ELSE event_id END
FROM runtime_events WHERE sequence = ?1
AND json_extract(event_json, '$.fact.input.kind') = 'message'",
)
.bind(sequence)
.bind(&session)
.execute(&mut *connection)
.await?;
} else if kind == "message_steered" {
crate::steering::project_catalog(connection, sequence, &session).await?;
} else if kind == "model_completed" {
// Current step only, excluding every delta and every earlier completion.
// Accepted text keeps the PartStarted time used by transcript projection.
sqlx::query(
"WITH starts AS (
SELECT catalog_time(json_extract(event_json, '$.recorded_at')) AS ts, event_id,
row_number() OVER (ORDER BY sequence) AS ordinal
FROM event_log INDEXED BY catalog_part_starts
WHERE json_extract(event_json, '$.invocation.session_id') = ?2
AND invocation_id = ?3 AND json_extract(event_json, '$.fact.step_id') = ?4
AND kind = 'model_observed'
AND json_extract(event_json, '$.fact.event.kind') = 'part_started'
), accepted AS (
SELECT row_number() OVER (ORDER BY CAST(part.key AS INTEGER)) AS ordinal,
CASE WHEN json_extract(part.value, '$.text_kind') = 'text'
THEN catalog_preview(json_extract(part.value, '$.text')) END AS preview
FROM runtime_events AS event, json_each(event.event_json, '$.fact.output.parts') AS part
WHERE event.sequence = ?1 AND json_extract(part.value, '$.kind') = 'text'
)
INSERT OR IGNORE INTO catalog_messages
SELECT ?1, ordinal, ?2, starts.ts, accepted.preview, starts.event_id
FROM starts JOIN accepted USING (ordinal)",
).bind(sequence).bind(&session).bind(&invocation).bind(&step)
.execute(&mut *connection).await?;
}
if kind == "model_interrupted" || kind == "invocation_ended" {
partial::project(connection, sequence, &session, &invocation, step.as_deref()).await?;
}
// TS rejects preview updates older than the previous message clock, even if
// that clock came from a thinking-only message without any preview. Apply the
// same guard in commit/part order, scanning only this boundary's new rows.
sqlx::query(
"UPDATE catalog_messages AS current SET preview = NULL
WHERE sequence = ?1 AND message_at < max(?2, COALESCE((
SELECT max(earlier.message_at) FROM catalog_messages AS earlier
WHERE earlier.sequence = ?1 AND earlier.ordinal < current.ordinal
), 0))",
)
.bind(sequence)
.bind(prior_clock)
.execute(&mut *connection)
.await?;
sqlx::query(
"UPDATE catalog_message_watermark SET sequence = max(sequence, ?) WHERE singleton = 1",
)
.bind(sequence)
.execute(&mut *connection)
.await?;
Ok(())
}
/// Durable user/assistant tail in presentation order, independent of preview clocks.
pub(crate) async fn latest_visible_message(
connection: &mut SqliteConnection,
session: &str,
) -> Result<Option<String>, StoreError> {
Ok(sqlx::query_scalar(
"SELECT message_id FROM catalog_messages INDEXED BY catalog_visible_tail
WHERE session_id = ? ORDER BY sequence DESC, ordinal DESC LIMIT 1",
)
.bind(session)
.fetch_optional(connection)
.await?)
}