@oh-my-pi/omp-stats 18.6.3 → 18.8.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/client/index.css +1 -1
  3. package/dist/client/index.js +96 -96
  4. package/dist/types/aggregator.d.ts +3 -0
  5. package/dist/types/client/api.d.ts +5 -1
  6. package/dist/types/client/data/live.d.ts +16 -11
  7. package/dist/types/client/data/query-store.d.ts +39 -0
  8. package/dist/types/client/data/query.d.ts +10 -5
  9. package/dist/types/client/traces/Minimap.d.ts +2 -0
  10. package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
  11. package/dist/types/client/traces/TranscriptList.d.ts +3 -1
  12. package/dist/types/db.d.ts +14 -2
  13. package/dist/types/embedded-client.d.ts +5 -0
  14. package/dist/types/live.d.ts +5 -3
  15. package/dist/types/rollup.d.ts +9 -1
  16. package/dist/types/trace.d.ts +18 -6
  17. package/dist/types/types.d.ts +8 -0
  18. package/package.json +4 -4
  19. package/src/aggregator.ts +26 -9
  20. package/src/client/api.ts +40 -5
  21. package/src/client/app/LiveChip.tsx +2 -2
  22. package/src/client/app/Shell.tsx +2 -2
  23. package/src/client/data/live.tsx +85 -45
  24. package/src/client/data/query-store.ts +108 -0
  25. package/src/client/data/query.ts +47 -56
  26. package/src/client/routes/CostsRoute.tsx +4 -2
  27. package/src/client/routes/ErrorsRoute.tsx +3 -1
  28. package/src/client/routes/FrustrationRoute.tsx +1 -1
  29. package/src/client/routes/GainRoute.tsx +3 -1
  30. package/src/client/routes/ModelsRoute.tsx +9 -2
  31. package/src/client/routes/OverviewRoute.tsx +2 -2
  32. package/src/client/routes/ProjectsRoute.tsx +1 -1
  33. package/src/client/routes/ProvidersRoute.tsx +5 -3
  34. package/src/client/routes/RequestsRoute.tsx +1 -1
  35. package/src/client/routes/ToolsRoute.tsx +1 -1
  36. package/src/client/routes/TracesRoute.tsx +5 -4
  37. package/src/client/styles.css +4 -2
  38. package/src/client/traces/Minimap.tsx +63 -26
  39. package/src/client/traces/TimelineCanvas.tsx +224 -105
  40. package/src/client/traces/TraceView.tsx +29 -10
  41. package/src/client/traces/TranscriptList.tsx +17 -9
  42. package/src/client/traces/traces.css +26 -0
  43. package/src/client/ui/RequestDrawer.tsx +3 -1
  44. package/src/db.ts +123 -69
  45. package/src/embedded-client.ts +9 -0
  46. package/src/live.ts +36 -11
  47. package/src/parser.ts +18 -17
  48. package/src/rollup.ts +41 -20
  49. package/src/server.ts +38 -24
  50. package/src/trace.ts +54 -40
  51. package/src/types.ts +8 -0
package/src/trace.ts CHANGED
@@ -750,11 +750,6 @@ async function buildTrackTree(
750
750
  // ---------------------------------------------------------------------------
751
751
  // Public surface
752
752
 
753
- /**
754
- * Assemble the full span tree for one root session transcript.
755
- * Throws {@link TracePathError} for paths outside the sessions root; ENOENT
756
- * passes through for the caller's 404 mapping.
757
- */
758
753
  // Single-entry memo: the dashboard polls one open trace every 15s. Keyed on
759
754
  // the root file's mtime PLUS a fingerprint of the child-transcript SET under
760
755
  // the root's artifacts tree (one cheap readdir+stat walk, no parse). A
@@ -788,35 +783,46 @@ async function childTranscriptsFingerprint(rootFile: string, depth = 0): Promise
788
783
  return "";
789
784
  }
790
785
  names.sort();
786
+ const dir = transcriptStem(rootFile);
791
787
  const transcriptNames = names.filter(name => name.endsWith(".jsonl") || name.endsWith(".jsonl.gz"));
