blob: bb4e2123385d74cb65ba093c0d1724650b1afaa2 [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::{
read::{row_position, sql_number, sql_position},
tool_metadata::read_on_worker as tool_metadata,
};
use crate::{EventLog, StoreError, sequence_number};
use maka_agent_facts::history::RowPosition;
use maka_agent_ports::session::history::Chunk;
use maka_agent_ports::session::history::Cursor;
use maka_agent_ports::session::history::Page;
use maka_agent_ports::session::history::Position;
use maka_agent_ports::session::history::Role;
use maka_agent_ports::session::history::{HistoryField, HistoryView};
use sqlx::{Row, sqlite::SqliteWorkerConnection};
// Text windows and exact source reads retain material labels in their body.
pub(super) const BODY: &str = "COALESCE(CASE json_extract(payload, '$.type')
WHEN 'user' THEN (SELECT group_concat(line, char(10)) FROM (
SELECT COALESCE(json_extract(payload, '$.displayText'), json_extract(payload, '$.text'), '') AS line
WHERE COALESCE(json_extract(payload, '$.displayText'), json_extract(payload, '$.text'), '') != ''
UNION ALL
SELECT '↳ ' || json_extract(value, '$.name') || CASE
WHEN json_type(value, '$.bytes') IS NOT 'integer' THEN ''
WHEN json_extract(value, '$.bytes') < 1024 THEN ' · ' || json_extract(value, '$.bytes') || ' B'
WHEN json_extract(value, '$.bytes') < 1048576 THEN printf(' · %.1f KiB', json_extract(value, '$.bytes') / 1024.0)
ELSE printf(' · %.1f MiB', json_extract(value, '$.bytes') / 1048576.0) END
FROM json_each(payload, '$.attachments') WHERE json_type(value, '$.name') = 'text'
UNION ALL
SELECT '▱ ' || json_extract(value, '$.path')
FROM json_each(payload, '$.directoryReferences') WHERE json_type(value, '$.path') = 'text'))
WHEN 'assistant' THEN json_extract(payload, '$.text')
WHEN 'model_interruption' THEN json_extract(payload, '$.message')
WHEN 'turn_state' THEN CASE WHEN json_extract(payload, '$.status') = 'failed' THEN
COALESCE(NULLIF(json_extract(payload, '$.failureMessage'), ''), NULLIF(json_extract(payload, '$.errorClass'), ''), 'failed') END
WHEN 'tool_call' THEN COALESCE(json_extract(payload, '$.intent'), json_extract(payload, '$.args.intent'), '')
WHEN 'tool_result' THEN CASE json_extract(payload, '$.content.kind')
WHEN 'text' THEN json_extract(payload, '$.content.text')
WHEN 'json' THEN (SELECT group_concat(atom, char(10)) FROM json_tree(payload, '$.content.value') WHERE type = 'text')
WHEN 'operation' THEN (SELECT group_concat(atom, char(10)) FROM json_tree(payload, '$.content.reply.receipt.outcome.value') WHERE type = 'text')
ELSE '' END
ELSE '' END, '')";
// Conversation windows carry attachments separately. Directory references
// still need their labels because they have no structured history field.
const CONVERSATION_USER: &str = "COALESCE((SELECT group_concat(line, char(10)) FROM (
SELECT COALESCE(json_extract(payload, '$.displayText'), json_extract(payload, '$.text'), '') AS line
WHERE COALESCE(json_extract(payload, '$.displayText'), json_extract(payload, '$.text'), '') != ''
UNION ALL
SELECT '▱ ' || json_extract(value, '$.path')
FROM json_each(payload, '$.directoryReferences') WHERE json_type(value, '$.path') = 'text')), '')";
impl EventLog {
/// Exact ascending UTF-8 windows over immutable derived text. The byte and
/// fragment budgets apply to the whole window, never to a silently clipped
/// message. Before reads a prefix ending exclusively at its cursor; After
/// starts inclusively. `max_json_bytes` targets the complete serialized page,
/// including metadata and empty projection boundaries. The first complete
/// fragment advances even when its metadata exceeds this target; subsequent
/// fragments are packed within it. Cursor positions grant no access.
#[allow(clippy::too_many_arguments)]
pub async fn history_text(
&self,
session: &str,
through: u64,
position: Position,
include_thinking: bool,
max_bytes: usize,
max_chunks: usize,
max_json_bytes: usize,
) -> Result<Page, StoreError> {
self.history_view(
session,
through,
position,
include_thinking,
HistoryView::Text,
max_bytes,
Some(max_chunks),
max_json_bytes,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn history_view(
&self,
session: &str,
through: u64,
position: Position,
include_thinking: bool,
view: HistoryView,
max_bytes: usize,
max_chunks: Option<usize>,
max_json_bytes: usize,
) -> Result<Page, StoreError> {
self.validate_root()?;
if !(4..=128 * 1024).contains(&max_bytes)
|| max_chunks == Some(0)
|| !(1024..=1024 * 1024).contains(&max_json_bytes)
{
return Err(invalid("invalid history window limits"));
}
crate::sessions::validate_id(session)?;
let fence = through;
sql_number(fence)?;
// A serialized chunk occupies at least one byte. The delivery budget
// therefore bounds rows even when a caller needs no item-count limit.
let max_chunks = max_chunks.unwrap_or(max_json_bytes).min(max_json_bytes);
let session = session.to_owned();
self.connection
.run(move |connection| {
super::bodycache::run(connection, move |tx| {
Box::pin(async move {
super::directory::check_read(tx, &session, fence).await?;
crate::connection::worker_query(tx, move |worker| {
Box::pin(async move {
read_page(
worker,
&session,
through,
position,
include_thinking,
view,
max_bytes,
max_chunks,
max_json_bytes,
)
.await
})
})
.await
})
})
})
.await
}
}
// Execute one bounded page with the original SQLx statement cache and row
// decoders. bodycache::run still owns transaction completion and maintenance.
#[allow(clippy::too_many_arguments)]
async fn read_page(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
through: u64,
position: Position,
include_thinking: bool,
view: HistoryView,
max_bytes: usize,
max_chunks: usize,
max_json_bytes: usize,
) -> Result<Page, StoreError> {
let fence = through;
let through_sql = sql_number(through)?;
let mut final_outputs = std::collections::HashMap::new();
let exists: bool =
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM session_control WHERE id = ?)")
.bind(session)
.fetch_one(&mut *tx)
.await?;
if !exists {
return Err(invalid("Session no longer exists"));
}
let target = if let Position::Row { position, field } = position {
if position.validate().is_err()
|| position.source_sequence > fence
|| (!include_thinking && field == HistoryField::Thinking)
{
return Err(invalid("history row exceeds its fence"));
}
materialize(tx, session, position, view, through_sql).await?;
// Header lookup requires the actual field; a tool Body cannot
// impersonate a Thinking field that was never recorded.
header(tx, session, position, field, view, through_sql).await?;
Some((position, field))
} else {
None
};
let anchor = match position {
Position::Before { cursor } | Position::After { cursor } => {
if cursor.position.source_sequence > fence
|| (!include_thinking && cursor.field == HistoryField::Thinking)
{
return Err(invalid("history cursor exceeds its fence"));
}
sql_number(cursor.offset)?;
materialize(tx, session, cursor.position, view, through_sql).await?;
let row = header(
tx,
session,
cursor.position,
cursor.field,
view,
through_sql,
)
.await?;
if cursor.offset > row.total {
return Err(invalid("history offset exceeds message"));
}
let byte = if cursor.offset == row.total {
Vec::new()
} else {
fragment(
tx,
session,
cursor,
1,
view == HistoryView::Conversation && row.role == "user",
)
.await?
};
if byte.first().is_some_and(|byte| byte & 0xc0 == 0x80) {
return Err(invalid("history offset splits UTF-8"));
}
Some((cursor, row))
}
_ => None,
};
let (reverse, boundary, inclusive) = match position {
Position::Start => (false, None, true),
Position::Tail => (true, None, true),
Position::Row { position, .. } => (false, Some(position), true),
Position::Before { cursor } => (
true,
Some(cursor.position),
cursor.offset > 0 || (include_thinking && cursor.field == HistoryField::Body),
),
Position::After { cursor } => (
false,
Some(cursor.position),
cursor.offset < anchor.as_ref().unwrap().1.total
|| cursor.field == HistoryField::Thinking,
),
Position::Event { sequence } => {
if sequence == 0 || sequence > through {
return Err(invalid("history event exceeds its fence"));
}
(
false,
Some(RowPosition {
source_sequence: sequence,
item_index: 0,
}),
true,
)
}
};
let query = match (reverse, boundary.is_some(), inclusive) {
(true, false, _) => {
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id=?1 AND source_sequence<=?2 ORDER BY source_sequence DESC,item_index DESC LIMIT ?3"
}
(false, false, _) => {
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id=?1 AND source_sequence<=?2 ORDER BY source_sequence,item_index LIMIT ?3"
}
(true, true, true) => {
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id=?1 AND (source_sequence,item_index)<=(?3,?4) ORDER BY source_sequence DESC,item_index DESC LIMIT ?2"
}
(true, true, false) => {
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id=?1 AND (source_sequence,item_index)<(?3,?4) ORDER BY source_sequence DESC,item_index DESC LIMIT ?2"
}
(false, true, true) => {
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id=?1 AND source_sequence<=?2 AND (source_sequence,item_index)>=(?4,?5) ORDER BY source_sequence,item_index LIMIT ?3"
}
(false, true, false) => {
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id=?1 AND source_sequence<=?2 AND (source_sequence,item_index)>(?4,?5) ORDER BY source_sequence,item_index LIMIT ?3"
}
};
let mut query = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(query)).bind(session);
// A validated reverse anchor already implies the raw fence. Keep the
// cursor pair as the sole upper bound so SQLite seeks directly to it.
if !(reverse && boundary.is_some()) {
query = query.bind(through_sql);
}
query = query.bind((max_chunks + 1) as i64);
if let Some(boundary) = boundary {
let (source, item) = sql_position(boundary)?;
query = query.bind(source).bind(item);
}
let rows = query.fetch_all(&mut *tx).await?;
let mut chunks = Vec::new();
let mut chunks_json_bytes = 0;
let mut bytes = 0;
let mut projected = 0;
let mut left = None;
let mut right = None;
'rows: for locator in rows {
if chunks.len() == max_chunks
|| bytes == max_bytes
|| (projected >= 1024 * 1024 && left.is_some())
{
break;
}
let locator = row_position(locator.0, locator.1)?;
// Materialize only actually visited rows, never a long message's
// following rows. One oversized first row still progresses.
projected += materialize(tx, session, locator, view, through_sql).await?;
let has_thinking: bool = include_thinking && sqlx::query_scalar(
"SELECT has_thinking FROM transcript_directory WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
).bind(session).bind(sql_position(locator)?.0).bind(sql_position(locator)?.1).fetch_one(&mut *tx).await?;
let mut fields = vec![HistoryField::Body];
if include_thinking && has_thinking {
fields.insert(0, HistoryField::Thinking);
}
if reverse {
fields.reverse();
}
for field in fields {
if chunks.len() == max_chunks || bytes == max_bytes {
break 'rows;
}
if target == Some((locator, HistoryField::Body)) && field == HistoryField::Thinking {
continue;
}
let row = header(tx, session, locator, field, view, through_sql).await?;
let exact_target = target == Some((locator, field));
if let Some((cursor, _)) = &anchor
&& cursor.position == locator
{
let before = field == HistoryField::Thinking && cursor.field == HistoryField::Body;
let after = field == HistoryField::Body && cursor.field == HistoryField::Thinking;
if (reverse && after) || (!reverse && before) {
continue;
}
if field == cursor.field
&& ((reverse && cursor.offset == 0) || (!reverse && cursor.offset == row.total))
{
continue;
}
}
let cursor_offset = anchor
.as_ref()
.filter(|(cursor, _)| cursor.position == locator && cursor.field == field)
.map(|(cursor, _)| cursor.offset);
let (mut start, end) = if reverse {
let end = cursor_offset.unwrap_or(row.total);
(end.saturating_sub((max_bytes - bytes) as u64), end)
} else {
let start = cursor_offset.unwrap_or(0);
(start, row.total.min(start + (max_bytes - bytes) as u64))
};
let fragment = if row.total == 0 {
Vec::new()
} else {
fragment(
tx,
session,
Cursor {
field,
position: locator,
offset: start,
},
end - start,
view == HistoryView::Conversation && row.role == "user",
)
.await?
};
let text = if reverse {
let skip = fragment
.iter()
.take_while(|byte| **byte & 0xc0 == 0x80)
.count();
start += skip as u64;
std::str::from_utf8(&fragment[skip..])
.map_err(|_| invalid("invalid history UTF-8"))?
} else {
match std::str::from_utf8(&fragment) {
Ok(text) => text,
Err(error) if error.error_len().is_none() => {
std::str::from_utf8(&fragment[..error.valid_up_to()])
.map_err(|_| invalid("invalid history UTF-8"))?
}
Err(_) => return Err(invalid("invalid history UTF-8")),
}
};
if text.is_empty() && row.total > 0 {
break 'rows;
}
let end = start + text.len() as u64;
let beginning = Cursor {
field,
position: locator,
offset: start,
};
let ending = (
Cursor {
field,
position: locator,
offset: end,
},
row.total,
);
let candidate_left = if reverse {
Some(beginning)
} else {
left.or(Some(beginning))
};
let candidate_right = if reverse {
right.or(Some(ending))
} else {
Some(ending)
};
let attachments: Vec<maka_agent_facts::attachment::AttachmentRef> = if start == 0
&& row.role == "user"
{
let encoded: Option<String> = sqlx::query_scalar(
"SELECT json_extract(payload, '$.attachments') FROM transcript_cache.rows WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
).bind(session).bind(sql_position(locator)?.0).bind(sql_position(locator)?.1).fetch_one(&mut *tx).await?;
encoded
.map(|value| serde_json::from_str(&value))
.transpose()?
.unwrap_or_default()
} else {
Vec::new()
};
let emit = (!row.hidden || exact_target)
&& (exact_target
|| row.total != 0
|| matches!(row.role.as_str(), "tool_call" | "tool_result")
|| (view == HistoryView::Conversation && !attachments.is_empty())
|| (view == HistoryView::Conversation && row.role == "context_compacted"));
if emit {
let role = match row.role.as_str() {
"user" => Role::User,
"assistant" => Role::Assistant,
"tool_call" => Role::ToolCall,
"tool_result" => Role::ToolResult,
"context_compacted" => Role::Compaction,
"turn_state" | "model_interruption" => Role::Failure,
_ => return Err(invalid("invalid history text role")),
};
let tool = if matches!(role, Role::ToolCall | Role::ToolResult) {
Some(tool_metadata(tx, session, locator, through_sql).await?)
} else {
None
};
let timing =
super::timing::read_on_worker(tx, session, &row.turn, through_sql).await?;
let compaction = if role == Role::Compaction {
let duration: i64 = sqlx::query_scalar(
"SELECT json_extract(payload, '$.durationMs') FROM transcript_cache.rows WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
).bind(session).bind(sql_position(locator)?.0).bind(sql_position(locator)?.1).fetch_one(&mut *tx).await?;
Some(maka_agent_ports::session::history::Compaction {
duration_ms: sequence_number(duration)?,
})
} else {
None
};
let (model, final_output) = if role == Role::Assistant {
let output = match final_outputs.entry(row.turn.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
std::collections::hash_map::Entry::Vacant(entry) => entry.insert(
super::final_output::resolve_on_worker(
tx,
session,
&row.turn,
through_sql,
)
.await?,
),
};
let metadata =
super::model::resolve_on_worker(tx, session, locator, through_sql).await?;
let final_output = field == HistoryField::Body
&& row.total > 0
&& metadata
.producer
.as_ref()
.is_some_and(|producer| output.as_ref() == Some(producer));
(metadata.model, final_output)
} else {
(None, false)
};
let chunk = Chunk {
field,
timing,
compaction,
origin: row.origin,
model,
final_output,
message_id: row.message,
turn_id: row.turn,
timestamp: row.timestamp,
role,
position: locator,
offset: start,
total_bytes: row.total,
text: text.to_owned(),
attachments,
tool,
};
chunks_json_bytes += serde_json::to_vec(&chunk)?.len();
chunks.push(chunk);
}
// Bound the complete canonical delivery, including attachment
// descriptors and JSON escaping. A rejected candidate does not
// advance the source boundary or silently discard any metadata.
let (before, after) = boundaries(
tx,
session,
through_sql,
include_thinking,
candidate_left,
candidate_right,
)
.await?;
// The empty envelope includes []; each chunk adds its exact
// encoded bytes, plus commas between elements. Cursor changes
// still count even when this boundary emits no chunk.
let envelope = Page::Ready {
through,
chunks: Vec::new(),
before,
after,
};
let fits = serde_json::to_vec(&envelope)?.len()
+ chunks_json_bytes
+ chunks.len().saturating_sub(1)
<= max_json_bytes;
if !fits && chunks.len() > usize::from(emit) {
if emit {
chunks.pop();
}
break 'rows;
}
bytes += text.len();
left = candidate_left;
right = candidate_right;
if (!reverse && end < row.total) || (reverse && start > 0) {
break 'rows;
}
}
}
if reverse {
chunks.reverse();
}
if left.is_none() {
if let Some((cursor, row)) = anchor {
left = Some(cursor);
right = Some((cursor, row.total));
} else if matches!(position, Position::Event { .. }) {
let last: Option<(i64,i64)> = sqlx::query_as(
"SELECT source_sequence,item_index FROM transcript_directory WHERE session_id = ?1 AND source_sequence <= ?2 ORDER BY source_sequence DESC,item_index DESC LIMIT 1"
).bind(session).bind(through_sql).fetch_optional(&mut *tx).await?;
if let Some(locator) = last {
let locator = row_position(locator.0, locator.1)?;
materialize(tx, session, locator, view, through_sql).await?;
let row =
header(tx, session, locator, HistoryField::Body, view, through_sql).await?;
let cursor = Cursor {
field: HistoryField::Body,
position: locator,
offset: row.total,
};
left = Some(cursor);
right = Some((cursor, row.total));
}
}
}
let (before, after) =
boundaries(tx, session, through_sql, include_thinking, left, right).await?;
Ok(Page::Ready {
through,
chunks,
before,
after,
})
}
async fn boundaries(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
through: i64,
include_thinking: bool,
left: Option<Cursor>,
right: Option<(Cursor, u64)>,
) -> Result<(Option<Cursor>, Option<Cursor>), StoreError> {
let before = match left {
Some(cursor)
if cursor.offset > 0
|| neighbor(tx, session, through, cursor, include_thinking, true).await? =>
{
Some(cursor)
}
_ => None,
};
let after = match right {
Some((cursor, total))
if cursor.offset < total
|| neighbor(tx, session, through, cursor, include_thinking, false).await? =>
{
Some(cursor)
}
_ => None,
};
Ok((before, after))
}
struct Header {
origin: Option<maka_agent_facts::message::MessageOrigin>,
message: String,
turn: String,
timestamp: u64,
role: String,
hidden: bool,
total: u64,
}
async fn header(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
locator: RowPosition,
field: HistoryField,
view: HistoryView,
through: i64,
) -> Result<Header, StoreError> {
if view == HistoryView::Conversation
&& field == HistoryField::Body
&& let Some(row) = conversation_tool(tx, session, locator, through).await?
{
return Ok(row);
}
let row = sqlx::query(
"SELECT source.message_id, source.turn_id, source.timestamp, source.role,
CASE WHEN source.role = 'turn_state' AND source.state_status IS NOT 'failed' AND ?4 = 'body'
THEN 0 ELSE length(cached.body) END,
CASE WHEN source.role = 'user' THEN source.user_origin END
FROM transcript_directory source LEFT JOIN transcript_cache.text cached
ON cached.session_id = source.session_id AND cached.source_sequence = source.source_sequence
AND cached.item_index = source.item_index AND cached.field = ?4
WHERE source.session_id = ?1 AND source.source_sequence = ?2 AND source.item_index = ?3
AND (cached.field IS NOT NULL OR
(source.role = 'turn_state' AND source.state_status IS NOT 'failed' AND ?4 = 'body'))"
).bind(session).bind(sql_position(locator)?.0).bind(sql_position(locator)?.1).bind(field_name(field)).fetch_one(&mut *tx).await?;
let mut header = Header {
origin: row
.try_get::<Option<String>, _>(5)?
.map(|value| serde_json::from_str(&value))
.transpose()?,
message: row.try_get(0)?,
turn: row.try_get(1)?,
timestamp: sequence_number(row.try_get(2)?)?,
role: row.try_get(3)?,
hidden: false,
total: sequence_number(row.try_get(4)?)?,
};
if view == HistoryView::Conversation && field == HistoryField::Body && header.role == "user" {
let query = format!(
"SELECT length(CAST({CONVERSATION_USER} AS BLOB)) FROM transcript_cache.rows
WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
);
header.total = sequence_number(
sqlx::query_scalar(sqlx::AssertSqlSafe(query.as_str()))
.bind(session)
.bind(sql_position(locator)?.0)
.bind(sql_position(locator)?.1)
.fetch_one(tx)
.await?,
)?;
}
Ok(header)
}
async fn fragment(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
cursor: Cursor,
bytes: u64,
conversation_user: bool,
) -> Result<Vec<u8>, StoreError> {
let query = if conversation_user {
format!(
"SELECT substr(CAST({CONVERSATION_USER} AS BLOB), ?4, ?5) FROM transcript_cache.rows
WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
)
} else {
"SELECT substr(body, ?4, ?5) FROM transcript_cache.text
WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3 AND field = ?6"
.into()
};
let mut query = sqlx::query_scalar(sqlx::AssertSqlSafe(query.as_str()))
.bind(session)
.bind(sql_position(cursor.position)?.0)
.bind(sql_position(cursor.position)?.1)
.bind(sql_number(cursor.offset)? + 1)
.bind(sql_number(bytes)?);
if !conversation_user {
query = query.bind(field_name(cursor.field));
}
Ok(query.fetch_one(tx).await?)
}
async fn conversation_tool(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
locator: RowPosition,
through: i64,
) -> Result<Option<Header>, StoreError> {
let row = sqlx::query(
"SELECT row.message_id, row.turn_id, row.timestamp,
row.role, row.tool_origin,
COALESCE(row.tool_name, call.tool_name),
COALESCE(row.is_error, 0)
FROM transcript_directory row LEFT JOIN transcript_directory call
ON call.session_id = row.session_id AND call.turn_id = row.turn_id
AND call.message_id = row.tool_use_id
AND call.role = 'tool_call' AND call.source_sequence <= ?4
WHERE row.session_id = ?1 AND row.source_sequence = ?2 AND row.item_index = ?3 AND row.source_sequence <= ?4
AND row.role IN ('tool_call', 'tool_result')"
).bind(session).bind(sql_position(locator)?.0).bind(sql_position(locator)?.1).bind(through).fetch_optional(tx).await?;
let Some(row) = row else {
return Ok(None);
};
let origin: Option<String> = row.try_get(4)?;
let name: Option<String> = row.try_get(5)?;
let error: bool = row.try_get(6)?;
let internal = !error
&& (matches!(
(origin.as_deref(), name.as_deref()),
(Some("provider"), Some("wait" | "tool_search"))
| (Some("code_mode"), Some("code_cell" | "tool_search"))
));
Ok(Some(Header {
origin: None,
message: row.try_get(0)?,
turn: row.try_get(1)?,
timestamp: sequence_number(row.try_get(2)?)?,
role: row.try_get(3)?,
hidden: internal,
total: 0,
}))
}
async fn neighbor(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
through: i64,
cursor: Cursor,
include_thinking: bool,
before: bool,
) -> Result<bool, StoreError> {
if include_thinking {
if !before && cursor.field == HistoryField::Thinking {
return Ok(true);
}
if before && cursor.field == HistoryField::Body {
let exists: bool = sqlx::query_scalar(
"SELECT has_thinking FROM transcript_directory WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
).bind(session).bind(sql_position(cursor.position)?.0).bind(sql_position(cursor.position)?.1).fetch_one(&mut *tx).await?;
if exists {
return Ok(true);
}
}
}
// A predecessor of a validated cursor is already within its fence. Keep
// the pair as the sole upper bound, as in reverse anchored pagination.
let query = if before {
"SELECT EXISTS(SELECT 1 FROM transcript_directory WHERE session_id = ?1 AND (source_sequence,item_index) < (?2,?3))"
} else {
"SELECT EXISTS(SELECT 1 FROM transcript_directory WHERE session_id = ?1 AND source_sequence <= ?2 AND (source_sequence,item_index) > (?3,?4))"
};
let mut query = sqlx::query_scalar(sqlx::AssertSqlSafe(query)).bind(session);
if !before {
query = query.bind(through);
}
Ok(query
.bind(sql_position(cursor.position)?.0)
.bind(sql_position(cursor.position)?.1)
.fetch_one(tx)
.await?)
}
async fn materialize(
tx: &mut SqliteWorkerConnection<'_>,
session: &str,
locator: RowPosition,
view: HistoryView,
through: i64,
) -> Result<usize, StoreError> {
if view == HistoryView::Conversation
&& conversation_tool(tx, session, locator, through)
.await?
.is_some()
{
return Ok(0);
}
// Non-failed turn states have no text under BODY. Their immutable directory
// identity is sufficient for cursor progress; replaying their producer only
// to cache an empty body repeats the completed invocation's evidence.
let empty_state: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM transcript_directory
WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3
AND role = 'turn_state' AND state_status IS NOT 'failed')",
)
.bind(session)
.bind(sql_position(locator)?.0)
.bind(sql_position(locator)?.1)
.fetch_one(&mut *tx)
.await?;
if empty_state {
return Ok(0);
}
super::bodycache::ensure_on_worker(tx, session, locator).await?;
let source: Option<(i64, bool)> = sqlx::query_as(
"SELECT total_bytes, EXISTS(SELECT 1 FROM transcript_cache.text cached WHERE cached.session_id = source.session_id AND cached.source_sequence = source.source_sequence AND cached.item_index = source.item_index AND cached.field = 'body')
FROM transcript_cache.rows source WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3"
).bind(session).bind(sql_position(locator)?.0).bind(sql_position(locator)?.1).fetch_optional(&mut *tx).await?;
let Some((bytes, cached)) = source else {
return Err(invalid("history row was not found"));
};
if cached {
return Ok(0);
}
let query = format!(
"INSERT INTO transcript_cache.text
SELECT session_id, source_sequence, item_index, 'body', json_extract(payload, '$.ts'), json_extract(payload, '$.type'),
CAST({BODY} AS BLOB)
FROM transcript_cache.rows WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3
UNION ALL
SELECT session_id, source_sequence, item_index, 'thinking', json_extract(payload, '$.ts'), 'assistant',
CAST(json_extract(payload, '$.thinking.text') AS BLOB)
FROM transcript_cache.rows WHERE session_id = ?1 AND source_sequence = ?2 AND item_index = ?3
AND json_extract(payload, '$.type') = 'assistant' AND json_type(payload, '$.thinking.text') = 'text'"
);
sqlx::query(sqlx::AssertSqlSafe(query.as_str()))
.bind(session)
.bind(sql_position(locator)?.0)
.bind(sql_position(locator)?.1)
.execute(tx)
.await?;
usize::try_from(sequence_number(bytes)?)
.map_err(|_| invalid("history source exceeds address space"))
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}
fn field_name(field: HistoryField) -> &'static str {
match field {
HistoryField::Body => "body",
HistoryField::Thinking => "thinking",
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use sqlx::{Connection, SqliteConnection};
async fn neighbor(
tx: &mut SqliteConnection,
session: &str,
through: i64,
cursor: Cursor,
include_thinking: bool,
before: bool,
) -> Result<bool, StoreError> {
let session = session.to_owned();
crate::connection::worker_query(tx, move |worker| {
Box::pin(async move {
super::neighbor(worker, &session, through, cursor, include_thinking, before).await
})
})
.await
}
async fn conversation_tool(
tx: &mut SqliteConnection,
session: &str,
locator: RowPosition,
through: i64,
) -> Result<Option<Header>, StoreError> {
let session = session.to_owned();
crate::connection::worker_query(tx, move |worker| {
Box::pin(
async move { super::conversation_tool(worker, &session, locator, through).await },
)
})
.await
}
#[tokio::test]
async fn conversation_retains_trace_parents_and_hides_transport_helpers() {
let mut db = SqliteConnection::connect(":memory:").await.unwrap();
let schema = include_str!("../../migrations/0033_transcript_directory.sql");
sqlx::raw_sql(&schema[schema.find("CREATE TABLE transcript_directory (").unwrap()..])
.execute(&mut db)
.await
.unwrap();
let rows = [
json!({"type":"tool_call","origin":"provider","toolName":"exec"}),
json!({"type":"tool_call","origin":"code_mode","toolName":"code_cell"}),
json!({"type":"tool_call","origin":"imported","toolName":"exec"}),
json!({"type":"tool_call","origin":"provider","toolName":"cua_repl"}),
json!({"type":"tool_call","origin":"host_sdk","toolName":"cua_operation","parentToolCallId":"4"}),
json!({"type":"tool_result","origin":"provider","toolUseId":"4","isError":false}),
json!({"type":"tool_result","origin":"provider","toolUseId":"1","isError":true}),
];
for (i, mut value) in rows.into_iter().enumerate() {
value["ts"] = json!(1);
sqlx::query("INSERT INTO transcript_directory (session_id,source_sequence,item_index,message_id,turn_id,timestamp,role,has_thinking,tool_origin,tool_name,tool_use_id,is_error) SELECT 'a',?1,0,?2,'turn',1,json_extract(?3,'$.type'),0,json_extract(?3,'$.origin'),json_extract(?3,'$.toolName'),json_extract(?3,'$.toolUseId'),json_extract(?3,'$.isError')")
.bind((i+1) as i64).bind((i+1).to_string()).bind(serde_json::to_vec(&value).unwrap())
.execute(&mut db)
.await
.unwrap();
}
for (locator, visible) in [
(1, true),
(2, false),
(3, true),
(4, true),
(5, true),
(6, true),
(7, true),
] {
let row = conversation_tool(
&mut db,
"a",
RowPosition {
source_sequence: locator,
item_index: 0,
},
7,
)
.await
.unwrap()
.unwrap();
assert_eq!(!row.hidden, visible, "locator {locator}");
assert_eq!(row.total, 0);
}
// A parent without a child at the requested fence remains visible.
assert_eq!(
conversation_tool(
&mut db,
"a",
RowPosition {
source_sequence: 4,
item_index: 0
},
4
)
.await
.unwrap()
.unwrap()
.role,
"tool_call"
);
}
#[tokio::test]
async fn predecessor_seek_work_is_bounded_by_cursor_not_following_history() {
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
async fn probe(db: &mut SqliteConnection, through: i64) -> usize {
let steps = Arc::new(AtomicUsize::new(0));
let observed = steps.clone();
db.lock_handle()
.await
.unwrap()
.set_progress_handler(1, move || {
observed.fetch_add(1, Ordering::Relaxed);
true
});
let found = neighbor(
db,
"a",
through,
Cursor {
position: RowPosition {
source_sequence: 1,
item_index: 0,
},
field: HistoryField::Body,
offset: 0,
},
false,
true,
)
.await;
db.lock_handle().await.unwrap().remove_progress_handler();
assert!(!found.unwrap(), "the first row has no predecessor");
steps.load(Ordering::Relaxed)
}
let mut db = SqliteConnection::connect(":memory:").await.unwrap();
let schema = include_str!("../../migrations/0033_transcript_directory.sql");
sqlx::raw_sql(&schema[schema.find("CREATE TABLE transcript_directory (").unwrap()..])
.execute(&mut db)
.await
.unwrap();
let mut measurements = Vec::new();
for (first, last) in [(1_i64, 64_i64), (65, 4096)] {
// Use the production directory schema/indexes. Setup is outside the
// handler; count only the real neighbor function, not a SQL copy.
sqlx::query("WITH RECURSIVE n(value) AS (SELECT ?1 UNION ALL SELECT value+1 FROM n WHERE value<?2) INSERT INTO transcript_directory(session_id,source_sequence,item_index,turn_id,message_id,timestamp,role,has_thinking) SELECT 'a',value,0,'turn',CAST(value AS TEXT),1,'user',0 FROM n")
.bind(first)
.bind(last)
.execute(&mut db)
.await
.unwrap();
measurements.push(probe(&mut db, last).await);
}
let [small, large] = measurements.as_slice() else {
unreachable!()
};
assert!(
*small > 0 && *large > 0,
"progress handler must observe work"
);
eprintln!("predecessor progress callbacks: 64={small}, 4096={large}");
assert!(
*large <= *small + 128,
"same empty predecessor lookup grew with following rows: 64={small}, 4096={large}"
);
let first = Cursor {
position: RowPosition {
source_sequence: 1,
item_index: 0,
},
field: HistoryField::Body,
offset: 0,
};
assert!(
neighbor(&mut db, "a", 4096, first, false, false)
.await
.unwrap()
);
assert!(
!neighbor(&mut db, "a", 1, first, false, false)
.await
.unwrap(),
"a successor beyond the frozen fence must not appear"
);
}
}