@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.
- package/CHANGELOG.md +14 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +96 -96
- package/dist/types/aggregator.d.ts +3 -0
- package/dist/types/client/api.d.ts +5 -1
- package/dist/types/client/data/live.d.ts +16 -11
- package/dist/types/client/data/query-store.d.ts +39 -0
- package/dist/types/client/data/query.d.ts +10 -5
- package/dist/types/client/traces/Minimap.d.ts +2 -0
- package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
- package/dist/types/client/traces/TranscriptList.d.ts +3 -1
- package/dist/types/db.d.ts +14 -2
- package/dist/types/embedded-client.d.ts +5 -0
- package/dist/types/live.d.ts +5 -3
- package/dist/types/rollup.d.ts +9 -1
- package/dist/types/trace.d.ts +18 -6
- package/dist/types/types.d.ts +8 -0
- package/package.json +4 -4
- package/src/aggregator.ts +26 -9
- package/src/client/api.ts +40 -5
- package/src/client/app/LiveChip.tsx +2 -2
- package/src/client/app/Shell.tsx +2 -2
- package/src/client/data/live.tsx +85 -45
- package/src/client/data/query-store.ts +108 -0
- package/src/client/data/query.ts +47 -56
- package/src/client/routes/CostsRoute.tsx +4 -2
- package/src/client/routes/ErrorsRoute.tsx +3 -1
- package/src/client/routes/FrustrationRoute.tsx +1 -1
- package/src/client/routes/GainRoute.tsx +3 -1
- package/src/client/routes/ModelsRoute.tsx +9 -2
- package/src/client/routes/OverviewRoute.tsx +2 -2
- package/src/client/routes/ProjectsRoute.tsx +1 -1
- package/src/client/routes/ProvidersRoute.tsx +5 -3
- package/src/client/routes/RequestsRoute.tsx +1 -1
- package/src/client/routes/ToolsRoute.tsx +1 -1
- package/src/client/routes/TracesRoute.tsx +5 -4
- package/src/client/styles.css +4 -2
- package/src/client/traces/Minimap.tsx +63 -26
- package/src/client/traces/TimelineCanvas.tsx +224 -105
- package/src/client/traces/TraceView.tsx +29 -10
- package/src/client/traces/TranscriptList.tsx +17 -9
- package/src/client/traces/traces.css +26 -0
- package/src/client/ui/RequestDrawer.tsx +3 -1
- package/src/db.ts +123 -69
- package/src/embedded-client.ts +9 -0
- package/src/live.ts +36 -11
- package/src/parser.ts +18 -17
- package/src/rollup.ts +41 -20
- package/src/server.ts +38 -24
- package/src/trace.ts +54 -40
- 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
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
return
|
|
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
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
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<
|
|
823
|
+
export async function traceFingerprintForEtag(fileParam: string): Promise<TraceFingerprint | undefined> {
|
|
815
824
|
try {
|
|
816
|
-
|
|
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
|
-
|
|
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
|
|
839
|
-
if (memo && memo.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 ??
|
|
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 !==
|
|
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:
|
|
945
|
-
etag: `${
|
|
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:
|
|
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
|
-
|
|
1121
|
-
|
|
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
|