792
- const parts: string[] = [`n=${transcriptNames.length}`];
793
- for (const name of transcriptNames) {
794
- const childFile = path.join(transcriptStem(rootFile), name);
795
- try {
796
- const stat = await fs.stat(childFile);
797
- parts.push(`${name}@${stat.mtimeMs}`);
798
- // Recurse like the builder (nested subagents), without parsing.
799
- const nested = await childTranscriptsFingerprint(childFile, depth + 1);
800
- if (nested) parts.push(`${name}/{${nested}}`);
801
- } catch {
802
- parts.push(`${name}@gone`);
803
- }
804
- }
805
- return parts.join(",");
788
+ const childParts = await Promise.all(
789
+ transcriptNames.map(async name => {
790
+ const childFile = path.join(dir, name);
791
+ try {
792
+ const stat = await fs.stat(childFile);
793
+ // Recurse like the builder (nested subagents), without parsing.
794
+ const nested = await childTranscriptsFingerprint(childFile, depth + 1);
795
+ return nested ? `${name}@${stat.mtimeMs},${name}/{${nested}}` : `${name}@${stat.mtimeMs}`;
796
+ } catch {
797
+ return `${name}@gone`;
798
+ }
799
+ }),
800
+ );
801
+ return [`n=${transcriptNames.length}`, ...childParts].join(",");
802
+ }
803
+
804
+ /** Freshness of a root trace: root mtime plus the child-transcript set fingerprint. */
805
+ export interface TraceFingerprint {
806
+ rootMtimeMs: number;
807
+ childFingerprint: string;
808
+ }
809
+
810
+ /** Stat the root and walk its child transcripts (readdir+stat, no parse). */
811
+ async function readTraceFingerprint(resolved: string): Promise<TraceFingerprint> {
812
+ const rootMtimeMs = (await fs.stat(resolved)).mtimeMs;
813
+ const childFingerprint = await childTranscriptsFingerprint(resolved);
814
+ return { rootMtimeMs, childFingerprint };
806
815
  }
807
816
 
808
817
  /**
809
- * Freshness fingerprint for the trace ETag pre-check: root mtime plus the
810
- * child-transcript set fingerprint (readdir+stat walk, no parse). Returns
811
- * undefined when the path is rejected or missing (the caller falls through
812
- * to the full build, which maps those to 400/404).
818
+ * Freshness fingerprint for the trace ETag pre-check; `SessionTrace.etag` is
819
+ * `${rootMtimeMs}:${childFingerprint}` for an unraced build. Returns undefined
820
+ * when the path is rejected or missing (the caller falls through to the full
821
+ * build, which maps those to 400/404).
813
822
  */
