use super::utils::{
open_readonly_sqlite_opt, sqlite_for_each_row_on, sqlite_for_each_row_on_with_params,
SqliteScan,
};
use super::{
normalize_opencode_agent_name, normalize_workspace_key, workspace_label_from_key,
UnifiedMessage,
};
use crate::{provider_identity, TokenBreakdown};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct OpenCodeSchemaMessage {
#[serde(default)]
pub id: Option<String>,
#[serde(rename = "sessionID", default)]
pub session_id: Option<String>,
#[serde(rename = "session_id", default)]
pub snake_session_id: Option<String>,
#[serde(default)]
pub role: Option<String>,
#[serde(rename = "modelID", default)]
pub model_id: Option<String>,
#[serde(rename = "providerID", default)]
pub provider_id: Option<String>,
#[serde(default)]
pub model: Option<OpenCodeSchemaModel>,
pub cost: Option<f64>,
pub tokens: Option<OpenCodeSchemaTokens>,
pub time: Option<OpenCodeSchemaTime>,
pub agent: Option<String>,
pub mode: Option<String>,
#[serde(default, deserialize_with = "deserialize_schema_path")]
pub path: Option<OpenCodeSchemaPath>,
}
impl OpenCodeSchemaMessage {
pub(crate) fn resolve_model_id(&self) -> Option<String> {
self.model_id
.clone()
.or_else(|| self.model.as_ref().and_then(|m| m.id.clone()))
}
pub(crate) fn resolve_provider_id(&self) -> Option<String> {
self.provider_id
.clone()
.or_else(|| self.model.as_ref().and_then(|m| m.provider_id.clone()))
}
pub(crate) fn is_assistant(&self) -> bool {
self.role.as_deref().is_none_or(|role| role == "assistant")
}
fn embedded_workspace_root(&self) -> Option<&str> {
self.path.as_ref().and_then(|path| path.root.as_deref())
}
}
#[derive(Debug, Deserialize)]
pub struct OpenCodeSchemaModel {
#[serde(default)]
pub id: Option<String>,
#[serde(rename = "providerID", default)]
pub provider_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct OpenCodeSchemaPath {
pub root: Option<String>,
}
fn deserialize_schema_path<'de, D>(deserializer: D) -> Result<Option<OpenCodeSchemaPath>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let root = value
.get("root")
.and_then(|root| root.as_str())
.map(str::to_string);
Ok(Some(OpenCodeSchemaPath { root }))
}
#[derive(Debug, Deserialize)]
pub struct OpenCodeSchemaTokens {
pub input: i64,
pub output: i64,
pub reasoning: Option<i64>,
#[serde(default)]
pub cache: Option<OpenCodeSchemaCache>,
}
#[derive(Debug, Default, Deserialize)]
pub struct OpenCodeSchemaCache {
#[serde(default)]
pub read: Option<i64>,
#[serde(default)]
pub write: Option<i64>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct OpenCodeSchemaTime {
pub created: f64,
pub completed: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CostProvenance {
Never,
WhenPositive,
WhenReported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DedupMode {
Off,
Merge,
MergeUnlessIdConflict,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct OpenCodeSchemaConfig {
pub client: &'static str,
pub query_groups: &'static [&'static [&'static str]],
pub fallback_provider: &'static str,
pub infer_provider_from_model: bool,
pub dual_schema: bool,
pub payload_session_id: bool,
pub normalize_agent: bool,
pub prefer_mode_over_agent: bool,
pub strict_cache: bool,
pub normalize_epoch_seconds: bool,
pub fallback_timestamp: Option<i64>,
pub record_duration: bool,
pub cost_provenance: CostProvenance,
pub capture_workspace: bool,
pub namespace_rowid_dedup_key: bool,
pub dedup: DedupMode,
pub incremental_groups: Option<&'static [OpenCodeIncrementalGroup]>,
}
impl OpenCodeSchemaConfig {
const fn base(client: &'static str) -> Self {
Self {
client,
query_groups: &[],
fallback_provider: "unknown",
infer_provider_from_model: false,
dual_schema: false,
payload_session_id: false,
normalize_agent: true,
prefer_mode_over_agent: true,
strict_cache: true,
normalize_epoch_seconds: false,
fallback_timestamp: None,
record_duration: true,
cost_provenance: CostProvenance::WhenPositive,
capture_workspace: true,
namespace_rowid_dedup_key: false,
dedup: DedupMode::MergeUnlessIdConflict,
incremental_groups: None,
}
}
pub(crate) const fn opencode() -> Self {
Self {
query_groups: OPENCODE_QUERY_GROUPS,
dual_schema: true,
incremental_groups: Some(OPENCODE_INCREMENTAL_GROUPS),
..Self::base("opencode")
}
}
pub(crate) const fn micode() -> Self {
Self {
query_groups: MICODE_QUERY_GROUPS,
strict_cache: false,
normalize_epoch_seconds: true,
cost_provenance: CostProvenance::WhenReported,
namespace_rowid_dedup_key: true,
dedup: DedupMode::Merge,
..Self::base("micode")
}
}
pub(crate) const fn kilo(fallback_timestamp: i64) -> Self {
Self {
query_groups: KILO_QUERY_GROUPS,
fallback_provider: "kilo",
infer_provider_from_model: true,
payload_session_id: true,
normalize_agent: false,
prefer_mode_over_agent: false,
fallback_timestamp: Some(fallback_timestamp),
record_duration: false,
cost_provenance: CostProvenance::Never,
capture_workspace: false,
dedup: DedupMode::Off,
..Self::base("kilo")
}
}
}
const OPENCODE_V2_QUERIES: &[&str] = &[
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, s.title AS session_title, 1 AS eligible
FROM session_message sm
LEFT JOIN session_v2 s ON s.id = sm.session_id
WHERE sm.type = 'assistant'
AND json_extract(sm.data, '$.tokens') IS NOT NULL
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, 1 AS eligible
FROM session_message sm
LEFT JOIN session_v2 s ON s.id = sm.session_id
WHERE sm.type = 'assistant'
AND json_extract(sm.data, '$.tokens') IS NOT NULL
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, s.title AS session_title, 1 AS eligible
FROM session_message sm
LEFT JOIN session s ON s.id = sm.session_id
WHERE sm.type = 'assistant'
AND json_extract(sm.data, '$.tokens') IS NOT NULL
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, 1 AS eligible
FROM session_message sm
LEFT JOIN session s ON s.id = sm.session_id
WHERE sm.type = 'assistant'
AND json_extract(sm.data, '$.tokens') IS NOT NULL
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULL AS workspace_root, NULL AS session_title, 1 AS eligible
FROM session_message sm
WHERE sm.type = 'assistant'
AND json_extract(sm.data, '$.tokens') IS NOT NULL
ORDER BY sm.id, sm.session_id
"#,
];
const OPENCODE_V1_QUERIES: &[&str] = &[
r#"
SELECT m.id, m.session_id, m.data, NULLIF(s.directory, '') AS workspace_root, s.title AS session_title, 1 AS eligible
FROM message m
LEFT JOIN session s ON s.id = m.session_id
WHERE json_extract(m.data, '$.role') = 'assistant'
AND json_extract(m.data, '$.tokens') IS NOT NULL
ORDER BY m.id, m.session_id
"#,
r#"
SELECT m.id, m.session_id, m.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
LEFT JOIN session s ON s.id = m.session_id
WHERE json_extract(m.data, '$.role') = 'assistant'
AND json_extract(m.data, '$.tokens') IS NOT NULL
ORDER BY m.id, m.session_id
"#,
r#"
SELECT m.id, m.session_id, m.data, NULL AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
WHERE json_extract(m.data, '$.role') = 'assistant'
AND json_extract(m.data, '$.tokens') IS NOT NULL
ORDER BY m.id, m.session_id
"#,
];
const OPENCODE_QUERY_GROUPS: &[&[&str]] = &[OPENCODE_V2_QUERIES, OPENCODE_V1_QUERIES];
const OPENCODE_V2_INCREMENTAL_QUERIES: &[&str] = &[
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, s.title AS session_title, sm.type = 'assistant' AS eligible
FROM session_message sm
LEFT JOIN session_v2 s ON s.id = sm.session_id
WHERE sm.time_updated >= ?1
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, sm.type = 'assistant' AS eligible
FROM session_message sm
LEFT JOIN session_v2 s ON s.id = sm.session_id
WHERE sm.time_updated >= ?1
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, s.title AS session_title, sm.type = 'assistant' AS eligible
FROM session_message sm
LEFT JOIN session s ON s.id = sm.session_id
WHERE sm.time_updated >= ?1
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, sm.type = 'assistant' AS eligible
FROM session_message sm
LEFT JOIN session s ON s.id = sm.session_id
WHERE sm.time_updated >= ?1
ORDER BY sm.id, sm.session_id
"#,
r#"
SELECT sm.id, sm.session_id, sm.data, NULL AS workspace_root, NULL AS session_title, sm.type = 'assistant' AS eligible
FROM session_message sm
WHERE sm.time_updated >= ?1
ORDER BY sm.id, sm.session_id
"#,
];
const OPENCODE_V1_INCREMENTAL_QUERIES: &[&str] = &[
r#"
SELECT m.id, m.session_id, m.data, NULLIF(s.directory, '') AS workspace_root, s.title AS session_title, 1 AS eligible
FROM message m
LEFT JOIN session s ON s.id = m.session_id
WHERE m.time_updated >= ?1
ORDER BY m.id, m.session_id
"#,
r#"
SELECT m.id, m.session_id, m.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
LEFT JOIN session s ON s.id = m.session_id
WHERE m.time_updated >= ?1
ORDER BY m.id, m.session_id
"#,
r#"
SELECT m.id, m.session_id, m.data, NULL AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
WHERE m.time_updated >= ?1
ORDER BY m.id, m.session_id
"#,
];
const OPENCODE_V2_ROW_INVENTORY_QUERY: &str =
"SELECT id, time_updated FROM session_message ORDER BY id";
const OPENCODE_V1_ROW_INVENTORY_QUERY: &str = "SELECT id, time_updated FROM message ORDER BY id";
const OPENCODE_V2_METADATA_QUERIES: &[&str] = &[
"SELECT s.id, NULLIF(s.directory, '') , s.title, s.time_updated FROM session_v2 s WHERE s.time_updated >= ?1",
"SELECT s.id, NULLIF(s.directory, '') , NULL, s.time_updated FROM session_v2 s WHERE s.time_updated >= ?1",
"SELECT s.id, NULLIF(s.directory, '') , s.title, s.time_updated FROM session s WHERE s.time_updated >= ?1",
"SELECT s.id, NULLIF(s.directory, '') , NULL, s.time_updated FROM session s WHERE s.time_updated >= ?1",
"",
];
const OPENCODE_V1_METADATA_QUERIES: &[&str] = &[
"SELECT s.id, NULLIF(s.directory, '') , s.title, s.time_updated FROM session s WHERE s.time_updated >= ?1",
"SELECT s.id, NULLIF(s.directory, '') , NULL, s.time_updated FROM session s WHERE s.time_updated >= ?1",
"",
];
const OPENCODE_V2_METADATA_STATS: &[&str] = &[
"SELECT MAX(time_updated) FROM session_v2",
"SELECT MAX(time_updated) FROM session_v2",
"SELECT MAX(time_updated) FROM session",
"SELECT MAX(time_updated) FROM session",
"",
];
const OPENCODE_V1_METADATA_STATS: &[&str] = &[
"SELECT MAX(time_updated) FROM session",
"SELECT MAX(time_updated) FROM session",
"",
];
#[derive(Debug, Clone, Copy)]
pub(crate) struct OpenCodeIncrementalGroup {
queries: &'static [&'static str],
inventory: &'static str,
metadata: &'static [&'static str],
metadata_stats: &'static [&'static str],
}
const OPENCODE_INCREMENTAL_GROUPS: &[OpenCodeIncrementalGroup] = &[
OpenCodeIncrementalGroup {
queries: OPENCODE_V2_INCREMENTAL_QUERIES,
inventory: OPENCODE_V2_ROW_INVENTORY_QUERY,
metadata: OPENCODE_V2_METADATA_QUERIES,
metadata_stats: OPENCODE_V2_METADATA_STATS,
},
OpenCodeIncrementalGroup {
queries: OPENCODE_V1_INCREMENTAL_QUERIES,
inventory: OPENCODE_V1_ROW_INVENTORY_QUERY,
metadata: OPENCODE_V1_METADATA_QUERIES,
metadata_stats: OPENCODE_V1_METADATA_STATS,
},
];
const MICODE_QUERIES: &[&str] = &[
r#"
SELECT m.id, m.session_id, m.data, NULLIF(s.directory, '') AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
LEFT JOIN session s ON s.id = m.session_id
WHERE json_extract(m.data, '$.role') = 'assistant'
AND json_extract(m.data, '$.tokens') IS NOT NULL
ORDER BY m.id, m.session_id
"#,
r#"
SELECT m.id, m.session_id, m.data, NULL AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
WHERE json_extract(m.data, '$.role') = 'assistant'
AND json_extract(m.data, '$.tokens') IS NOT NULL
ORDER BY m.id, m.session_id
"#,
];
const MICODE_QUERY_GROUPS: &[&[&str]] = &[MICODE_QUERIES];
const KILO_QUERIES: &[&str] = &[r#"
SELECT m.id, m.session_id, m.data, NULL AS workspace_root, NULL AS session_title, 1 AS eligible
FROM message m
WHERE json_valid(m.data)
AND json_extract(m.data, '$.role') = 'assistant'
AND json_extract(m.data, '$.tokens') IS NOT NULL
"#];
const KILO_QUERY_GROUPS: &[&[&str]] = &[KILO_QUERIES];
fn workspace_from_root(root: Option<&str>) -> (Option<String>, Option<String>) {
let workspace_key = root.and_then(normalize_workspace_key);
let workspace_label = workspace_key.as_deref().and_then(workspace_label_from_key);
(workspace_key, workspace_label)
}
pub(crate) fn set_workspace_from_root(message: &mut UnifiedMessage, root: Option<&str>) {
let (workspace_key, workspace_label) = workspace_from_root(root);
message.set_workspace(workspace_key, workspace_label);
}
fn merge_duplicate_workspace(
message: &mut UnifiedMessage,
state: &mut SchemaDedupState,
root: Option<&str>,
) {
if state.has_workspace_conflict {
return;
}
let (candidate_key, candidate_label) = workspace_from_root(root);
match (message.workspace_key.as_deref(), candidate_key) {
(None, Some(key)) => message.set_workspace(Some(key), candidate_label),
(Some(existing), Some(candidate)) if existing != candidate => {
state.has_workspace_conflict = true;
message.set_workspace(None, None);
}
_ => {}
}
}
fn resolve_cache(cache: Option<&OpenCodeSchemaCache>, strict: bool) -> Option<(i64, i64)> {
match cache {
Some(cache) => match (cache.read, cache.write) {
(Some(read), Some(write)) => Some((read.max(0), write.max(0))),
_ if strict => None,
(read, write) => Some((read.unwrap_or(0).max(0), write.unwrap_or(0).max(0))),
},
None if strict => None,
None => Some((0, 0)),
}
}
fn normalize_epoch(timestamp: f64, cfg: &OpenCodeSchemaConfig) -> f64 {
if !cfg.normalize_epoch_seconds || timestamp > 1e12 {
timestamp
} else {
timestamp * 1000.0
}
}
fn duration_ms(created_ms: f64, completed_ms: Option<f64>) -> Option<i64> {
let duration = completed_ms? - created_ms;
if duration.is_finite() && duration > 0.0 {
Some(duration as i64)
} else {
None
}
}
fn resolve_provider(
msg: &OpenCodeSchemaMessage,
model_id: &str,
cfg: &OpenCodeSchemaConfig,
) -> String {
let explicit = if cfg.dual_schema {
msg.resolve_provider_id()
} else {
msg.provider_id.clone()
};
let provider = explicit
.or_else(|| {
if cfg.infer_provider_from_model {
provider_identity::inferred_provider_from_model(model_id).map(str::to_string)
} else {
None
}
})
.unwrap_or_else(|| cfg.fallback_provider.to_string());
provider_identity::canonical_provider(&provider).unwrap_or(provider)
}
pub(crate) fn reported_cost(cost: Option<f64>) -> Option<f64> {
cost.filter(|cost| cost.is_finite() && *cost >= 0.0)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct OpenCodeSchemaFingerprint {
created_bits: u64,
completed_bits: Option<u64>,
model_id: String,
provider_id: String,
input: i64,
output: i64,
reasoning: i64,
cache_read: i64,
cache_write: i64,
cost_bits: u64,
agent: Option<String>,
}
#[derive(Debug, Clone)]
struct SchemaDedupState {
message_id: Option<String>,
has_workspace_conflict: bool,
}
struct OpenCodeSchemaRow {
row_id: String,
session_id: String,
data_json: String,
workspace_root: Option<String>,
session_title: Option<String>,
eligible: bool,
}
#[derive(Default)]
struct SchemaAccumulator {
messages: Vec<UnifiedMessage>,
fingerprint_indices: HashMap<OpenCodeSchemaFingerprint, Vec<usize>>,
dedup_states: Vec<SchemaDedupState>,
merged_dedup_keys: std::collections::HashSet<String>,
}
impl SchemaAccumulator {
fn ingest(
&mut self,
row: OpenCodeSchemaRow,
cfg: &OpenCodeSchemaConfig,
db_namespace: &str,
) -> Option<usize> {
let OpenCodeSchemaRow {
row_id,
session_id: row_session_id,
data_json,
workspace_root: row_workspace_root,
session_title: row_session_title,
eligible,
} = row;
if !eligible {
return None;
}
let mut bytes = data_json.into_bytes();
let msg: OpenCodeSchemaMessage = match simd_json::from_slice(&mut bytes) {
Ok(m) => m,
Err(_) => return None,
};
let is_assistant = if cfg.dual_schema {
msg.is_assistant()
} else {
msg.role.as_deref() == Some("assistant")
};
if !is_assistant {
return None;
}
let tokens = msg.tokens.as_ref()?;
let (cache_read, cache_write) = resolve_cache(tokens.cache.as_ref(), cfg.strict_cache)?;
let resolved_model_id = if cfg.dual_schema {
msg.resolve_model_id()
} else {
msg.model_id.clone()
};
let model_id = resolved_model_id?;
let provider_id = resolve_provider(&msg, &model_id, cfg);
let (created_ms, completed_ms) = match msg.time {
Some(ref time) => (
normalize_epoch(time.created, cfg),
time.completed
.map(|completed| normalize_epoch(completed, cfg)),
),
None => (cfg.fallback_timestamp? as f64, None),
};
let agent_or_mode = if cfg.prefer_mode_over_agent {
msg.mode.clone().or_else(|| msg.agent.clone())
} else {
msg.agent.clone().or_else(|| msg.mode.clone())
};
let agent = agent_or_mode.map(|agent| {
if cfg.normalize_agent {
normalize_opencode_agent_name(&agent)
} else {
agent
}
});
let input = tokens.input.max(0);
let output = tokens.output.max(0);
let reasoning = tokens.reasoning.unwrap_or(0).max(0);
let reported = reported_cost(msg.cost);
let cost = reported.unwrap_or(0.0);
let session_id = if cfg.payload_session_id {
msg.snake_session_id.clone().unwrap_or(row_session_id)
} else {
row_session_id
};
let message_id = msg.id.clone();
let dedup_key = match message_id.clone() {
Some(id) => id,
None if cfg.namespace_rowid_dedup_key => format!("{db_namespace}:{row_id}"),
None => row_id,
};
let mut unified = UnifiedMessage::new_with_agent(
cfg.client,
model_id.clone(),
provider_id.clone(),
session_id,
created_ms as i64,
TokenBreakdown {
input,
output,
cache_read,
cache_write,
reasoning,
},
cost,
agent.clone(),
);
if cfg.record_duration {
unified.duration_ms = duration_ms(created_ms, completed_ms);
}
match cfg.cost_provenance {
CostProvenance::Never => {}
CostProvenance::WhenPositive => {
if cost > 0.0 {
unified.mark_provider_reported_cost();
}
}
CostProvenance::WhenReported => {
if reported.is_some() {
unified.mark_provider_reported_cost();
}
}
}
unified.dedup_key = Some(dedup_key);
let workspace_root = if cfg.capture_workspace {
row_workspace_root
.as_deref()
.or_else(|| msg.embedded_workspace_root())
} else {
None
};
if cfg.capture_workspace {
set_workspace_from_root(&mut unified, workspace_root);
}
if let Some(ref title) = row_session_title {
let trimmed = title.trim();
if !trimmed.is_empty() {
unified.session_title = Some(trimmed.to_string());
}
}
if cfg.dedup == DedupMode::Off {
let index = self.messages.len();
self.messages.push(unified);
return Some(index);
}
let fingerprint = OpenCodeSchemaFingerprint {
created_bits: created_ms.to_bits(),
completed_bits: completed_ms.map(f64::to_bits),
model_id,
provider_id,
input,
output,
reasoning,
cache_read,
cache_write,
cost_bits: cost.to_bits(),
agent,
};
let candidate = {
let slots = self
.fingerprint_indices
.get(&fingerprint)
.cloned()
.unwrap_or_default();
match cfg.dedup {
DedupMode::Off => None,
DedupMode::Merge => slots.first().copied(),
DedupMode::MergeUnlessIdConflict => slots.into_iter().find(|&index| {
!matches!(
(&self.dedup_states[index].message_id, &message_id),
(Some(existing), Some(incoming)) if existing != incoming
)
}),
}
};
if let Some(index) = candidate {
let absorbed_dedup_key = if cfg.incremental_groups.is_some() {
unified.dedup_key.clone()
} else {
None
};
if unified.has_authoritative_cost() {
self.messages[index].mark_provider_reported_cost();
}
let dedup_state = &mut self.dedup_states[index];
if message_id.is_some() && dedup_state.message_id.is_none() {
dedup_state.message_id = message_id;
self.messages[index].dedup_key = unified.dedup_key;
}
merge_duplicate_workspace(&mut self.messages[index], dedup_state, workspace_root);
if cfg.incremental_groups.is_some() {
if let Some(key) = self.messages[index].dedup_key.clone() {
self.merged_dedup_keys.insert(key);
}
if let Some(key) = absorbed_dedup_key {
self.merged_dedup_keys.insert(key);
}
}
return Some(index);
}
let new_index = self.messages.len();
self.dedup_states.push(SchemaDedupState {
message_id,
has_workspace_conflict: false,
});
self.fingerprint_indices
.entry(fingerprint)
.or_default()
.push(new_index);
self.messages.push(unified);
Some(new_index)
}
}
fn collect_rows(
db_path: &Path,
conn: &rusqlite::Connection,
query: &str,
on_row: &mut dyn FnMut(OpenCodeSchemaRow),
) -> SqliteScan {
sqlite_for_each_row_on(conn, db_path, query, None, &mut |row| {
let id: String = row.get(0)?;
let session_id: String = row.get(1)?;
let data_json: String = row.get(2)?;
let workspace_root: Option<String> = row.get(3)?;
let session_title: Option<String> = row.get(4)?;
let eligible: bool = row.get(5)?;
on_row(OpenCodeSchemaRow {
row_id: id,
session_id,
data_json,
workspace_root,
session_title,
eligible,
});
Ok(())
})
}
fn collect_rows_since(
db_path: &Path,
conn: &rusqlite::Connection,
query: &str,
since: i64,
on_row: &mut dyn FnMut(OpenCodeSchemaRow),
) -> bool {
let scan =
sqlite_for_each_row_on_with_params(conn, db_path, query, &[&since], None, &mut |row| {
let id: String = row.get(0)?;
let session_id: String = row.get(1)?;
let data_json: String = row.get(2)?;
let workspace_root: Option<String> = row.get(3)?;
let session_title: Option<String> = row.get(4)?;
let eligible: bool = row.get(5)?;
on_row(OpenCodeSchemaRow {
row_id: id,
session_id,
data_json,
workspace_root,
session_title,
eligible,
});
Ok(())
});
scan.completed()
}
fn read_metadata_high_water(
db_path: &Path,
conn: &rusqlite::Connection,
query: &str,
) -> Option<i64> {
if query.is_empty() {
return Some(i64::MIN);
}
let mut high_water = i64::MIN;
let scan = sqlite_for_each_row_on(conn, db_path, query, None, &mut |row| {
high_water = row.get::<_, Option<i64>>(0)?.unwrap_or(i64::MIN);
Ok(())
});
scan.completed().then_some(high_water)
}
fn refresh_changed_session_metadata(
db_path: &Path,
conn: &rusqlite::Connection,
query: &str,
stats_query: &str,
since: i64,
cached: &mut [UnifiedMessage],
already_refreshed: &mut std::collections::HashSet<String>,
) -> Option<i64> {
if query.is_empty() {
return Some(i64::MIN);
}
let mut changed: HashMap<String, (Option<String>, Option<String>)> = HashMap::new();
let mut usable = true;
let scan =
sqlite_for_each_row_on_with_params(conn, db_path, query, &[&since], None, &mut |row| {
let session_id: String = row.get(0)?;
let workspace_root: Option<String> = row.get(1)?;
let session_title: Option<String> = row.get(2)?;
if workspace_root.is_none() {
usable = false;
}
changed.insert(session_id, (workspace_root, session_title));
Ok(())
});
if !scan.completed() || !usable {
return None;
}
if changed.keys().any(|id| already_refreshed.contains(id)) {
return None;
}
already_refreshed.extend(changed.keys().cloned());
if !changed.is_empty() {
for message in cached.iter_mut() {
let Some((workspace_root, session_title)) = changed.get(&message.session_id) else {
continue;
};
set_workspace_from_root(message, workspace_root.as_deref());
message.session_title = session_title
.as_deref()
.map(str::trim)
.filter(|title| !title.is_empty())
.map(str::to_string);
}
}
read_metadata_high_water(db_path, conn, stats_query)
}
pub(crate) fn parse_opencode_schema_sqlite(
db_path: &Path,
cfg: OpenCodeSchemaConfig,
) -> Vec<UnifiedMessage> {
scan_opencode_schema_sqlite(db_path, cfg).messages
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct OpenCodeGroupMark {
pub query_digest: u64,
pub updated_high_water: i64,
pub metadata_high_water: i64,
pub rows: Vec<OpenCodeRowProvenance>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum OpenCodeRowMessageKey {
Rejected,
RowId,
Override(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct OpenCodeRowProvenance {
pub row_id: String,
pub updated_at: i64,
pub message_key: OpenCodeRowMessageKey,
}
impl OpenCodeRowProvenance {
fn dedup_key(&self) -> Option<&str> {
match &self.message_key {
OpenCodeRowMessageKey::Rejected => None,
OpenCodeRowMessageKey::RowId => Some(self.row_id.as_str()),
OpenCodeRowMessageKey::Override(key) => Some(key.as_str()),
}
}
fn set_dedup_key(&mut self, key: Option<&str>) {
self.message_key = match key {
None => OpenCodeRowMessageKey::Rejected,
Some(key) if key == self.row_id => OpenCodeRowMessageKey::RowId,
Some(key) => OpenCodeRowMessageKey::Override(key.to_string()),
};
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct OpenCodeIncrementalState {
pub groups: Vec<Option<OpenCodeGroupMark>>,
pub merged_dedup_keys: Vec<String>,
}
const MAX_MERGED_DEDUP_KEYS: usize = 250_000;
#[cfg(test)]
pub(crate) static INCREMENTAL_RESCANS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
pub(crate) struct OpenCodeSchemaScan {
pub messages: Vec<UnifiedMessage>,
pub incremental: Option<OpenCodeIncrementalState>,
}
impl OpenCodeSchemaScan {
fn empty() -> Self {
Self {
messages: Vec::new(),
incremental: None,
}
}
}
fn query_digest(query: &str) -> u64 {
let mut digest: u64 = 0xcbf2_9ce4_8422_2325;
for byte in query.as_bytes() {
digest ^= u64::from(*byte);
digest = digest.wrapping_mul(0x0000_0100_0000_01b3);
}
digest
}
fn read_row_inventory(
db_path: &Path,
conn: &rusqlite::Connection,
query: &str,
) -> Option<Vec<OpenCodeRowProvenance>> {
let mut rows = Vec::new();
let scan = sqlite_for_each_row_on(conn, db_path, query, None, &mut |row| {
rows.push(OpenCodeRowProvenance {
row_id: row.get(0)?,
updated_at: row.get(1)?,
message_key: OpenCodeRowMessageKey::Rejected,
});
Ok(())
});
scan.completed().then_some(rows)
}
pub(crate) fn scan_opencode_schema_sqlite(
db_path: &Path,
cfg: OpenCodeSchemaConfig,
) -> OpenCodeSchemaScan {
let Some(conn) = open_readonly_sqlite_opt(db_path) else {
return OpenCodeSchemaScan::empty();
};
let db_namespace = if cfg.namespace_rowid_dedup_key {
db_path.to_string_lossy().into_owned()
} else {
String::new()
};
let mut acc = SchemaAccumulator::default();
let mut marks: Vec<Option<OpenCodeGroupMark>> = Vec::with_capacity(cfg.query_groups.len());
let mut parsed_rows: Vec<Vec<(String, usize)>> = Vec::with_capacity(cfg.query_groups.len());
let mut resumable = true;
for (group_index, group) in cfg.query_groups.iter().enumerate() {
let incremental = cfg
.incremental_groups
.and_then(|groups| groups.get(group_index));
let inventory = incremental
.and_then(|incremental| read_row_inventory(db_path, &conn, incremental.inventory));
let mut chosen = None;
let mut read_every_row = false;
let mut group_parsed_rows = Vec::new();
for (index, query) in group.iter().enumerate() {
let scan = collect_rows(db_path, &conn, query, &mut |row| {
let row_id = row.row_id.clone();
if let Some(message_index) = acc.ingest(row, &cfg, &db_namespace) {
group_parsed_rows.push((row_id, message_index));
}
});
if scan.prepared() {
chosen = Some(index);
read_every_row = scan.completed();
break;
}
}
if chosen.is_some() && !read_every_row {
resumable = false;
}
parsed_rows.push(group_parsed_rows);
marks.push(match (chosen, inventory) {
(Some(index), Some(rows)) if read_every_row => {
let metadata_high_water = cfg
.incremental_groups
.and_then(|groups| groups.get(group_index))
.and_then(|incremental| incremental.metadata_stats.get(index))
.and_then(|query| read_metadata_high_water(db_path, &conn, query));
match metadata_high_water {
Some(metadata_high_water) => Some(OpenCodeGroupMark {
query_digest: query_digest(group[index]),
updated_high_water: rows
.iter()
.map(|row| row.updated_at)
.max()
.unwrap_or(i64::MIN),
metadata_high_water,
rows,
}),
None => {
resumable = false;
None
}
}
}
(Some(_), None) if incremental.is_some() => {
resumable = false;
None
}
_ => None,
});
}
for (group_index, group_rows) in parsed_rows.into_iter().enumerate() {
let Some(Some(mark)) = marks.get_mut(group_index) else {
continue;
};
let row_index: HashMap<String, usize> = mark
.rows
.iter()
.enumerate()
.map(|(index, row)| (row.row_id.clone(), index))
.collect();
for (row_id, message_index) in group_rows {
let Some(&index) = row_index.get(row_id.as_str()) else {
resumable = false;
continue;
};
let key = acc.messages[message_index].dedup_key.as_deref();
mark.rows[index].set_dedup_key(key);
}
}
let mut backing_counts: HashMap<String, usize> = HashMap::new();
for row in marks
.iter()
.filter_map(Option::as_ref)
.flat_map(|mark| mark.rows.iter())
{
if let Some(key) = row.dedup_key() {
*backing_counts.entry(key.to_string()).or_default() += 1;
}
}
for (key, count) in backing_counts {
if count > 1 {
acc.merged_dedup_keys.insert(key);
}
}
resumable &= acc.merged_dedup_keys.len() <= MAX_MERGED_DEDUP_KEYS;
let merged_dedup_keys = acc.merged_dedup_keys.into_iter().collect();
OpenCodeSchemaScan {
messages: acc.messages,
incremental: cfg.incremental_groups.filter(|_| resumable).map(|_| {
OpenCodeIncrementalState {
groups: marks,
merged_dedup_keys,
}
}),
}
}
pub(crate) fn rescan_opencode_schema_sqlite(
db_path: &Path,
cfg: OpenCodeSchemaConfig,
cached_state: &OpenCodeIncrementalState,
mut cached_messages: Vec<UnifiedMessage>,
) -> Option<OpenCodeSchemaScan> {
let incremental_groups = cfg.incremental_groups?;
if cached_state.groups.len() != cfg.query_groups.len() {
return None;
}
let conn = open_readonly_sqlite_opt(db_path)?;
let db_namespace = if cfg.namespace_rowid_dedup_key {
db_path.to_string_lossy().into_owned()
} else {
String::new()
};
let mut acc = SchemaAccumulator::default();
let mut marks: Vec<Option<OpenCodeGroupMark>> = Vec::with_capacity(cached_state.groups.len());
let mut pending_metadata: Vec<(usize, &'static str, &'static str, i64)> = Vec::new();
let mut parsed_row_slots: Vec<Vec<(usize, usize)>> =
Vec::with_capacity(cached_state.groups.len());
let previously_merged: std::collections::HashSet<&str> = cached_state
.merged_dedup_keys
.iter()
.map(String::as_str)
.collect();
for (group_index, group) in cfg.query_groups.iter().enumerate() {
let incremental = incremental_groups.get(group_index)?;
let cached_mark = cached_state.groups[group_index].as_ref();
let inventory = read_row_inventory(db_path, &conn, incremental.inventory);
let (mark, mut rows) = match (cached_mark, inventory) {
(Some(mark), Some(rows)) => (mark, rows),
(None, None) => {
if group.iter().any(|query| conn.prepare(query).is_ok()) {
return None;
}
marks.push(None);
parsed_row_slots.push(Vec::new());
continue;
}
_ => return None,
};
let chosen = group.iter().position(|query| conn.prepare(query).is_ok())?;
if query_digest(group[chosen]) != mark.query_digest {
return None;
}
let mut expected_changed: HashMap<String, usize> = HashMap::new();
let mut old_index = 0;
for (row_slot, row) in rows.iter_mut().enumerate() {
while old_index < mark.rows.len()
&& mark.rows[old_index].row_id.as_str() < row.row_id.as_str()
{
let deleted = &mark.rows[old_index];
if deleted
.dedup_key()
.is_some_and(|key| previously_merged.contains(key))
{
return None;
}
old_index += 1;
}
if old_index < mark.rows.len() && mark.rows[old_index].row_id == row.row_id {
let old = &mark.rows[old_index];
if old.updated_at == row.updated_at && row.updated_at < mark.updated_high_water {
row.message_key = old.message_key.clone();
} else {
if old
.dedup_key()
.is_some_and(|key| previously_merged.contains(key))
{
return None;
}
expected_changed.insert(row.row_id.clone(), row_slot);
}
old_index += 1;
} else {
expected_changed.insert(row.row_id.clone(), row_slot);
}
}
for deleted in &mark.rows[old_index..] {
if deleted
.dedup_key()
.is_some_and(|key| previously_merged.contains(key))
{
return None;
}
}
let mut seen_changed = std::collections::HashSet::new();
let mut group_parsed_slots = Vec::new();
let mut unsafe_delta = false;
let query = incremental.queries.get(chosen)?;
if !collect_rows_since(db_path, &conn, query, mark.updated_high_water, &mut |row| {
let Some(&row_slot) = expected_changed.get(&row.row_id) else {
if rows
.binary_search_by(|candidate| candidate.row_id.cmp(&row.row_id))
.is_err()
{
unsafe_delta = true;
}
return;
};
seen_changed.insert(row.row_id.clone());
if let Some(message_slot) = acc.ingest(row, &cfg, &db_namespace) {
group_parsed_slots.push((row_slot, message_slot));
}
}) {
return None;
}
if unsafe_delta || seen_changed.len() != expected_changed.len() {
return None;
}
parsed_row_slots.push(group_parsed_slots);
pending_metadata.push((
marks.len(),
incremental.metadata.get(chosen).copied().unwrap_or(""),
incremental
.metadata_stats
.get(chosen)
.copied()
.unwrap_or(""),
mark.metadata_high_water,
));
marks.push(Some(OpenCodeGroupMark {
query_digest: mark.query_digest,
updated_high_water: rows
.iter()
.map(|row| row.updated_at)
.max()
.unwrap_or(i64::MIN),
metadata_high_water: mark.metadata_high_water,
rows,
}));
}
for (group_index, slots) in parsed_row_slots.iter().enumerate() {
let Some(Some(mark)) = marks.get_mut(group_index) else {
continue;
};
for &(row_slot, message_slot) in slots {
let key = acc.messages[message_slot].dedup_key.as_deref();
mark.rows[row_slot].set_dedup_key(key);
if key.is_some_and(|key| previously_merged.contains(key)) {
return None;
}
}
}
let mut refreshed_sessions: std::collections::HashSet<String> =
std::collections::HashSet::new();
for (index, query, stats_query, since) in pending_metadata {
let metadata_high_water = refresh_changed_session_metadata(
db_path,
&conn,
query,
stats_query,
since,
&mut cached_messages,
&mut refreshed_sessions,
)?;
if let Some(Some(mark)) = marks.get_mut(index) {
mark.metadata_high_water = metadata_high_water;
}
}
let mut backing_counts: HashMap<String, usize> = HashMap::new();
let mut active_keys = std::collections::HashSet::new();
for row in marks
.iter()
.filter_map(Option::as_ref)
.flat_map(|mark| mark.rows.iter())
{
if let Some(key) = row.dedup_key() {
active_keys.insert(key.to_string());
*backing_counts.entry(key.to_string()).or_default() += 1;
}
}
if backing_counts
.iter()
.any(|(key, &count)| count > 1 && !previously_merged.contains(key.as_str()))
{
return None;
}
cached_messages.retain(|message| {
message
.dedup_key
.as_deref()
.is_some_and(|key| active_keys.contains(key))
});
let merged_dedup_keys: std::collections::HashSet<String> =
cached_state.merged_dedup_keys.iter().cloned().collect();
let messages = merge_incremental_messages(cached_messages, acc.messages, &merged_dedup_keys)?;
#[cfg(test)]
INCREMENTAL_RESCANS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Some(OpenCodeSchemaScan {
messages,
incremental: Some(OpenCodeIncrementalState {
groups: marks,
merged_dedup_keys: merged_dedup_keys.into_iter().collect(),
}),
})
}
fn content_digest(message: &UnifiedMessage) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
message.timestamp.hash(&mut hasher);
message.duration_ms.hash(&mut hasher);
message.model_id.hash(&mut hasher);
message.provider_id.hash(&mut hasher);
message.tokens.input.hash(&mut hasher);
message.tokens.output.hash(&mut hasher);
message.tokens.reasoning.hash(&mut hasher);
message.tokens.cache_read.hash(&mut hasher);
message.tokens.cache_write.hash(&mut hasher);
message.cost.to_bits().hash(&mut hasher);
message.agent.hash(&mut hasher);
hasher.finish()
}
fn merge_incremental_messages(
cached: Vec<UnifiedMessage>,
changed: Vec<UnifiedMessage>,
merged_dedup_keys: &std::collections::HashSet<String>,
) -> Option<Vec<UnifiedMessage>> {
let mut merged = cached;
let mut index_by_key: HashMap<String, usize> = HashMap::with_capacity(merged.len());
let mut index_by_digest: HashMap<u64, usize> = HashMap::with_capacity(merged.len());
for (index, message) in merged.iter().enumerate() {
index_by_key
.entry(message.dedup_key.clone()?)
.or_insert(index);
index_by_digest
.entry(content_digest(message))
.or_insert(index);
}
for message in changed {
let key = message.dedup_key.clone()?;
if merged_dedup_keys.contains(&key) {
return None;
}
let digest = content_digest(&message);
match index_by_key.get(&key).copied() {
Some(index) => {
if index_by_digest
.get(&digest)
.is_some_and(|&other| other != index)
{
return None;
}
index_by_digest.insert(digest, index);
merged[index] = message;
}
None => {
if index_by_digest.contains_key(&digest) {
return None;
}
index_by_digest.insert(digest, merged.len());
index_by_key.insert(key, merged.len());
merged.push(message);
}
}
}
Some(merged)
}