Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/authorship/authorship_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ pub struct PromptRecord {
/// Full URL to CAS-stored messages (format: {api_base_url}/cas/{hash})
#[serde(default, skip_serializing_if = "Option::is_none")]
pub messages_url: Option<String>,
/// Hash of the parent prompt record (for subagent transcripts)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}

impl Eq for PromptRecord {}
Expand Down Expand Up @@ -249,6 +252,7 @@ mod tests {
accepted_lines: 0,
overriden_lines: 0,
messages_url: None,
parent_id: None,
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/authorship/authorship_log_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ mod tests {
accepted_lines: 0,
overriden_lines: 0,
messages_url: None,
parent_id: None,
},
);

Expand Down Expand Up @@ -842,6 +843,7 @@ mod tests {
accepted_lines: 0,
overriden_lines: 0,
messages_url: None,
parent_id: None,
},
);

Expand Down Expand Up @@ -891,6 +893,7 @@ mod tests {
accepted_lines: 0,
overriden_lines: 0,
messages_url: None,
parent_id: None,
},
);

Expand Down Expand Up @@ -1070,6 +1073,7 @@ mod tests {
accepted_lines: 11,
overriden_lines: 0,
messages_url: None,
parent_id: None,
},
);

Expand Down Expand Up @@ -1241,6 +1245,7 @@ mod tests {
accepted_lines: 10,
overriden_lines: 0,
messages_url: None,
parent_id: None,
},
);

Expand All @@ -1265,6 +1270,7 @@ mod tests {
accepted_lines: 20,
overriden_lines: 0,
messages_url: None,
parent_id: None,
},
);

Expand Down
52 changes: 34 additions & 18 deletions src/authorship/internal_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};

/// Current schema version (must match MIGRATIONS.len())
const SCHEMA_VERSION: usize = 3;
const SCHEMA_VERSION: usize = 4;

/// Database migrations - each migration upgrades the schema by one version
/// Migration at index N upgrades from version N to version N+1
Expand Down Expand Up @@ -77,6 +77,10 @@ const MIGRATIONS: &[&str] = &[
cached_at INTEGER NOT NULL
);
"#,
// Migration 3 -> 4: Add parent_id for subagent prompt records
r#"
ALTER TABLE prompts ADD COLUMN parent_id TEXT;
"#,
];

/// Global database singleton
Expand All @@ -98,8 +102,9 @@ pub struct PromptDbRecord {
pub total_deletions: Option<u32>, // Line deletions from checkpoint stats
pub accepted_lines: Option<u32>, // Lines accepted in commit (future)
pub overridden_lines: Option<u32>, // Lines overridden in commit (future)
pub created_at: i64, // Unix timestamp
pub updated_at: i64, // Unix timestamp
pub parent_id: Option<String>, // Parent prompt hash (for subagent records)
pub created_at: i64, // Unix timestamp
pub updated_at: i64, // Unix timestamp
}

impl PromptDbRecord {
Expand Down Expand Up @@ -138,6 +143,7 @@ impl PromptDbRecord {
total_deletions: Some(checkpoint.line_stats.deletions),
accepted_lines: None, // Not yet calculated
overridden_lines: None, // Not yet calculated
parent_id: None,
created_at,
updated_at,
})
Expand All @@ -161,6 +167,7 @@ impl PromptDbRecord {
accepted_lines: self.accepted_lines.unwrap_or(0),
overriden_lines: self.overridden_lines.unwrap_or(0),
messages_url: None,
parent_id: self.parent_id.clone(),
}
}

