Skip to content

Commit e85987c

Browse files
committed
feat: add agent mode time breakdown
- New mode_times table (session_id, mode, duration_secs) - Sync reads $.mode and $.time.completed - $.time.created for duration - db.rs: insert_mode_times() method for storing mode durations - Tracks time spent in 'build', 'chat', 'read', etc. per session
1 parent 71d2cf5 commit e85987c

2 files changed

Lines changed: 78 additions & 12 deletions

File tree

src/db.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ pub struct ToolUsage {
3939
pub call_count: i64,
4040
}
4141

42+
#[derive(Debug, Clone, Serialize, Deserialize)]
43+
pub struct ModeTime {
44+
pub session_id: String,
45+
pub mode: String,
46+
pub duration_secs: i64,
47+
}
48+
4249
#[derive(Debug, Clone, Default)]
4350
pub struct UsageSummary {
4451
pub total_tokens: i64,
@@ -207,6 +214,14 @@ impl Database {
207214
call_count INTEGER NOT NULL DEFAULT 0,
208215
PRIMARY KEY (session_id, tool_name)
209216
);
217+
218+
-- Agent mode time per session (built vs chat vs read vs etc)
219+
CREATE TABLE IF NOT EXISTS mode_times (
220+
session_id TEXT NOT NULL,
221+
mode TEXT NOT NULL,
222+
duration_secs INTEGER NOT NULL DEFAULT 0,
223+
PRIMARY KEY (session_id, mode)
224+
);
210225
",
211226
)?;
212227

@@ -337,6 +352,25 @@ impl Database {
337352
Ok(inserted)
338353
}
339354

