Skip to content

Commit 73cf809

Browse files
committed
fix: sign session export cursors with install secret
1 parent ce4158e commit 73cf809

4 files changed

Lines changed: 76 additions & 8 deletions

File tree

internal/db/session_export.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ import (
1818
"go.kenn.io/agentsview/internal/export"
1919
)
2020

21-
const (
22-
sessionExportOrder = "last_activity_at DESC, id ASC"
23-
sessionExportCursorHMACKey = "agentsview-session-summary-export-v1"
24-
)
21+
const sessionExportOrder = "last_activity_at DESC, id ASC"
2522

2623
type sessionExportQuerier interface {
2724
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
@@ -744,7 +741,11 @@ func (db *DB) attachSessionExportWorktrees(
744741

745742
func (db *DB) encodeSessionExportCursor(c sessionExportCursorPayload) string {
746743
data, _ := json.Marshal(c)
747-
mac := hmac.New(sha256.New, []byte(sessionExportCursorHMACKey))
744+
745+
db.cursorMu.RLock()
746+
mac := hmac.New(sha256.New, db.cursorSecret)
747+
db.cursorMu.RUnlock()
748+
748749
mac.Write(data)
749750
sig := mac.Sum(nil)
750751
return base64.RawURLEncoding.EncodeToString(data) + "." +
@@ -775,7 +776,10 @@ func (db *DB) decodeSessionExportCursor(
775776
return sessionExportCursorPayload{},
776777
fmt.Errorf("%w: invalid signature encoding: %v", ErrInvalidCursor, err)
777778
}
778-
mac := hmac.New(sha256.New, []byte(sessionExportCursorHMACKey))
779+
db.cursorMu.RLock()
780+
mac := hmac.New(sha256.New, db.cursorSecret)
781+
db.cursorMu.RUnlock()
782+
779783
mac.Write(data)
780784
if !hmac.Equal(sig, mac.Sum(nil)) {
781785
return sessionExportCursorPayload{},

internal/db/session_export_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package db
22

33
import (
44
"context"
5+
"crypto/hmac"
6+
"crypto/sha256"
57
"database/sql"
68
"encoding/base64"
79
"encoding/json"
@@ -969,6 +971,12 @@ func TestSessionExportCursorRejectsWrongDatabaseAndChangedFilters(t *testing.T)
969971
d2 := testDB(t)
970972
require.NoError(t, d1.SetDatabaseIDForTest(ctx, "db-one"), "set db one")
971973
require.NoError(t, d2.SetDatabaseIDForTest(ctx, "db-two"), "set db two")
974+
// Two archives on the same install share the config cursor_secret; the
975+
// wrong-database reset path applies to that case. Cursors from another
976+
// install fail signature verification instead.
977+
sharedSecret := []byte("session-export-shared-install-secret")
978+
d1.SetCursorSecret(sharedSecret)
979+
d2.SetCursorSecret(sharedSecret)
972980
for _, d := range []*DB{d1, d2} {
973981
insertExportSession(t, d, Session{
974982
ID: "cursor-reset-row",
@@ -1123,6 +1131,62 @@ func TestSessionExportCursorTamperingReturnsInvalidCursor(t *testing.T) {
11231131
"tampered cursor must not be treated as a valid wrong-database cursor")
11241132
}
11251133

1134+
func TestSessionExportCursorRejectsForgeryWithKnownKey(t *testing.T) {
1135+
ctx := context.Background()
1136+
d := testSessionExportDB(t)
1137+
require.NoError(t, d.SetDatabaseIDForTest(ctx, "forge-db"), "set database id")
1138+
for _, row := range []struct {
1139+
id, ended string
1140+
}{
1141+
{"forge-a", "2026-05-01T10:00:00Z"},
1142+
{"forge-b", "2026-05-01T09:00:00Z"},
1143+
} {
1144+
insertExportSession(t, d, Session{
1145+
ID: row.id,
1146+
Project: "forge",
1147+
Machine: "local",
1148+
Agent: "claude",
1149+
StartedAt: Ptr("2026-05-01T00:00:00Z"),
1150+
EndedAt: Ptr(row.ended),
1151+
MessageCount: 1,
1152+
UserMessageCount: 1,
1153+
})
1154+
}
1155+
first, err := d.ExportSessionSummaries(ctx, SessionExportOptions{
1156+
Filter: SessionFilter{Project: "forge"},
1157+
Limit: 1,
1158+
})
1159+
require.NoError(t, err, "first page")
1160+
require.NotEmpty(t, first.NextCursor, "next cursor")
1161+
1162+
// A consumer holding a cursor must not be able to widen its scope by
1163+
// rewriting the payload and re-signing with a publicly known key, such
1164+
// as the constant this cursor was signed with before per-install
1165+
// secrets.
1166+
parts := strings.Split(first.NextCursor, ".")
1167+
require.Len(t, parts, 2, "cursor parts")
1168+
data, err := base64.RawURLEncoding.DecodeString(parts[0])
1169+
require.NoError(t, err, "decode cursor payload")
1170+
var payload map[string]any
1171+
require.NoError(t, json.Unmarshal(data, &payload), "unmarshal payload")
1172+
payload["filters"] = map[string]any{}
1173+
forged, err := json.Marshal(payload)
1174+
require.NoError(t, err, "marshal forged payload")
1175+
mac := hmac.New(sha256.New, []byte("agentsview-session-summary-export-v1"))
1176+
mac.Write(forged)
1177+
forgedCursor := base64.RawURLEncoding.EncodeToString(forged) + "." +
1178+
base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
1179+
1180+
_, err = d.ExportSessionSummaries(ctx, SessionExportOptions{
1181+
Cursor: forgedCursor,
1182+
UseCursorFilter: true,
1183+
Limit: 1,
1184+
})
1185+
require.Error(t, err, "forged cursor")
1186+
assert.True(t, errors.Is(err, ErrInvalidCursor),
1187+
"expected invalid cursor, got %v", err)
1188+
}
1189+
11261190
func testSessionExportDB(t *testing.T) *DB {
11271191
t.Helper()
11281192
d := testDB(t)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"schema_version":1,"database_id":"00000000-0000-4000-8000-000000000001","cursor":{"next":"eyJkYXRhYmFzZV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImZpbHRlcnMiOnsiZXhjbHVkZV9vbmVfc2hvdCI6dHJ1ZSwiYXV0b21hdGVkX3Njb3BlIjoiaHVtYW4ifSwib3JkZXIiOiJsYXN0X2FjdGl2aXR5X2F0IERFU0MsIGlkIEFTQyIsIndhdGVybWFyayI6IjIwMjYtMDctMDNUMTE6MTA6MDBaIiwid2F0ZXJtYXJrX3NvcnQiOjI0NjEyMjQuOTY1Mjc3Nzc4LCJsYXN0X2FjdGl2aXR5X2F0IjoiMjAyNi0wNy0wM1QxMDozMDowMFoiLCJsYXN0X2FjdGl2aXR5X3NvcnQiOjI0NjEyMjQuOTM3NSwibGFzdF9pZCI6InJlbW90ZS1jdXJyZW50IiwibGltaXQiOjIsInNuYXBzaG90X2NvdW50Ijo0LCJzbmFwc2hvdF9kaWdlc3QiOiJlYTdmMmFjNGMzMDUxYzRlMDI0NDVhMjM4YTM1M2M3Yjc5ZmRhYmU2OTE3ZjBlNzMyNTE3ZTg4ZjZmNDk0MTBkIiwicHJlZml4X2NvdW50IjoyLCJwcmVmaXhfZGlnZXN0IjoiNzgyNjMzYmE5MzcwODBjMGE4MWExZTA3NTc3YWVmNjJjMGJhMjM5Zjg4NmEwNDc2ZjY0MDU5NGI5NjMxN2M5ZCJ9.EC0xdv3RLutSecdfxC3L_uVQxc9KLHGDn8iI5lF2Qek"},"pricing":{"source":"fetched","table_version":"2026-07-03T12:00:00Z","latest_row_updated_at":"2026-07-03T12:00:00Z","custom_override_count":0,"effective_row_count":2,"digest":"sha256:28ef6086990aaf44088d7bffc0a7f952068e7399c6a042121ce631beb66d901d","cost_source":"mixed","fallback":{"used":false,"models":[]},"models":{"fixture-model-computed":{"matched_pattern":"fixture-model-computed","input_cost_per_mtok":2,"output_cost_per_mtok":8,"cache_write_cost_per_mtok":3,"cache_read_cost_per_mtok":0.5,"cost_source":"computed"},"fixture-model-reported":{"matched_pattern":"fixture-model-reported","input_cost_per_mtok":1,"output_cost_per_mtok":4,"cache_write_cost_per_mtok":2,"cache_read_cost_per_mtok":0.25,"cost_source":"reported"}}},"projects":{"path-project":{"resolution":"resolved","identity":{"key":"sha256:ae326f252aaa4ab91664d4f43245d8e7f3e3fd073da5bd36a3f72eb72f44526d","key_source":"root_path","root_path":"/fixtures/path-project","machine_local":true}},"remote-project":{"resolution":"resolved","identity":{"key":"sha256:97879729c8ab311e9d4b28941e3a04830b28c527f00af53f2270212eccdbbd39","key_source":"git_remote","normalized_remote":"github.com/acme/remote-project"}}},"sessions":[{"id":"path-current","project":"path-project","machine":"golden-host","agent":"claude","cwd":"/fixtures/path-project/pkg","started_at":"2026-07-03T11:00:00Z","ended_at":"2026-07-03T11:10:00Z","last_activity_at":"2026-07-03T11:10:00Z","duration_seconds":600,"message_count":4,"user_message_count":2,"assistant_message_count":2,"turn_count":2,"classification":"interactive","is_automated":false,"model_usage":{"models":["fixture-model-reported"],"input_tokens":300,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"reasoning_tokens":0,"cost_usd":0.0125,"has_cost":true,"by_model":{"fixture-model-reported":{"model":"fixture-model-reported","input_tokens":300,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"reasoning_tokens":0,"cost_usd":0.0125,"has_cost":true,"cost_source":"reported"}}},"parent_session_id":null,"relationship_type":"root","worktree":null,"total_output_tokens":0,"peak_context_tokens":0,"has_total_output_tokens":false,"has_peak_context_tokens":false},{"id":"remote-current","project":"remote-project","machine":"golden-host","agent":"codex","cwd":"/fixtures/remote-project/worktrees/feature/app","git_branch":"feature/golden","started_at":"2026-07-03T10:00:00Z","ended_at":"2026-07-03T10:30:00Z","last_activity_at":"2026-07-03T10:30:00Z","duration_seconds":1800,"message_count":4,"user_message_count":2,"assistant_message_count":2,"turn_count":2,"classification":"interactive","is_automated":false,"model_usage":{"models":["fixture-model-computed"],"input_tokens":1200,"output_tokens":240,"cache_creation_input_tokens":80,"cache_read_input_tokens":400,"reasoning_tokens":0,"cost_usd":0.00476,"has_cost":true,"by_model":{"fixture-model-computed":{"model":"fixture-model-computed","input_tokens":1200,"output_tokens":240,"cache_creation_input_tokens":80,"cache_read_input_tokens":400,"reasoning_tokens":0,"cost_usd":0.00476,"has_cost":true,"cost_source":"computed"}}},"parent_session_id":null,"relationship_type":"root","worktree":{"name":"feature","root_path":"/fixtures/remote-project"},"total_output_tokens":240,"peak_context_tokens":0,"has_total_output_tokens":true,"has_peak_context_tokens":false}]}
1+
{"schema_version":1,"database_id":"00000000-0000-4000-8000-000000000001","cursor":{"next":"eyJkYXRhYmFzZV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImZpbHRlcnMiOnsiZXhjbHVkZV9vbmVfc2hvdCI6dHJ1ZSwiYXV0b21hdGVkX3Njb3BlIjoiaHVtYW4ifSwib3JkZXIiOiJsYXN0X2FjdGl2aXR5X2F0IERFU0MsIGlkIEFTQyIsIndhdGVybWFyayI6IjIwMjYtMDctMDNUMTE6MTA6MDBaIiwid2F0ZXJtYXJrX3NvcnQiOjI0NjEyMjQuOTY1Mjc3Nzc4LCJsYXN0X2FjdGl2aXR5X2F0IjoiMjAyNi0wNy0wM1QxMDozMDowMFoiLCJsYXN0X2FjdGl2aXR5X3NvcnQiOjI0NjEyMjQuOTM3NSwibGFzdF9pZCI6InJlbW90ZS1jdXJyZW50IiwibGltaXQiOjIsInNuYXBzaG90X2NvdW50Ijo0LCJzbmFwc2hvdF9kaWdlc3QiOiJlYTdmMmFjNGMzMDUxYzRlMDI0NDVhMjM4YTM1M2M3Yjc5ZmRhYmU2OTE3ZjBlNzMyNTE3ZTg4ZjZmNDk0MTBkIiwicHJlZml4X2NvdW50IjoyLCJwcmVmaXhfZGlnZXN0IjoiNzgyNjMzYmE5MzcwODBjMGE4MWExZTA3NTc3YWVmNjJjMGJhMjM5Zjg4NmEwNDc2ZjY0MDU5NGI5NjMxN2M5ZCJ9.Ey7pMRuVxGabOp684h6ymkwQZvPRC4rj8_Sv-jQKdws"},"pricing":{"source":"fetched","table_version":"2026-07-03T12:00:00Z","latest_row_updated_at":"2026-07-03T12:00:00Z","custom_override_count":0,"effective_row_count":2,"digest":"sha256:28ef6086990aaf44088d7bffc0a7f952068e7399c6a042121ce631beb66d901d","cost_source":"mixed","fallback":{"used":false,"models":[]},"models":{"fixture-model-computed":{"matched_pattern":"fixture-model-computed","input_cost_per_mtok":2,"output_cost_per_mtok":8,"cache_write_cost_per_mtok":3,"cache_read_cost_per_mtok":0.5,"cost_source":"computed"},"fixture-model-reported":{"matched_pattern":"fixture-model-reported","input_cost_per_mtok":1,"output_cost_per_mtok":4,"cache_write_cost_per_mtok":2,"cache_read_cost_per_mtok":0.25,"cost_source":"reported"}}},"projects":{"path-project":{"resolution":"resolved","identity":{"key":"sha256:ae326f252aaa4ab91664d4f43245d8e7f3e3fd073da5bd36a3f72eb72f44526d","key_source":"root_path","root_path":"/fixtures/path-project","machine_local":true}},"remote-project":{"resolution":"resolved","identity":{"key":"sha256:97879729c8ab311e9d4b28941e3a04830b28c527f00af53f2270212eccdbbd39","key_source":"git_remote","normalized_remote":"github.com/acme/remote-project"}}},"sessions":[{"id":"path-current","project":"path-project","machine":"golden-host","agent":"claude","cwd":"/fixtures/path-project/pkg","started_at":"2026-07-03T11:00:00Z","ended_at":"2026-07-03T11:10:00Z","last_activity_at":"2026-07-03T11:10:00Z","duration_seconds":600,"message_count":4,"user_message_count":2,"assistant_message_count":2,"turn_count":2,"classification":"interactive","is_automated":false,"model_usage":{"models":["fixture-model-reported"],"input_tokens":300,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"reasoning_tokens":0,"cost_usd":0.0125,"has_cost":true,"by_model":{"fixture-model-reported":{"model":"fixture-model-reported","input_tokens":300,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"reasoning_tokens":0,"cost_usd":0.0125,"has_cost":true,"cost_source":"reported"}}},"parent_session_id":null,"relationship_type":"root","worktree":null,"total_output_tokens":0,"peak_context_tokens":0,"has_total_output_tokens":false,"has_peak_context_tokens":false},{"id":"remote-current","project":"remote-project","machine":"golden-host","agent":"codex","cwd":"/fixtures/remote-project/worktrees/feature/app","git_branch":"feature/golden","started_at":"2026-07-03T10:00:00Z","ended_at":"2026-07-03T10:30:00Z","last_activity_at":"2026-07-03T10:30:00Z","duration_seconds":1800,"message_count":4,"user_message_count":2,"assistant_message_count":2,"turn_count":2,"classification":"interactive","is_automated":false,"model_usage":{"models":["fixture-model-computed"],"input_tokens":1200,"output_tokens":240,"cache_creation_input_tokens":80,"cache_read_input_tokens":400,"reasoning_tokens":0,"cost_usd":0.00476,"has_cost":true,"by_model":{"fixture-model-computed":{"model":"fixture-model-computed","input_tokens":1200,"output_tokens":240,"cache_creation_input_tokens":80,"cache_read_input_tokens":400,"reasoning_tokens":0,"cost_usd":0.00476,"has_cost":true,"cost_source":"computed"}}},"parent_session_id":null,"relationship_type":"root","worktree":{"name":"feature","root_path":"/fixtures/remote-project"},"total_output_tokens":240,"peak_context_tokens":0,"has_total_output_tokens":true,"has_peak_context_tokens":false}]}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{"type":"meta","schema_version":1,"database_id":"00000000-0000-4000-8000-000000000001","cursor":{"next":"eyJkYXRhYmFzZV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImZpbHRlcnMiOnsiZXhjbHVkZV9vbmVfc2hvdCI6dHJ1ZSwiYXV0b21hdGVkX3Njb3BlIjoiaHVtYW4ifSwib3JkZXIiOiJsYXN0X2FjdGl2aXR5X2F0IERFU0MsIGlkIEFTQyIsIndhdGVybWFyayI6IjIwMjYtMDctMDNUMTE6MTA6MDBaIiwid2F0ZXJtYXJrX3NvcnQiOjI0NjEyMjQuOTY1Mjc3Nzc4LCJsYXN0X2FjdGl2aXR5X2F0IjoiMjAyNi0wNy0wM1QxMDozMDowMFoiLCJsYXN0X2FjdGl2aXR5X3NvcnQiOjI0NjEyMjQuOTM3NSwibGFzdF9pZCI6InJlbW90ZS1jdXJyZW50IiwibGltaXQiOjIsInNuYXBzaG90X2NvdW50Ijo0LCJzbmFwc2hvdF9kaWdlc3QiOiJlYTdmMmFjNGMzMDUxYzRlMDI0NDVhMjM4YTM1M2M3Yjc5ZmRhYmU2OTE3ZjBlNzMyNTE3ZTg4ZjZmNDk0MTBkIiwicHJlZml4X2NvdW50IjoyLCJwcmVmaXhfZGlnZXN0IjoiNzgyNjMzYmE5MzcwODBjMGE4MWExZTA3NTc3YWVmNjJjMGJhMjM5Zjg4NmEwNDc2ZjY0MDU5NGI5NjMxN2M5ZCJ9.EC0xdv3RLutSecdfxC3L_uVQxc9KLHGDn8iI5lF2Qek"},"pricing":{"source":"fetched","table_version":"2026-07-03T12:00:00Z","latest_row_updated_at":"2026-07-03T12:00:00Z","custom_override_count":0,"effective_row_count":2,"digest":"sha256:28ef6086990aaf44088d7bffc0a7f952068e7399c6a042121ce631beb66d901d","cost_source":"mixed","fallback":{"used":false,"models":[]},"models":{"fixture-model-computed":{"matched_pattern":"fixture-model-computed","input_cost_per_mtok":2,"output_cost_per_mtok":8,"cache_write_cost_per_mtok":3,"cache_read_cost_per_mtok":0.5,"cost_source":"computed"},"fixture-model-reported":{"matched_pattern":"fixture-model-reported","input_cost_per_mtok":1,"output_cost_per_mtok":4,"cache_write_cost_per_mtok":2,"cache_read_cost_per_mtok":0.25,"cost_source":"reported"}}},"projects":{"path-project":{"resolution":"resolved","identity":{"key":"sha256:ae326f252aaa4ab91664d4f43245d8e7f3e3fd073da5bd36a3f72eb72f44526d","key_source":"root_path","root_path":"/fixtures/path-project","machine_local":true}},"remote-project":{"resolution":"resolved","identity":{"key":"sha256:97879729c8ab311e9d4b28941e3a04830b28c527f00af53f2270212eccdbbd39","key_source":"git_remote","normalized_remote":"github.com/acme/remote-project"}}}}
1+
{"type":"meta","schema_version":1,"database_id":"00000000-0000-4000-8000-000000000001","cursor":{"next":"eyJkYXRhYmFzZV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsImZpbHRlcnMiOnsiZXhjbHVkZV9vbmVfc2hvdCI6dHJ1ZSwiYXV0b21hdGVkX3Njb3BlIjoiaHVtYW4ifSwib3JkZXIiOiJsYXN0X2FjdGl2aXR5X2F0IERFU0MsIGlkIEFTQyIsIndhdGVybWFyayI6IjIwMjYtMDctMDNUMTE6MTA6MDBaIiwid2F0ZXJtYXJrX3NvcnQiOjI0NjEyMjQuOTY1Mjc3Nzc4LCJsYXN0X2FjdGl2aXR5X2F0IjoiMjAyNi0wNy0wM1QxMDozMDowMFoiLCJsYXN0X2FjdGl2aXR5X3NvcnQiOjI0NjEyMjQuOTM3NSwibGFzdF9pZCI6InJlbW90ZS1jdXJyZW50IiwibGltaXQiOjIsInNuYXBzaG90X2NvdW50Ijo0LCJzbmFwc2hvdF9kaWdlc3QiOiJlYTdmMmFjNGMzMDUxYzRlMDI0NDVhMjM4YTM1M2M3Yjc5ZmRhYmU2OTE3ZjBlNzMyNTE3ZTg4ZjZmNDk0MTBkIiwicHJlZml4X2NvdW50IjoyLCJwcmVmaXhfZGlnZXN0IjoiNzgyNjMzYmE5MzcwODBjMGE4MWExZTA3NTc3YWVmNjJjMGJhMjM5Zjg4NmEwNDc2ZjY0MDU5NGI5NjMxN2M5ZCJ9.Ey7pMRuVxGabOp684h6ymkwQZvPRC4rj8_Sv-jQKdws"},"pricing":{"source":"fetched","table_version":"2026-07-03T12:00:00Z","latest_row_updated_at":"2026-07-03T12:00:00Z","custom_override_count":0,"effective_row_count":2,"digest":"sha256:28ef6086990aaf44088d7bffc0a7f952068e7399c6a042121ce631beb66d901d","cost_source":"mixed","fallback":{"used":false,"models":[]},"models":{"fixture-model-computed":{"matched_pattern":"fixture-model-computed","input_cost_per_mtok":2,"output_cost_per_mtok":8,"cache_write_cost_per_mtok":3,"cache_read_cost_per_mtok":0.5,"cost_source":"computed"},"fixture-model-reported":{"matched_pattern":"fixture-model-reported","input_cost_per_mtok":1,"output_cost_per_mtok":4,"cache_write_cost_per_mtok":2,"cache_read_cost_per_mtok":0.25,"cost_source":"reported"}}},"projects":{"path-project":{"resolution":"resolved","identity":{"key":"sha256:ae326f252aaa4ab91664d4f43245d8e7f3e3fd073da5bd36a3f72eb72f44526d","key_source":"root_path","root_path":"/fixtures/path-project","machine_local":true}},"remote-project":{"resolution":"resolved","identity":{"key":"sha256:97879729c8ab311e9d4b28941e3a04830b28c527f00af53f2270212eccdbbd39","key_source":"git_remote","normalized_remote":"github.com/acme/remote-project"}}}}
22
{"id":"path-current","project":"path-project","machine":"golden-host","agent":"claude","cwd":"/fixtures/path-project/pkg","started_at":"2026-07-03T11:00:00Z","ended_at":"2026-07-03T11:10:00Z","last_activity_at":"2026-07-03T11:10:00Z","duration_seconds":600,"message_count":4,"user_message_count":2,"assistant_message_count":2,"turn_count":2,"classification":"interactive","is_automated":false,"model_usage":{"models":["fixture-model-reported"],"input_tokens":300,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"reasoning_tokens":0,"cost_usd":0.0125,"has_cost":true,"by_model":{"fixture-model-reported":{"model":"fixture-model-reported","input_tokens":300,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"reasoning_tokens":0,"cost_usd":0.0125,"has_cost":true,"cost_source":"reported"}}},"parent_session_id":null,"relationship_type":"root","worktree":null,"total_output_tokens":0,"peak_context_tokens":0,"has_total_output_tokens":false,"has_peak_context_tokens":false}
33
{"id":"remote-current","project":"remote-project","machine":"golden-host","agent":"codex","cwd":"/fixtures/remote-project/worktrees/feature/app","git_branch":"feature/golden","started_at":"2026-07-03T10:00:00Z","ended_at":"2026-07-03T10:30:00Z","last_activity_at":"2026-07-03T10:30:00Z","duration_seconds":1800,"message_count":4,"user_message_count":2,"assistant_message_count":2,"turn_count":2,"classification":"interactive","is_automated":false,"model_usage":{"models":["fixture-model-computed"],"input_tokens":1200,"output_tokens":240,"cache_creation_input_tokens":80,"cache_read_input_tokens":400,"reasoning_tokens":0,"cost_usd":0.00476,"has_cost":true,"by_model":{"fixture-model-computed":{"model":"fixture-model-computed","input_tokens":1200,"output_tokens":240,"cache_creation_input_tokens":80,"cache_read_input_tokens":400,"reasoning_tokens":0,"cost_usd":0.00476,"has_cost":true,"cost_source":"computed"}}},"parent_session_id":null,"relationship_type":"root","worktree":{"name":"feature","root_path":"/fixtures/remote-project"},"total_output_tokens":240,"peak_context_tokens":0,"has_total_output_tokens":true,"has_peak_context_tokens":false}

0 commit comments

Comments
 (0)