@os-eco/overstory-cli 0.7.7 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -535,6 +535,7 @@ describe("token snapshots", () => {
535
535
  cacheCreationTokens: 100,
536
536
  estimatedCostUsd: 0.15,
537
537
  modelUsed: "claude-sonnet-4-5",
538
+ runId: null,
538
539
  createdAt: new Date().toISOString(),
539
540
  };
540
541
 
@@ -558,6 +559,7 @@ describe("token snapshots", () => {
558
559
  cacheCreationTokens: 0,
559
560
  estimatedCostUsd: 0.01,
560
561
  modelUsed: "claude-sonnet-4-5",
562
+ runId: null,
561
563
  createdAt: new Date(now - 60_000).toISOString(), // 1 min ago
562
564
  });
563
565
 
@@ -569,6 +571,7 @@ describe("token snapshots", () => {
569
571
  cacheCreationTokens: 0,
570
572
  estimatedCostUsd: 0.02,
571
573
  modelUsed: "claude-sonnet-4-5",
574
+ runId: null,
572
575
  createdAt: new Date(now).toISOString(), // now (most recent)
573
576
  });
574
577
 
@@ -580,6 +583,7 @@ describe("token snapshots", () => {
580
583
  cacheCreationTokens: 0,
581
584
  estimatedCostUsd: 0.03,
582
585
  modelUsed: "claude-sonnet-4-5",
586
+ runId: null,
583
587
  createdAt: new Date(now - 30_000).toISOString(), // 30s ago
584
588
  });
585
589
 
@@ -606,6 +610,7 @@ describe("token snapshots", () => {
606
610
  cacheCreationTokens: 0,
607
611
  estimatedCostUsd: null,
608
612
  modelUsed: null,
613
+ runId: null,
609
614
  createdAt: time1,
610
615
  });
611
616
 
@@ -617,6 +622,7 @@ describe("token snapshots", () => {
617
622
  cacheCreationTokens: 0,
618
623
  estimatedCostUsd: null,
619
624
  modelUsed: null,
625
+ runId: null,
620
626
  createdAt: time2,
621
627
  });
622
628
 
@@ -638,6 +644,7 @@ describe("token snapshots", () => {
638
644
  cacheCreationTokens: 0,
639
645
  estimatedCostUsd: null,
640
646
  modelUsed: null,
647
+ runId: null,
641
648
  createdAt: new Date().toISOString(),
642
649
  });
643
650
 
@@ -649,6 +656,7 @@ describe("token snapshots", () => {
649
656
  cacheCreationTokens: 0,
650
657
  estimatedCostUsd: null,
651
658
  modelUsed: null,
659
+ runId: null,
652
660
  createdAt: new Date().toISOString(),
653
661
  });
654
662
 
@@ -666,6 +674,7 @@ describe("token snapshots", () => {
666
674
  cacheCreationTokens: 0,
667
675
  estimatedCostUsd: null,
668
676
  modelUsed: null,
677
+ runId: null,
669
678
  createdAt: new Date().toISOString(),
670
679
  });
671
680
 
@@ -677,6 +686,7 @@ describe("token snapshots", () => {
677
686
  cacheCreationTokens: 0,
678
687
  estimatedCostUsd: null,
679
688
  modelUsed: null,
689
+ runId: null,
680
690
  createdAt: new Date().toISOString(),
681
691
  });
682
692
 
@@ -698,6 +708,7 @@ describe("token snapshots", () => {
698
708
  cacheCreationTokens: 0,
699
709
  estimatedCostUsd: null,
700
710
  modelUsed: null,
711
+ runId: null,
701
712
  createdAt: new Date(now - 120_000).toISOString(), // 2 min ago
702
713
  });
703
714
 
@@ -709,6 +720,7 @@ describe("token snapshots", () => {
709
720
  cacheCreationTokens: 0,
710
721
  estimatedCostUsd: null,
711
722
  modelUsed: null,
723
+ runId: null,
712
724
  createdAt: new Date(now - 10_000).toISOString(), // 10s ago (recent)
713
725
  });
714
726
 
@@ -729,6 +741,7 @@ describe("token snapshots", () => {
729
741
  cacheCreationTokens: 0,
730
742
  estimatedCostUsd: null,
731
743
  modelUsed: null,
744
+ runId: null,
732
745
  createdAt: new Date().toISOString(),
733
746
  });
734
747
 
@@ -740,6 +753,220 @@ describe("token snapshots", () => {
740
753
  expect(snapshots).toHaveLength(1);
741
754
  expect(snapshots[0]?.agentName).toBe("test-agent");
742
755
  });
756
+
757
+ test("runId roundtrips correctly through snapshot record and retrieval", () => {
758
+ const now = Date.now();
759
+ store.recordSnapshot({
760
+ agentName: "agent-a",
761
+ inputTokens: 100,
762
+ outputTokens: 50,
763
+ cacheReadTokens: 0,
764
+ cacheCreationTokens: 0,
765
+ estimatedCostUsd: null,
766
+ modelUsed: null,
767
+ runId: "run-abc",
768
+ createdAt: new Date(now).toISOString(),
769
+ });
770
+
771
+ store.recordSnapshot({
772
+ agentName: "agent-b",
773
+ inputTokens: 200,
774
+ outputTokens: 100,
775
+ cacheReadTokens: 0,
776
+ cacheCreationTokens: 0,
777
+ estimatedCostUsd: null,
778
+ modelUsed: null,
779
+ runId: null,
780
+ createdAt: new Date(now).toISOString(),
781
+ });
782
+
783
+ const snapshots = store.getLatestSnapshots();
784
+ const agentA = snapshots.find((s) => s.agentName === "agent-a");
785
+ const agentB = snapshots.find((s) => s.agentName === "agent-b");
786
+
787
+ expect(agentA?.runId).toBe("run-abc");
788
+ expect(agentB?.runId).toBeNull();
789
+ });
790
+
791
+ test("getLatestSnapshots(runId) returns only snapshots matching that run", () => {
792
+ const now = Date.now();
793
+ store.recordSnapshot({
794
+ agentName: "agent-a",
795
+ inputTokens: 100,
796
+ outputTokens: 50,
797
+ cacheReadTokens: 0,
798
+ cacheCreationTokens: 0,
799
+ estimatedCostUsd: null,
800
+ modelUsed: null,
801
+ runId: "run-001",
802
+ createdAt: new Date(now).toISOString(),
803
+ });
804
+
805
+ store.recordSnapshot({
806
+ agentName: "agent-b",
807
+ inputTokens: 200,
808
+ outputTokens: 100,
809
+ cacheReadTokens: 0,
810
+ cacheCreationTokens: 0,
811
+ estimatedCostUsd: null,
812
+ modelUsed: null,
813
+ runId: "run-001",
814
+ createdAt: new Date(now).toISOString(),
815
+ });
816
+
817
+ store.recordSnapshot({
818
+ agentName: "agent-c",
819
+ inputTokens: 300,
820
+ outputTokens: 150,
821
+ cacheReadTokens: 0,
822
+ cacheCreationTokens: 0,
823
+ estimatedCostUsd: null,
824
+ modelUsed: null,
825
+ runId: "run-002",
826
+ createdAt: new Date(now).toISOString(),
827
+ });
828
+
829
+ const run001Snapshots = store.getLatestSnapshots("run-001");
830
+ expect(run001Snapshots).toHaveLength(2);
831
+ expect(run001Snapshots.every((s) => s.runId === "run-001")).toBe(true);
832
+
833
+ const run002Snapshots = store.getLatestSnapshots("run-002");
834
+ expect(run002Snapshots).toHaveLength(1);
835
+ expect(run002Snapshots[0]?.agentName).toBe("agent-c");
836
+ });
837
+
838
+ test("getLatestSnapshots(runId) returns empty array for unknown run", () => {
839
+ store.recordSnapshot({
840
+ agentName: "agent-a",
841
+ inputTokens: 100,
842
+ outputTokens: 50,
843
+ cacheReadTokens: 0,
844
+ cacheCreationTokens: 0,
845
+ estimatedCostUsd: null,
846
+ modelUsed: null,
847
+ runId: "run-001",
848
+ createdAt: new Date().toISOString(),
849
+ });
850
+
851
+ const snapshots = store.getLatestSnapshots("run-nonexistent");
852
+ expect(snapshots).toEqual([]);
853
+ });
854
+
855
+ test("getLatestSnapshots(runId) excludes snapshots with null run_id", () => {
856
+ const now = Date.now();
857
+ store.recordSnapshot({
858
+ agentName: "agent-a",
859
+ inputTokens: 100,
860
+ outputTokens: 50,
861
+ cacheReadTokens: 0,
862
+ cacheCreationTokens: 0,
863
+ estimatedCostUsd: null,
864
+ modelUsed: null,
865
+ runId: null, // no run
866
+ createdAt: new Date(now).toISOString(),
867
+ });
868
+
869
+ store.recordSnapshot({
870
+ agentName: "agent-b",
871
+ inputTokens: 200,
872
+ outputTokens: 100,
873
+ cacheReadTokens: 0,
874
+ cacheCreationTokens: 0,
875
+ estimatedCostUsd: null,
876
+ modelUsed: null,
877
+ runId: "run-001",
878
+ createdAt: new Date(now).toISOString(),
879
+ });
880
+
881
+ const run001Snapshots = store.getLatestSnapshots("run-001");
882
+ expect(run001Snapshots).toHaveLength(1);
883
+ expect(run001Snapshots[0]?.agentName).toBe("agent-b");
884
+ });
885
+
886
+ test("getLatestSnapshots(runId) returns latest per agent within the run", () => {
887
+ const now = Date.now();
888
+ // Two snapshots for agent-a in run-001: should only get the latest
889
+ store.recordSnapshot({
890
+ agentName: "agent-a",
891
+ inputTokens: 100,
892
+ outputTokens: 50,
893
+ cacheReadTokens: 0,
894
+ cacheCreationTokens: 0,
895
+ estimatedCostUsd: null,
896
+ modelUsed: null,
897
+ runId: "run-001",
898
+ createdAt: new Date(now - 30_000).toISOString(), // older
899
+ });
900
+
901
+ store.recordSnapshot({
902
+ agentName: "agent-a",
903
+ inputTokens: 500,
904
+ outputTokens: 250,
905
+ cacheReadTokens: 0,
906
+ cacheCreationTokens: 0,
907
+ estimatedCostUsd: null,
908
+ modelUsed: null,
909
+ runId: "run-001",
910
+ createdAt: new Date(now).toISOString(), // latest
911
+ });
912
+
913
+ const snapshots = store.getLatestSnapshots("run-001");
914
+ expect(snapshots).toHaveLength(1);
915
+ expect(snapshots[0]?.inputTokens).toBe(500); // most recent
916
+ });
917
+
918
+ test("migration adds run_id to existing token_snapshots table", () => {
919
+ store.close();
920
+
921
+ // Create a DB with old token_snapshots schema (no run_id column)
922
+ const { Database } = require("bun:sqlite");
923
+ const oldDb = new Database(dbPath);
924
+ oldDb.exec("DROP TABLE IF EXISTS token_snapshots");
925
+ oldDb.exec(`
926
+ CREATE TABLE token_snapshots (
927
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
928
+ agent_name TEXT NOT NULL,
929
+ input_tokens INTEGER NOT NULL DEFAULT 0,
930
+ output_tokens INTEGER NOT NULL DEFAULT 0,
931
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
932
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
933
+ estimated_cost_usd REAL,
934
+ model_used TEXT,
935
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
936
+ )
937
+ `);
938
+ oldDb.exec(`
939
+ INSERT INTO token_snapshots (agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, created_at)
940
+ VALUES ('old-agent', 100, 50, 0, 0, '2026-01-01T00:00:00.000Z')
941
+ `);
942
+ oldDb.close();
943
+
944
+ // Re-open with createMetricsStore which should migrate
945
+ store = createMetricsStore(dbPath);
946
+
947
+ // Old row should be readable with null run_id
948
+ const snapshots = store.getLatestSnapshots();
949
+ expect(snapshots).toHaveLength(1);
950
+ expect(snapshots[0]?.agentName).toBe("old-agent");
951
+ expect(snapshots[0]?.runId).toBeNull();
952
+
953
+ // New rows with run_id should work
954
+ store.recordSnapshot({
955
+ agentName: "new-agent",
956
+ inputTokens: 200,
957
+ outputTokens: 100,
958
+ cacheReadTokens: 0,
959
+ cacheCreationTokens: 0,
960
+ estimatedCostUsd: null,
961
+ modelUsed: null,
962
+ runId: "run-xyz",
963
+ createdAt: new Date().toISOString(),
964
+ });
965
+
966
+ const newSnapshots = store.getLatestSnapshots("run-xyz");
967
+ expect(newSnapshots).toHaveLength(1);
968
+ expect(newSnapshots[0]?.runId).toBe("run-xyz");
969
+ });
743
970
  });