Expand Down Expand Up @@ -509,8 +516,8 @@ impl InternalDatabase {
id, workdir, tool, model, external_thread_id,
messages, commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
overridden_lines, created_at, updated_at, parent_id
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
ON CONFLICT(id) DO UPDATE SET
workdir = excluded.workdir,
model = excluded.model,
Expand All @@ -522,7 +529,8 @@ impl InternalDatabase {
total_deletions = excluded.total_deletions,
accepted_lines = excluded.accepted_lines,
overridden_lines = excluded.overridden_lines,
updated_at = excluded.updated_at
updated_at = excluded.updated_at,
parent_id = excluded.parent_id
"#,
params![
record.id,
Expand All @@ -540,6 +548,7 @@ impl InternalDatabase {
record.overridden_lines,
record.created_at,
record.updated_at,
record.parent_id,
],
)?;

Expand All @@ -562,8 +571,8 @@ impl InternalDatabase {
id, workdir, tool, model, external_thread_id,
messages, commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
overridden_lines, created_at, updated_at, parent_id
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
ON CONFLICT(id) DO UPDATE SET
workdir = excluded.workdir,
model = excluded.model,
Expand All @@ -575,7 +584,8 @@ impl InternalDatabase {
total_deletions = excluded.total_deletions,
accepted_lines = excluded.accepted_lines,
overridden_lines = excluded.overridden_lines,
updated_at = excluded.updated_at
updated_at = excluded.updated_at,
parent_id = excluded.parent_id
"#,
)?;

Expand All @@ -602,6 +612,7 @@ impl InternalDatabase {
record.overridden_lines,
record.created_at,
record.updated_at,
record.parent_id,
])?;
}
}
Expand All @@ -616,7 +627,7 @@ impl InternalDatabase {
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE id = ?1",
)?;

Expand Down Expand Up @@ -648,6 +659,7 @@ impl InternalDatabase {
total_deletions: row.get(10)?,
accepted_lines: row.get(11)?,
overridden_lines: row.get(12)?,
parent_id: row.get(15)?,
created_at: row.get(13)?,
updated_at: row.get(14)?,
})
Expand All @@ -670,7 +682,7 @@ impl InternalDatabase {
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE commit_sha = ?1",
)?;

Expand Down Expand Up @@ -702,6 +714,7 @@ impl InternalDatabase {
total_deletions: row.get(10)?,
accepted_lines: row.get(11)?,
overridden_lines: row.get(12)?,
parent_id: row.get(15)?,
created_at: row.get(13)?,
updated_at: row.get(14)?,
})
Expand All @@ -728,31 +741,31 @@ impl InternalDatabase {
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE workdir = ?1 AND updated_at >= ?2 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4".to_string(),
vec![Box::new(wd.to_string()), Box::new(ts), Box::new(limit as i64), Box::new(offset as i64)],
),
(Some(wd), None) => (
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE workdir = ?1 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3".to_string(),
vec![Box::new(wd.to_string()), Box::new(limit as i64), Box::new(offset as i64)],
),
(None, Some(ts)) => (
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE updated_at >= ?1 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3".to_string(),
vec![Box::new(ts), Box::new(limit as i64), Box::new(offset as i64)],
),
(None, None) => (
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts ORDER BY updated_at DESC LIMIT ?1 OFFSET ?2".to_string(),
vec![Box::new(limit as i64), Box::new(offset as i64)],
),
Expand Down Expand Up @@ -789,6 +802,7 @@ impl InternalDatabase {
total_deletions: row.get(10)?,
accepted_lines: row.get(11)?,
overridden_lines: row.get(12)?,
parent_id: row.get(15)?,
created_at: row.get(13)?,
updated_at: row.get(14)?,
})
Expand Down Expand Up @@ -817,15 +831,15 @@ impl InternalDatabase {
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE messages LIKE ?1 AND workdir = ?2 ORDER BY updated_at DESC LIMIT ?3 OFFSET ?4".to_string(),
vec![Box::new(search_pattern), Box::new(wd.to_string()), Box::new(limit as i64), Box::new(offset as i64)],
),
None => (
"SELECT id, workdir, tool, model, external_thread_id, messages,
commit_sha, agent_metadata, human_author,
total_additions, total_deletions, accepted_lines,
overridden_lines, created_at, updated_at
overridden_lines, created_at, updated_at, parent_id
FROM prompts WHERE messages LIKE ?1 ORDER BY updated_at DESC LIMIT ?2 OFFSET ?3".to_string(),
vec![Box::new(search_pattern), Box::new(limit as i64), Box::new(offset as i64)],
),
Expand Down Expand Up @@ -862,6 +876,7 @@ impl InternalDatabase {
total_deletions: row.get(10)?,
accepted_lines: row.get(11)?,
overridden_lines: row.get(12)?,
parent_id: row.get(15)?,
created_at: row.get(13)?,
updated_at: row.get(14)?,
})
Expand Down Expand Up @@ -1106,6 +1121,7 @@ mod tests {
total_deletions: Some(5),
accepted_lines: None,
overridden_lines: None,
parent_id: None,
created_at: 1234567890,
updated_at: 1234567890,
}
Expand Down Expand Up @@ -1135,7 +1151,7 @@ mod tests {
|row| row.get(0),
)
.unwrap();
assert_eq!(version, "3");
assert_eq!(version, "4");
}