814
- export async function traceFingerprintForEtag(fileParam: string): Promise<string | undefined> {
823
+ export async function traceFingerprintForEtag(fileParam: string): Promise<TraceFingerprint | undefined> {
815
824
  try {
816
- const resolved = resolveSessionPath(fileParam);
817
- const rootMs = (await fs.stat(resolved)).mtimeMs;
818
- const childFp = await childTranscriptsFingerprint(resolved);
819
- return `${rootMs}:${childFp}`;
825
+ return await readTraceFingerprint(resolveSessionPath(fileParam));
820
826
  } catch {
821
827
  return undefined;
822
828
  }
@@ -827,16 +833,22 @@ export function traceMemoForTests(): { file: string; mtimeMs: number } | undefin
827
833
  return { file: traceMemo.file, mtimeMs: traceMemo.entry.mtimeMs };
828
834
  }
829
835
 
830
- export async function buildSessionTrace(fileParam: string): Promise<SessionTrace> {
836
+ /**
837
+ * Assemble the full span tree for one root session transcript.
838
+ * Throws {@link TracePathError} for paths outside the sessions root; ENOENT
839
+ * passes through for the caller's 404 mapping. `fingerprint` may carry the
840
+ * {@link traceFingerprintForEtag} value the caller just computed for the same
841
+ * file (its ETag pre-check), sparing a second walk.
842
+ */
843
+ export async function buildSessionTrace(fileParam: string, fingerprint?: TraceFingerprint): Promise<SessionTrace> {
831
844
  const resolved = resolveSessionPath(fileParam);
832
- const rootStat = await fs.stat(resolved);
833
845
  const memo = traceMemo?.file === resolved ? traceMemo.entry : undefined;
834
846
  // Pre-build child fingerprint on EVERY path (hit check and rebuild
835
847
  // alike): the post-build race guard compares against this, so a child
836
848
  // append landing mid-parse is detected even on first builds and
837
849
  // memo-less rebuilds, where no prior fingerprint exists to compare.
838
- const preChildFingerprint = await childTranscriptsFingerprint(resolved);
839
- if (memo && memo.mtimeMs === rootStat.mtimeMs) {
850
+ const { rootMtimeMs, childFingerprint: preChildFingerprint } = fingerprint ?? (await readTraceFingerprint(resolved));
851
+ if (memo && memo.mtimeMs === rootMtimeMs) {
840
852
  // Equality, not <=: a LOWER mark (delete/older-mtime replace) is
841
853
  // also stale — only an identical child set may reuse the trace.
842
854
  if (preChildFingerprint === memo.childFingerprint) return memo.trace;
@@ -901,7 +913,7 @@ export async function buildSessionTrace(fileParam: string): Promise<SessionTrace
901
913
  else if (span.kind === "turn") turns++;
902
914
  }
903
915
 
904
- const start = startedAt ?? rootScan.headerTs ?? rootStat.mtimeMs;
916
+ const start = startedAt ?? rootScan.headerTs ?? rootMtimeMs;
905
917
  const end = endedAt ?? start;
906
918
  const wallMs = end - start;
907
919
 
@@ -925,7 +937,7 @@ export async function buildSessionTrace(fileParam: string): Promise<SessionTrace
925
937
  // neither the trace nor the key.
926
938
  const childFingerprint = await childTranscriptsFingerprint(resolved);
927
939
  // Root rewritten mid-build: same no-cache rule as the child race below.
928
- const rootChanged = (await fs.stat(resolved).catch(() => null))?.mtimeMs !== rootStat.mtimeMs;
940
+ const rootChanged = (await fs.stat(resolved).catch(() => null))?.mtimeMs !== rootMtimeMs;
929
941
  const raced = childFingerprint !== preChildFingerprint;
930
942
  // Raced builds keep the PRE-build fingerprint as their ETag: the parsed
931
943
  // bytes predate the racy write, so claiming the post-write state would
@@ -941,15 +953,15 @@ export async function buildSessionTrace(fileParam: string): Promise<SessionTrace
941
953
  cwd: rootScan.cwd,
942
954
  startedAt: start,
943
955
  endedAt: end,
944
- mtimeMs: rootStat.mtimeMs,
945
- etag: `${rootStat.mtimeMs}:${etagFingerprint}`,
956
+ mtimeMs: rootMtimeMs,
957
+ etag: `${rootMtimeMs}:${etagFingerprint}`,
946
958
  tracks,
947
959
  summary,
948
960
  };
949
961
  if (!raced && !rootChanged) {
950
962
  traceMemo = {
951
963
  file: resolved,
952
- entry: { mtimeMs: rootStat.mtimeMs, childFingerprint, trace },
964
+ entry: { mtimeMs: rootMtimeMs, childFingerprint, trace },
953
965
  };
954
966
  }
955
967
  return trace;
@@ -1068,7 +1080,8 @@ function basenameTimestamp(base: string): number | undefined {
1068
1080
  // Short-TTL memo for the disk sweep behind /api/sessions (30s poll): the
1069
1081
  // session list changes only on create/edit, so readdir+stat of every file per
1070
1082
  // poll is pure syscall churn. TTL is deliberately short so a just-created
1071
- // session appears within seconds.
1083
+ // session appears within seconds. Only the newest `limit` roots are kept, so
1084
+ // the memo stays small however many sessions exist.
1072
1085
  let diskRootsMemo:
1073
1086
  | {
1074
1087
  atMs: number;
@@ -1117,8 +1130,9 @@ async function scanDiskRoots(limit: number): Promise<Array<{ file: string; mtime
1117
1130
  }),
1118
1131
  );
1119
1132
  roots.sort((a, b) => b.mtimeMs - a.mtimeMs);
1120
- if (roots.length <= 1000) diskRootsMemo = { atMs: Date.now(), sessionsDir, limit, roots };
1121
- return roots.slice(0, limit);
1133
+ const newest = roots.slice(0, limit);
1134
+ diskRootsMemo = { atMs: Date.now(), sessionsDir, limit, roots: newest };
1135
+ return newest.slice();
1122
1136
  }
1123
1137
 
1124
1138
  /**
package/src/types.ts CHANGED
@@ -35,6 +35,14 @@ export interface MessageStats {
35
35
  usage: Usage;
36
36
  /** Which agent produced this message (main agent, task subagent, advisor) */
37
37
  agentType: AgentType;
38
+ /**
39
+ * Service tier the provider reported serving this turn, or `null` when the
40
+ * provider reported none. Lets consumers separate a fast serving path's
41
+ * throughput and premium traffic from standard-tier traffic. Always written by
42
+ * `rowToMessageStats`; optional only because session fixtures that stand in
43
+ * for ingest input are typed as `MessageStats` too.
44
+ */
45
+ serviceTier?: ServiceTier | null;
38
46
  /**
39
47
  * Ingest refused to price this request: a scheduled (time-based) card with no
40
48
  * recoverable request timestamp, so `usage.cost.total` of 0 is unknown spend