744
971
 
745
972
  // === close ===
@@ -21,8 +21,9 @@ export interface MetricsStore {
21
21
  purge(options: { all?: boolean; agent?: string }): number;
22
22
  /** Record a token usage snapshot for a running agent. */
23
23
  recordSnapshot(snapshot: TokenSnapshot): void;
24
- /** Get the most recent snapshot per active agent (one row per agent). */
25
- getLatestSnapshots(): TokenSnapshot[];
24
+ /** Get the most recent snapshot per active agent (one row per agent).
25
+ * When runId is provided, restricts to snapshots recorded for that run. */
26
+ getLatestSnapshots(runId?: string): TokenSnapshot[];
26
27
  /** Get the timestamp of the most recent snapshot for an agent, or null. */
27
28
  getLatestSnapshotTime(agentName: string): string | null;
28
29
  /** Delete snapshots matching criteria. Returns number of rows deleted. */
@@ -60,6 +61,7 @@ interface SnapshotRow {
60
61
  cache_creation_tokens: number;
61
62
  estimated_cost_usd: number | null;
62
63
  model_used: string | null;
64
+ run_id: string | null;
63
65
  created_at: string;
64
66
  }
65
67
 
@@ -94,6 +96,7 @@ CREATE TABLE IF NOT EXISTS token_snapshots (
94
96
  cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
95
97
  estimated_cost_usd REAL,
96
98
  model_used TEXT,
99
+ run_id TEXT,
97
100
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
98
101
  )`;
99
102
 
@@ -136,6 +139,18 @@ function migrateRunIdColumn(db: Database): void {
136
139
  }
137
140
  }
138
141
 
142
+ /**
143
+ * Migrate an existing token_snapshots table to include the run_id column.
144
+ * Safe to call multiple times — only adds the column if missing.
145
+ */
146
+ function migrateSnapshotRunIdColumn(db: Database): void {
147
+ const rows = db.prepare("PRAGMA table_info(token_snapshots)").all() as Array<{ name: string }>;
148
+ const existingColumns = new Set(rows.map((r) => r.name));
149
+ if (!existingColumns.has("run_id")) {
150
+ db.exec("ALTER TABLE token_snapshots ADD COLUMN run_id TEXT");
151
+ }
152
+ }
153
+
139
154
  /**
140
155
  * Migrate an existing sessions table to include token columns.
141
156
  * Safe to call multiple times — only adds columns that are missing.
@@ -183,6 +198,7 @@ function rowToSnapshot(row: SnapshotRow): TokenSnapshot {
183
198
  cacheCreationTokens: row.cache_creation_tokens,
184
199
  estimatedCostUsd: row.estimated_cost_usd,
185
200
  modelUsed: row.model_used,
201
+ runId: row.run_id,
186
202
  createdAt: row.created_at,
187
203
  };
188
204
  }
@@ -210,6 +226,7 @@ export function createMetricsStore(dbPath: string): MetricsStore {
210
226
  migrateBeadIdToTaskId(db);
211
227
  migrateTokenColumns(db);
212
228
  migrateRunIdColumn(db);
229
+ migrateSnapshotRunIdColumn(db);
213
230
 
214
231
  // Prepare statements for all queries
215
232
  const insertStmt = db.prepare<
@@ -282,13 +299,14 @@ export function createMetricsStore(dbPath: string): MetricsStore {
282
299
  $cache_creation_tokens: number;
283
300
  $estimated_cost_usd: number | null;
284
301
  $model_used: string | null;
302
+ $run_id: string | null;
285
303
  $created_at: string;
286
304
  }
287
305
  >(`
288
306
  INSERT INTO token_snapshots
289
- (agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, model_used, created_at)
307
+ (agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, model_used, run_id, created_at)
290
308
  VALUES
291
- ($agent_name, $input_tokens, $output_tokens, $cache_read_tokens, $cache_creation_tokens, $estimated_cost_usd, $model_used, $created_at)
309
+ ($agent_name, $input_tokens, $output_tokens, $cache_read_tokens, $cache_creation_tokens, $estimated_cost_usd, $model_used, $run_id, $created_at)
292
310
  `);
293
311
 
294
312
  const latestSnapshotsStmt = db.prepare<SnapshotRow, Record<string, never>>(`
@@ -301,6 +319,18 @@ export function createMetricsStore(dbPath: string): MetricsStore {
301
319
  ) latest ON s.agent_name = latest.agent_name AND s.created_at = latest.max_created_at
302
320
  `);
303
321
 
322
+ const latestSnapshotsByRunStmt = db.prepare<SnapshotRow, { $run_id: string }>(`
323
+ SELECT s.*
324
+ FROM token_snapshots s
325
+ INNER JOIN (
326
+ SELECT agent_name, MAX(created_at) as max_created_at
327
+ FROM token_snapshots
328
+ WHERE run_id = $run_id
329
+ GROUP BY agent_name
330
+ ) latest ON s.agent_name = latest.agent_name AND s.created_at = latest.max_created_at
331
+ WHERE s.run_id = $run_id
332
+ `);
333
+
304
334
  const latestSnapshotTimeStmt = db.prepare<
305
335
  { created_at: string } | null,
306
336
  { $agent_name: string }
@@ -401,11 +431,16 @@ export function createMetricsStore(dbPath: string): MetricsStore {
401
431
  $cache_creation_tokens: snapshot.cacheCreationTokens,
402
432
  $estimated_cost_usd: snapshot.estimatedCostUsd,
403
433
  $model_used: snapshot.modelUsed,
434
+ $run_id: snapshot.runId,
404
435
  $created_at: snapshot.createdAt,
405
436
  });
406
437
  },
407
438
 
408
- getLatestSnapshots(): TokenSnapshot[] {
439
+ getLatestSnapshots(runId?: string): TokenSnapshot[] {
440
+ if (runId !== undefined) {
441
+ const rows = latestSnapshotsByRunStmt.all({ $run_id: runId });
442
+ return rows.map(rowToSnapshot);
443
+ }
409
444
  const rows = latestSnapshotsStmt.all({});
410
445
  return rows.map(rowToSnapshot);
411
446
  },