355+
/// Insert or update mode time logs.
356+
pub fn insert_mode_times(&self, modes: &[ModeTime]) -> Result<usize> {
357+
let mut inserted = 0;
358+
for m in modes {
359+
self.conn.execute(
360+
"INSERT OR REPLACE INTO mode_times
361+
(session_id, mode, duration_secs)
362+
VALUES (?1, ?2, ?3)",
363+
params![
364+
m.session_id,
365+
m.mode,
366+
m.duration_secs,
367+
],
368+
)?;
369+
inserted += 1;
370+
}
371+
Ok(inserted)
372+
}
373+
340374
/// Get tool usage breakdown for sessions within the time range.
341375
pub fn tool_usage_by_session(&self, days: i64) -> Result<Vec<(String, i64)>> {
342376
let since = Self::since(days);

src/sync/opencode.rs

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use std::collections::HashMap;
3636

3737
use super::{normalise_provider, RecordBuilder};
3838
use crate::config::Config;
39-
use crate::db::{Database, SessionMeta, ToolUsage};
39+
use crate::db::{Database, ModeTime, SessionMeta, ToolUsage};
4040

4141
pub fn sync(cfg: &Config, db: &Database) -> Result<usize> {
4242
let src_path = locate_db(cfg)?;
@@ -46,7 +46,7 @@ pub fn sync(cfg: &Config, db: &Database) -> Result<usize> {
4646
let last_sync = db.get_last_sync("opencode")?.unwrap_or_default();
4747

4848
// ── Try the "standard" schema first ───────────────────────────────────────
49-
let (records, metas, tool_usage) = match read_standard_schema(&src, &last_sync, cfg) {
49+
let (records, metas, tool_usage, mode_times) = match read_standard_schema(&src, &last_sync, cfg) {
5050
Ok(r) => r,
5151
Err(e) => {
5252
// Fallback: try alternate column names seen in some builds
@@ -72,6 +72,9 @@ pub fn sync(cfg: &Config, db: &Database) -> Result<usize> {
7272
if !tool_usage.is_empty() {
7373
db.insert_tool_usage(&tool_usage)?;
7474
}
75+
if !mode_times.is_empty() {
76+
db.insert_mode_times(&mode_times)?;
77+
}
7578
db.update_sync_state("opencode")?;
7679
Ok(inserted)
7780
}
@@ -113,7 +116,7 @@ fn read_standard_schema(
113116
conn: &Connection,
114117
since: &str,
115118
cfg: &Config,
116-
) -> Result<(Vec<crate::db::UsageRecord>, Vec<SessionMeta>, Vec<ToolUsage>)> {
119+
) -> Result<(Vec<crate::db::UsageRecord>, Vec<SessionMeta>, Vec<ToolUsage>, Vec<ModeTime>)> {
117120
let builder = RecordBuilder { source: "opencode" };
118121

119122
// Convert RFC 3339 last_sync to Unix ms for integer comparison.
@@ -137,6 +140,8 @@ let mut stmt = conn.prepare(
137140
COALESCE(json_extract(m.data, '$.tokens.cacheRead'), 0) AS cache_read,
138141
COALESCE(json_extract(m.data, '$.tokens.cacheWrite'), 0) AS cache_write,
139142
COALESCE(json_extract(m.data, '$.tokens.total'), 0) AS total_tokens,
143+
COALESCE(json_extract(m.data, '$.mode'), 'unknown') AS mode,
144+
COALESCE(json_extract(m.data, '$.time.completed'), 0) - COALESCE(json_extract(m.data, '$.time.created'), 0) AS duration_ms,
140145
m.time_created,
141146
COALESCE(json_extract(m.data, '$.path.cwd'), s.directory) AS cwd,
142147
s.parent_id,
@@ -163,18 +168,21 @@ let mut stmt = conn.prepare(
163168
row.get::<_, i64>(6)?, // cache_read_tokens
164169
row.get::<_, i64>(7)?, // cache_write_tokens
165170
row.get::<_, i64>(8)?, // total_tokens
166-
row.get::<_, i64>(9)?, // time_created (Unix ms)
167-
row.get::<_, Option<String>>(10)?, // cwd
168-
row.get::<_, Option<String>>(11)?, // parent_id
169-
row.get::<_, i64>(12)?, // additions
170-
row.get::<_, i64>(13)?, // deletions
171-
row.get::<_, i64>(14)?, // files_changed
171+
row.get::<_, Option<String>>(9)?, // mode
172+
row.get::<_, i64>(10)?, // duration_ms
173+
row.get::<_, i64>(11)?, // time_created (Unix ms)
174+
row.get::<_, Option<String>>(12)?, // cwd
175+
row.get::<_, Option<String>>(13)?, // parent_id
176+
row.get::<_, i64>(14)?, // additions
177+
row.get::<_, i64>(15)?, // deletions
178+
row.get::<_, i64>(16)?, // files_changed
172179
))
173180
})?;
174181

175182
let mut records = Vec::new();
176183
let mut session_metas: HashMap<String, SessionMeta> = HashMap::new();
177184
let mut tool_counts: HashMap<String, HashMap<String, i64>> = HashMap::new();
185+
let mut mode_durations: HashMap<String, HashMap<String, i64>> = HashMap::new();
178186

179187
// Also fetch tool calls from the part table
180188
let mut tool_stmt = conn.prepare(
@@ -210,6 +218,8 @@ let mut stmt = conn.prepare(
210218
cache_read,
211219
cache_write,
212220
total_tokens,
221+
mode,
222+
duration_ms,
213223
time_ms,
214224
cwd,
215225
parent_id,
@@ -218,6 +228,16 @@ let mut stmt = conn.prepare(
218228
files_changed,
219229
) = row;
220230

231+
// Track mode time per session
232+
let mode_str = mode.unwrap_or_else(|| "unknown".to_string());
233+
if duration_ms > 0 {
234+
let duration_sec = duration_ms / 1000;
235+
mode_durations
236+
.entry(session_id.clone())
237+
.or_default()
238+
.insert(mode_str.clone(), duration_sec);
239+
}
240+
221241
// Track session metadata (code diff summary) - accumulate per session
222242
let entry = session_metas.entry(session_id.clone()).or_insert(SessionMeta {
223243
session_id: session_id.clone(),
@@ -281,7 +301,19 @@ let mut stmt = conn.prepare(
281301
}
282302
}
283303

284-
Ok((records, metas, tool_usage))
304+
// Convert mode_durations to ModeTime vec
305+
let mut mode_times: Vec<ModeTime> = Vec::new();
306+
for (session_id, modes) in mode_durations {
307+
for (mode, duration) in modes {
308+
mode_times.push(ModeTime {
309+
session_id: session_id.clone(),
310+
mode,
311+
duration_secs: duration,
312+
});
313+
}
314+
}
315+
316+
Ok((records, metas, tool_usage, mode_times))
285317
}
286318

287319
// ── Flat-column fallback (older OpenCode builds) ──────────────────────────────
@@ -290,7 +322,7 @@ fn read_alternate_schema(
290322
conn: &Connection,
291323
since: &str,
292324
cfg: &Config,
293-
) -> Result<(Vec<crate::db::UsageRecord>, Vec<SessionMeta>, Vec<ToolUsage>)> {
325+
) -> Result<(Vec<crate::db::UsageRecord>, Vec<SessionMeta>, Vec<ToolUsage>, Vec<ModeTime>)> {
294326
let builder = RecordBuilder { source: "opencode" };
295327

296328
// Older builds stored columns directly on the message table
@@ -353,7 +385,7 @@ fn read_alternate_schema(
353385
records.push(record);
354386
}
355387
// No session metadata or tool usage available in alternate schema
356-
Ok((records, Vec::new(), Vec::new()))
388+
Ok((records, Vec::new(), Vec::new(), Vec::new()))
357389
}
358390

359391
fn infer_provider_from_model(model: &str) -> String {

0 commit comments

Comments
 (0)