Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/migrations/agent-trace-repository/007_commit_patch_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE agent_traces
ADD COLUMN commit_patch_id TEXT;
11 changes: 3 additions & 8 deletions cli/src/services/agent_trace_db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ FROM diff_traces
WHERE time_ms >= ?1 AND time_ms <= ?2
ORDER BY time_ms ASC, id ASC";

/// Parameterized SQL for inserting a post-commit patch intersection result.
pub const INSERT_POST_COMMIT_PATCH_INTERSECTION_SQL: &str =
"INSERT INTO post_commit_patch_intersections (
commit_id,
Expand All @@ -48,12 +47,9 @@ pub const INSERT_POST_COMMIT_PATCH_INTERSECTION_SQL: &str =
intersection_patch
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)";

/// Parameterized SQL for inserting a built agent trace payload.
pub const INSERT_AGENT_TRACE_SQL: &str =
"INSERT INTO agent_traces (commit_id, commit_time_ms, trace_json, agent_trace_id, url, remote_url) VALUES (?1, ?2, ?3, ?4, ?5, ?6)";
"INSERT INTO agent_traces (commit_id, commit_time_ms, trace_json, agent_trace_id, url, remote_url, commit_patch_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)";

/// Parameterized SQL for inserting a message row. Duplicate
/// `(session_id, message_id)` writes are ignored.
pub const INSERT_MESSAGE_SQL: &str =
"INSERT INTO messages (session_id, message_id, role, generated_at_unix_ms)
VALUES (?1, ?2, ?3, ?4)
Expand Down Expand Up @@ -212,7 +208,6 @@ impl RecentDiffTracePatches {
}
}

/// Post-commit patch intersection result to persist in the agent trace database.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PostCommitPatchIntersectionInsert<'a> {
pub commit_id: &'a str,
Expand All @@ -224,7 +219,6 @@ pub struct PostCommitPatchIntersectionInsert<'a> {
pub intersection_patch: &'a str,
}

/// Built agent trace payload to persist in the agent trace database.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AgentTraceInsert<'a> {
pub commit_id: &'a str,
Expand All @@ -233,9 +227,9 @@ pub struct AgentTraceInsert<'a> {
pub agent_trace_id: &'a str,
pub url: &'a str,
pub remote_url: &'a str,
pub commit_patch_id: Option<&'a str>,
}

/// Message role constraint for the `messages` table.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
Expand Down Expand Up @@ -342,6 +336,7 @@ async fn insert_agent_trace_with<M: DbSpec>(
input.agent_trace_id,
input.url,
input.remote_url,
input.commit_patch_id,
),
)
.await
Expand Down
2 changes: 0 additions & 2 deletions cli/src/services/agent_trace_db/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,6 @@ impl RepositoryAgentTraceDb {
insert_post_commit_patch_intersection_with(self, input).await
}

/// Insert a built Agent Trace payload into the repository-scoped
/// `agent_traces` table.
pub async fn insert_agent_trace(&self, input: AgentTraceInsert<'_>) -> Result<u64> {
insert_agent_trace_with(self, input).await
}
Expand Down
81 changes: 62 additions & 19 deletions cli/src/services/agent_trace_export/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ pub fn validate_js_safe_integer(value: i64) -> Result<()> {
Ok(())
}

/// Owned, wire-compatible export row for the `messages` capture stream.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTraceMessageExportRow {
Expand All @@ -62,7 +61,6 @@ pub struct AgentTraceMessageExportRow {
pub generated_at_unix_ms: i64,
}

/// Owned, wire-compatible export row for the `parts` capture stream.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTracePartExportRow {
Expand All @@ -75,7 +73,6 @@ pub struct AgentTracePartExportRow {
pub generated_at_unix_ms: i64,
}

/// Owned, wire-compatible export row for the `diff_traces` capture stream.
#[allow(
dead_code,
reason = "diff_traces compatibility surface retained per context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md"
Expand All @@ -93,7 +90,6 @@ pub struct AgentTraceDiffTraceExportRow {
pub payload_type: String,
}

/// Owned, wire-compatible export row for the `agent_traces` capture stream.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTraceAgentTraceExportRow {
Expand All @@ -104,6 +100,7 @@ pub struct AgentTraceAgentTraceExportRow {
pub trace_json: String,
pub url: String,
pub remote_url: Option<String>,
pub commit_patch_id: Option<String>,
}

const SELECT_MESSAGES_AFTER_SQL: &str =
Expand Down Expand Up @@ -132,16 +129,12 @@ ORDER BY id ASC
LIMIT ?2";

const SELECT_AGENT_TRACES_AFTER_SQL: &str =
"SELECT id, agent_trace_id, commit_id, commit_time_ms, trace_json, url, remote_url
"SELECT id, agent_trace_id, commit_id, commit_time_ms, trace_json, url, remote_url, commit_patch_id
FROM agent_traces
WHERE id > ?1
ORDER BY id ASC
LIMIT ?2";