#[test]
Expand Down
77 changes: 76 additions & 1 deletion src/authorship/post_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::authorship::ignore::{
use crate::authorship::prompt_utils::{PromptUpdateResult, update_prompt_from_tool};
use crate::authorship::secrets::{redact_secrets_from_prompts, strip_prompt_messages};
use crate::authorship::stats::{stats_for_commit_stats, write_stats_to_terminal};
use crate::authorship::transcript::AiTranscript;
use crate::authorship::virtual_attribution::VirtualAttributions;
use crate::authorship::working_log::{Checkpoint, CheckpointKind, WorkingLogEntry};
use crate::config::{Config, PromptStorageMode};
Expand Down Expand Up @@ -395,12 +396,31 @@ fn update_prompts_to_latest(checkpoints: &mut [Checkpoint]) -> Result<(), GitAiE

// Apply the update to the last checkpoint only
match result {
PromptUpdateResult::Updated(latest_transcript, latest_model) => {
PromptUpdateResult::Updated(latest_transcript, latest_model, subagents) => {
let checkpoint = &mut checkpoints[last_idx];
checkpoint.transcript = Some(latest_transcript);
if let Some(agent_id) = &mut checkpoint.agent_id {
agent_id.model = latest_model;
}
// Store subagent info in agent_metadata for downstream expansion
if !subagents.is_empty() {
let checkpoint = &mut checkpoints[last_idx];
let metadata = checkpoint.agent_metadata.get_or_insert_with(HashMap::new);
if let Ok(subagents_json) = serde_json::to_string(
&subagents
.iter()
.map(|s| {
serde_json::json!({
"agent_id": s.agent_id,
"transcript": s.transcript,
"model": s.model,
})
})
.collect::<Vec<_>>(),
) {
metadata.insert("__subagents".to_string(), subagents_json);
}
}
}
PromptUpdateResult::Unchanged => {
// No update available, keep existing transcript
Expand Down Expand Up @@ -455,6 +475,61 @@ fn batch_upsert_prompts_to_db(
) {
records.push(record);
}

// Check for subagent data in agent_metadata and expand into separate records
if let Some(metadata) = &checkpoint.agent_metadata
&& let Some(subagents_json) = metadata.get("__subagents")
&& let Ok(subagents) = serde_json::from_str::<Vec<serde_json::Value>>(subagents_json)
{
let parent_hash = checkpoint.agent_id.as_ref().map(|aid| {
crate::authorship::authorship_log_serialization::generate_short_hash(
&aid.id, &aid.tool,
)
});
for subagent in subagents {
if let (Some(agent_id_str), Some(transcript_value)) = (
subagent.get("agent_id").and_then(|v| v.as_str()),
subagent.get("transcript"),
) {
let subagent_hash =
crate::authorship::authorship_log_serialization::generate_short_hash(
agent_id_str,
"claude",
);
if let Ok(transcript) =
serde_json::from_value::<AiTranscript>(transcript_value.clone())
{
let model = subagent
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
records.push(PromptDbRecord {
id: subagent_hash,
workdir: Some(workdir.clone()),
tool: "claude".to_string(),
model,
external_thread_id: agent_id_str.to_string(),
messages: transcript,
commit_sha: Some(commit_sha.to_string()),
agent_metadata: None,
human_author: Some(checkpoint.author.clone()),
total_additions: None,
total_deletions: None,
accepted_lines: None,
overridden_lines: None,
parent_id: parent_hash.clone(),
created_at: now,
updated_at: now,
});
}
}
}
}
}

if records.is_empty() {
Expand Down
Loading
Loading