/// Read-only incremental export reader over one repository-scoped Agent Trace
/// database. Holds no local cursor, performs no mutation, and makes no
/// network calls; the caller supplies the last server-accepted `id` as
/// `cursor` on every call.
pub struct AgentTraceExportReader<'a> {
db: &'a RepositoryAgentTraceDb,
}
Expand All @@ -151,8 +144,6 @@ impl<'a> AgentTraceExportReader<'a> {
Self { db }
}

/// Read `messages` rows with `id > cursor`, ordered by `id ASC`, capped
/// at `limit`.
pub async fn read_messages_after(
&self,
cursor: i64,
Expand All @@ -178,8 +169,6 @@ impl<'a> AgentTraceExportReader<'a> {
Ok(rows)
}

/// Read `parts` rows with `id > cursor`, ordered by `id ASC`, capped at
/// `limit`.
pub async fn read_parts_after(
&self,
cursor: i64,
Expand All @@ -205,9 +194,6 @@ impl<'a> AgentTraceExportReader<'a> {
Ok(rows)
}

/// Read `diff_traces` rows with `id > cursor`, ordered by `id ASC`,
/// capped at `limit`. `patch` and `payload_type` are returned raw and
/// unmodified: no patch parsing or normalization is performed.
#[allow(
dead_code,
reason = "diff_traces compatibility surface retained per context/decisions/2026-10-01-retire-diff-traces-remote-sync-compatibility-gate.md"
Expand Down Expand Up @@ -237,9 +223,6 @@ impl<'a> AgentTraceExportReader<'a> {
Ok(rows)
}

/// Read `agent_traces` rows with `id > cursor`, ordered by `id ASC`,
/// capped at `limit`. `trace_json` is returned as the exact raw string
/// from `SQLite`: no parse/reserialize is performed.
pub async fn read_agent_traces_after(
&self,
cursor: i64,
Expand Down Expand Up @@ -337,6 +320,9 @@ fn agent_trace_export_row_from_turso(row: &turso::Row) -> Result<AgentTraceAgent
remote_url: row
.get(6)
.context("failed to read agent_traces.remote_url")?,
commit_patch_id: row
.get(7)
.context("failed to read agent_traces.commit_patch_id")?,
})
}

Expand All @@ -354,3 +340,60 @@ fn message_role_from_column(value: &str) -> Result<MessageRole> {
fn limit_as_i64(limit: usize) -> i64 {
i64::try_from(limit).expect("validated limit should fit in i64")
}

#[cfg(test)]
mod tests {
use super::AgentTraceExportReader;
use crate::services::agent_trace_db::transaction_tests::open;
use crate::services::agent_trace_db::AgentTraceInsert;

const RAW_TRACE_JSON: &str = "{\"b\": 1,\n \"a\":2 }";
const PATCH_ID: &str = "0123456789abcdef0123456789abcdef01234567";

#[tokio::test]
async fn read_agent_traces_after_exports_commit_patch_id_with_raw_trace_json() {
let dir = tempfile::tempdir().expect("tempdir");
let db = open(&dir.path().join("agent-trace.db")).await;
for (commit_id, commit_patch_id) in [("with", Some(PATCH_ID)), ("without", None)] {
db.insert_agent_trace(AgentTraceInsert {
commit_id,
commit_time_ms: 1,
trace_json: RAW_TRACE_JSON,
agent_trace_id: commit_id,
url: "https://example.test/trace",
remote_url: "https://example.test/repo.git",
commit_patch_id,
})
.await
.expect("insert agent trace");
}

let rows = AgentTraceExportReader::new(&db)
.read_agent_traces_after(0, 100)
.await
.expect("read agent traces");

let ids: Vec<i64> = rows.iter().map(|row| row.source_row_id).collect();
assert_eq!(ids, [1, 2]);
assert_eq!(rows[0].commit_patch_id.as_deref(), Some(PATCH_ID));
assert_eq!(rows[1].commit_patch_id, None);
assert!(rows.iter().all(|row| row.trace_json == RAW_TRACE_JSON));

let serialized: Vec<serde_json::Value> = rows
.iter()
.map(|row| serde_json::to_value(row).expect("serialize row"))
.collect();
assert_eq!(serialized[0]["commitPatchId"], PATCH_ID);
assert!(serialized[1]
.as_object()
.expect("row object")
.get("commitPatchId")
.is_some_and(serde_json::Value::is_null));
assert!(serialized
.iter()
.all(|row| row["traceJson"] == RAW_TRACE_JSON
&& row["commitId"].is_string()
&& row["sourceRowId"].is_i64()
&& row["remoteUrl"] == "https://example.test/repo.git"));
}
}
Loading
Loading