@oh-my-pi/omp-stats 18.13.2 → 18.14.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 +16 -0
- package/README.md +18 -18
- package/dist/client/index.js +78 -78
- package/dist/types/chunked-insert.d.ts +32 -0
- package/dist/types/client/traces/SpanDrawer.d.ts +9 -3
- package/dist/types/client/traces/TimelineCanvas.d.ts +3 -1
- package/dist/types/client/traces/TraceView.d.ts +13 -0
- package/dist/types/client/traces/index.d.ts +5 -0
- package/dist/types/client/traces/trace-colors.d.ts +3 -1
- package/dist/types/client/ui/SearchInput.d.ts +2 -1
- package/dist/types/db.d.ts +5 -5
- package/dist/types/port-conflict.d.ts +9 -3
- package/dist/types/server.d.ts +7 -3
- package/dist/types/shared-types.d.ts +3 -2
- package/dist/types/trace.d.ts +27 -1
- package/dist/types/types.d.ts +6 -1
- package/package.json +11 -6
- package/src/chunked-insert.ts +89 -0
- package/src/client/traces/SpanDrawer.tsx +56 -9
- package/src/client/traces/TimelineCanvas.tsx +13 -4
- package/src/client/traces/TraceView.tsx +165 -158
- package/src/client/traces/index.ts +5 -0
- package/src/client/traces/trace-colors.ts +14 -10
- package/src/client/ui/SearchInput.tsx +3 -1
- package/src/db.ts +197 -147
- package/src/parser.ts +13 -2
- package/src/port-conflict.ts +67 -30
- package/src/server.ts +39 -31
- package/src/shared-types.ts +3 -2
- package/src/trace.ts +119 -35
- package/src/types.ts +6 -1
- package/THIRD-PARTY-NOTICES.txt +0 -22901
package/src/server.ts
CHANGED
|
@@ -474,19 +474,18 @@ function liveEventStream(headers: Record<string, string>, signal: AbortSignal):
|
|
|
474
474
|
});
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
/**
|
|
478
|
-
* Start the HTTP server, reusing a live dashboard or reclaiming a stale omp listener.
|
|
479
|
-
*/
|
|
477
|
+
/** A dashboard this process started or found; `port` is where it listens, which may differ from the one requested. */
|
|
480
478
|
export interface StatsServerHandle {
|
|
481
479
|
hostname: string;
|
|
482
480
|
port: number;
|
|
483
481
|
stop: () => void;
|
|
484
482
|
}
|
|
485
483
|
|
|
486
|
-
// Dashboards this process already bound, keyed by requested
|
|
487
|
-
// A second in-process start (e.g. `/trace` twice in one
|
|
488
|
-
//
|
|
489
|
-
//
|
|
484
|
+
// Dashboards this process already bound, keyed by requested and bound
|
|
485
|
+
// `hostname:port`. A second in-process start (e.g. `/trace` twice in one
|
|
486
|
+
// session, or a caller passing back a fallback port) must return the live
|
|
487
|
+
// handle: probing our own port can time out under load and would then start
|
|
488
|
+
// yet another dashboard on a fallback port.
|
|
490
489
|
const activeServers = new Map<string, StatsServerHandle>();
|
|
491
490
|
/** Dashboards bound by this process (any port); background ingest stops when the last one does. */
|
|
492
491
|
let liveServers = 0;
|
|
@@ -496,30 +495,34 @@ export interface StartServerOptions {
|
|
|
496
495
|
judge?: StatsJudgeProvider;
|
|
497
496
|
}
|
|
498
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Start the dashboard on `port`, or reuse the current dashboard already serving it.
|
|
500
|
+
* An older dashboard there is stopped and replaced; any other holder (a foreign
|
|
501
|
+
* program, or a dashboard too busy to answer the identity probe) is left running
|
|
502
|
+
* and the dashboard binds an ephemeral port instead.
|
|
503
|
+
*/
|
|
499
504
|
export async function startServer(
|
|
500
505
|
port = 3847,
|
|
501
506
|
hostname = STATS_DASHBOARD_HOSTNAME,
|
|
502
507
|
options: StartServerOptions = {},
|
|
503
508
|
): Promise<StatsServerHandle> {
|
|
504
509
|
if (options.judge) setStatsJudgeProvider(options.judge);
|
|
505
|
-
const
|
|
510
|
+
const requestedKey = `${hostname}:${port}`;
|
|
506
511
|
if (port !== 0) {
|
|
507
|
-
const active = activeServers.get(
|
|
512
|
+
const active = activeServers.get(requestedKey);
|
|
508
513
|
if (active) return active;
|
|
509
514
|
}
|
|
510
515
|
await ensureClientBuild();
|
|
511
|
-
const preparation = await prepareStatsPort(port, hostname);
|
|
512
|
-
if (preparation === "reuse") {
|
|
513
|
-
return { hostname, port, stop: () => {} };
|
|
514
|
-
}
|
|
515
516
|
const register = (server: Server<undefined>): StatsServerHandle => {
|
|
516
517
|
liveServers++;
|
|
517
518
|
let stopped = false;
|
|
519
|
+
const boundPort = server.port ?? port;
|
|
520
|
+
const keys = port === 0 ? [`${hostname}:${boundPort}`] : [requestedKey, `${hostname}:${boundPort}`];
|
|
518
521
|
const handle: StatsServerHandle = {
|
|
519
522
|
hostname,
|
|
520
|
-
port:
|
|
523
|
+
port: boundPort,
|
|
521
524
|
stop: () => {
|
|
522
|
-
activeServers.delete(
|
|
525
|
+
for (const key of keys) activeServers.delete(key);
|
|
523
526
|
server.stop(true);
|
|
524
527
|
if (stopped) return;
|
|
525
528
|
stopped = true;
|
|
@@ -527,26 +530,31 @@ export async function startServer(
|
|
|
527
530
|
if (--liveServers === 0) statsLive().stop();
|
|
528
531
|
},
|
|
529
532
|
};
|
|
530
|
-
|
|
533
|
+
for (const key of keys) activeServers.set(key, handle);
|
|
531
534
|
return handle;
|
|
532
535
|
};
|
|
533
536
|
|
|
537
|
+
let claim = await prepareStatsPort(port, hostname);
|
|
538
|
+
if (claim === "bind") {
|
|
539
|
+
const server = bindDashboardServer(port, hostname);
|
|
540
|
+
if (server) return register(server);
|
|
541
|
+
claim = await recoverStatsPort(port, hostname);
|
|
542
|
+
}
|
|
543
|
+
if (claim === "reuse") return { hostname, port, stop: () => {} };
|
|
544
|
+
if (claim === "bind") {
|
|
545
|
+
const server = bindDashboardServer(port, hostname);
|
|
546
|
+
if (server) return register(server);
|
|
547
|
+
}
|
|
548
|
+
logger.warn("Stats dashboard port is held by another process; serving on an ephemeral port", { hostname, port });
|
|
549
|
+
return register(createDashboardServer(0, hostname));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** {@link createDashboardServer}, or null when the port is already in use. */
|
|
553
|
+
function bindDashboardServer(port: number, hostname: string): Server<undefined> | null {
|
|
534
554
|
try {
|
|
535
|
-
return
|
|
555
|
+
return createDashboardServer(port, hostname);
|
|
536
556
|
} catch (error) {
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
const recovery = await recoverStatsPort(port, hostname);
|
|
540
|
-
if (recovery === "reuse") {
|
|
541
|
-
return { hostname, port, stop: () => {} };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
try {
|
|
545
|
-
return register(createDashboardServer(port, hostname));
|
|
546
|
-
} catch (retryError) {
|
|
547
|
-
throw new Error(`Failed to start stats dashboard on ${hostname}:${port} after reclaiming it.`, {
|
|
548
|
-
cause: retryError,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
557
|
+
if (error instanceof Error && "code" in error && error.code === "EADDRINUSE") return null;
|
|
558
|
+
throw error;
|
|
551
559
|
}
|
|
552
560
|
}
|
package/src/shared-types.ts
CHANGED
|
@@ -604,7 +604,7 @@ export interface TraceTrack {
|
|
|
604
604
|
agent: string | null;
|
|
605
605
|
/** session_init.resolvedModel ?? first assistant model. */
|
|
606
606
|
model: string | null;
|
|
607
|
-
/** Absolute transcript path
|
|
607
|
+
/** Absolute transcript path for live entry fetch, or the embedded snapshot key. */
|
|
608
608
|
file: string;
|
|
609
609
|
/** Sorted by start. */
|
|
610
610
|
spans: TraceSpan[];
|
|
@@ -648,12 +648,13 @@ export interface SessionTrace {
|
|
|
648
648
|
cwd: string | null;
|
|
649
649
|
startedAt: number;
|
|
650
650
|
endedAt: number;
|
|
651
|
-
/** Root transcript mtime
|
|
651
|
+
/** Root transcript mtime, or recorded root activity/header time for offline snapshots. */
|
|
652
652
|
mtimeMs: number;
|
|
653
653
|
/**
|
|
654
654
|
* Conditional-request fingerprint `"<rootMs>:<childHighMs>"` (see
|
|
655
655
|
* traceFingerprintForEtag): the server ETag is built from this, so a
|
|
656
656
|
* subagent-only append changes the ETag even when the root is untouched.
|
|
657
|
+
* Offline snapshots use "snapshot" and do not support HTTP revalidation.
|
|
657
658
|
*/
|
|
658
659
|
etag: string;
|
|
659
660
|
/** DFS order, main first. */
|
package/src/trace.ts
CHANGED
|
@@ -35,7 +35,7 @@ export class TracePathError extends Error {}
|
|
|
35
35
|
* span assembly changes so browsers don't revalidate stale cached bodies
|
|
36
36
|
* against an unchanged transcript mtime.
|
|
37
37
|
*/
|
|
38
|
-
export const TRACE_ETAG_VERSION =
|
|
38
|
+
export const TRACE_ETAG_VERSION = 4;
|
|
39
39
|
|
|
40
40
|
const MAX_TRACK_DEPTH = 6;
|
|
41
41
|
const LABEL_MAX = 80;
|
|
@@ -230,7 +230,7 @@ function unionLength(intervals: Array<[number, number]>): number {
|
|
|
230
230
|
// Single-transcript scan
|
|
231
231
|
|
|
232
232
|
/** Keep only the active branch: the parentId chain walked back from the last entry. */
|
|
233
|
-
function activeChain(views: EntryView[]): EntryView[] {
|
|
233
|
+
function activeChain(views: readonly EntryView[]): EntryView[] {
|
|
234
234
|
const byId = new Map<string, EntryView>();
|
|
235
235
|
let last: EntryView | undefined;
|
|
236
236
|
for (const entry of views) {
|
|
@@ -278,13 +278,14 @@ function argsPreview(args: unknown): string | undefined {
|
|
|
278
278
|
* markers, and the cross-track facts (task results, request counts).
|
|
279
279
|
*/
|
|
280
280
|
function scanTranscript(
|
|
281
|
-
entries: SessionEntry[],
|
|
281
|
+
entries: readonly SessionEntry[],
|
|
282
282
|
trackId: string,
|
|
283
283
|
parentTrackId: string | null,
|
|
284
284
|
file: string,
|
|
285
|
+
header?: TraceSnapshotHeader | null,
|
|
285
286
|
): TrackScan {
|
|
286
287
|
// Boundary cast: persisted JSONL entries are re-validated field-by-field below.
|
|
287
|
-
const views = entries as EntryView[];
|
|
288
|
+
const views = entries as readonly EntryView[];
|
|
288
289
|
const chain = activeChain(views);
|
|
289
290
|
const spans: TraceSpan[] = [];
|
|
290
291
|
const markers: TraceMarker[] = [];
|
|
@@ -297,9 +298,9 @@ function scanTranscript(
|
|
|
297
298
|
const taskResults: TaskResultFact[] = [];
|
|
298
299
|
|
|
299
300
|
let sessionExit: { recordedAt: number; pendingIds: Set<string> } | undefined;
|
|
300
|
-
let cwd
|
|
301
|
-
let headerTitle
|
|
302
|
-
let headerTs
|
|
301
|
+
let cwd = typeof header?.cwd === "string" ? header.cwd : null;
|
|
302
|
+
let headerTitle = typeof header?.title === "string" ? header.title : null;
|
|
303
|
+
let headerTs = toMs(header?.timestamp);
|
|
303
304
|
let slotTitle: string | null = null;
|
|
304
305
|
let initAgent: string | null = null;
|
|
305
306
|
let initModel: string | null = null;
|
|
@@ -689,7 +690,7 @@ async function buildTrackTree(
|
|
|
689
690
|
const childTrackId = trackId === "main" ? stem : `${trackId}/${stem}`;
|
|
690
691
|
const childScan = await buildTrackTree(childFile, childTrackId, trackId, depth + 1, visited, out, scans);
|
|
691
692
|
if (!childScan) continue;
|
|
692
|
-
const isAdvisor =
|
|
693
|
+
const isAdvisor = stem === "__advisor" || stem.startsWith("__advisor.");
|
|
693
694
|
childScan.track.label = isAdvisor
|
|
694
695
|
? "Advisor"
|
|
695
696
|
: headText(`${childScan.track.agent ?? "agent"} ${stem}`, LABEL_MAX);
|
|
@@ -697,6 +698,13 @@ async function buildTrackTree(
|
|
|
697
698
|
}
|
|
698
699
|
}
|
|
699
700
|
|
|
701
|
+
linkChildTracks(scan, childrenById);
|
|
702
|
+
return scan;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Place child activity and structured task-result spans on their parent track. */
|
|
706
|
+
function linkChildTracks(scan: TrackScan, childrenById: ReadonlyMap<string, TrackScan>): void {
|
|
707
|
+
const trackId = scan.track.id;
|
|
700
708
|
// Subagent spans on this track, one per task SingleResult.
|
|
701
709
|
const linkedChildren = new Set<string>();
|
|
702
710
|
for (const result of scan.taskResults) {
|
|
@@ -746,8 +754,6 @@ async function buildTrackTree(
|
|
|
746
754
|
scan.track.spans.push(span);
|
|
747
755
|
}
|
|
748
756
|
scan.track.spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
749
|
-
|
|
750
|
-
return scan;
|
|
751
757
|
}
|
|
752
758
|
|
|
753
759
|
// ---------------------------------------------------------------------------
|
|
@@ -863,6 +869,106 @@ export async function buildSessionTrace(fileParam: string, fingerprint?: TraceFi
|
|
|
863
869
|
if (!rootScan) {
|
|
864
870
|
throw Object.assign(new Error(`ENOENT: session not found: ${resolved}`), { code: "ENOENT" });
|
|
865
871
|
}
|
|
872
|
+
// Re-collect the fingerprint AFTER the parse: a child append landing
|
|
873
|
+
// between the pre-build check and now would otherwise be baked into
|
|
874
|
+
// neither the trace nor the key.
|
|
875
|
+
const childFingerprint = await childTranscriptsFingerprint(resolved);
|
|
876
|
+
// Root rewritten mid-build: same no-cache rule as the child race below.
|
|
877
|
+
const rootChanged = (await fs.stat(resolved).catch(() => null))?.mtimeMs !== rootMtimeMs;
|
|
878
|
+
const raced = childFingerprint !== preChildFingerprint;
|
|
879
|
+
// Raced builds keep the PRE-build fingerprint as their ETag: the parsed
|
|
880
|
+
// bytes predate the racy write, so claiming the post-write state would
|
|
881
|
+
// 304 stale forever when that write was the child's last. The pre-build
|
|
882
|
+
// key guarantees the next conditional poll misses (filesystem moved on)
|
|
883
|
+
// and rebuilds — at most one stale response per race, never an
|
|
884
|
+
// indefinitely cached one. Unraced builds use the post-build key, which
|
|
885
|
+
// equals the pre-build key by construction.
|
|
886
|
+
const etagFingerprint = raced || rootChanged ? preChildFingerprint : childFingerprint;
|
|
887
|
+
const trace = assembleSessionTrace(rootScan, tracks, scans, rootMtimeMs, `${rootMtimeMs}:${etagFingerprint}`);
|
|
888
|
+
if (!raced && !rootChanged) {
|
|
889
|
+
traceMemo = {
|
|
890
|
+
file: resolved,
|
|
891
|
+
entry: { mtimeMs: rootMtimeMs, childFingerprint, trace },
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
return trace;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Header metadata used by snapshot traces, including journals without a schema version. */
|
|
898
|
+
export interface TraceSnapshotHeader {
|
|
899
|
+
timestamp?: string;
|
|
900
|
+
cwd?: string;
|
|
901
|
+
title?: string;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** The journal data for one embedded transcript; its header may be stored separately. */
|
|
905
|
+
export interface SessionTraceSnapshot {
|
|
906
|
+
header?: TraceSnapshotHeader | null;
|
|
907
|
+
entries: readonly SessionEntry[];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** A collected child transcript, keyed by its slash-joined path from the root. */
|
|
911
|
+
export interface SubSessionTraceSnapshot extends SessionTraceSnapshot {
|
|
912
|
+
parent: string | null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Assemble an offline trace using the same timing joins and rollups as the live
|
|
917
|
+
* filesystem builder. Only supplied snapshots participate: no filesystem, DB,
|
|
918
|
+
* network, or wall-clock reads. Track files are snapshot keys, never disk paths.
|
|
919
|
+
*
|
|
920
|
+
* Missing parent transcripts attach their readable descendants to main. Unlike
|
|
921
|
+
* filesystem discovery, already-collected snapshots need no depth limit.
|
|
922
|
+
* `mtimeMs` is the root's last activity (or header time, then 0); the constant
|
|
923
|
+
* `etag` identifies a snapshot, not a conditional HTTP response.
|
|
924
|
+
*/
|
|
925
|
+
export function buildSessionTraceFromSnapshots(
|
|
926
|
+
root: SessionTraceSnapshot,
|
|
927
|
+
subSessions?: Readonly<Record<string, SubSessionTraceSnapshot>>,
|
|
928
|
+
): SessionTrace {
|
|
929
|
+
const children = new Map<string | null, Array<[string, SubSessionTraceSnapshot]>>();
|
|
930
|
+
if (subSessions) {
|
|
931
|
+
for (const key of Object.keys(subSessions).sort()) {
|
|
932
|
+
const snapshot = subSessions[key];
|
|
933
|
+
const parent =
|
|
934
|
+
snapshot.parent !== null && Object.hasOwn(subSessions, snapshot.parent) ? snapshot.parent : null;
|
|
935
|
+
const siblings = children.get(parent);
|
|
936
|
+
if (siblings) siblings.push([key, snapshot]);
|
|
937
|
+
else children.set(parent, [[key, snapshot]]);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const tracks: TraceTrack[] = [];
|
|
942
|
+
const scans: TrackScan[] = [];
|
|
943
|
+
const visit = (snapshot: SessionTraceSnapshot, key: string, parentId: string | null): TrackScan => {
|
|
944
|
+
const scan = scanTranscript(snapshot.entries, key, parentId, key, snapshot.header);
|
|
945
|
+
tracks.push(scan.track);
|
|
946
|
+
scans.push(scan);
|
|
947
|
+
const childrenById = new Map<string, TrackScan>();
|
|
948
|
+
for (const [childKey, childSnapshot] of children.get(parentId === null ? null : key) ?? []) {
|
|
949
|
+
const child = visit(childSnapshot, childKey, key);
|
|
950
|
+
const stem = childKey.slice(childKey.lastIndexOf("/") + 1);
|
|
951
|
+
child.track.label =
|
|
952
|
+
stem === "__advisor" || stem.startsWith("__advisor.")
|
|
953
|
+
? "Advisor"
|
|
954
|
+
: headText(`${child.track.agent ?? "agent"} ${stem}`, LABEL_MAX);
|
|
955
|
+
childrenById.set(stem, child);
|
|
956
|
+
}
|
|
957
|
+
linkChildTracks(scan, childrenById);
|
|
958
|
+
return scan;
|
|
959
|
+
};
|
|
960
|
+
const rootScan = visit(root, "main", null);
|
|
961
|
+
return assembleSessionTrace(rootScan, tracks, scans, rootScan.lastActivity ?? rootScan.headerTs ?? 0, "snapshot");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** Shared aggregation after either filesystem discovery or snapshot traversal. */
|
|
965
|
+
function assembleSessionTrace(
|
|
966
|
+
rootScan: TrackScan,
|
|
967
|
+
tracks: TraceTrack[],
|
|
968
|
+
scans: TrackScan[],
|
|
969
|
+
rootMtimeMs: number,
|
|
970
|
+
etag: string,
|
|
971
|
+
): SessionTrace {
|
|
866
972
|
rootScan.track.label = "Main";
|
|
867
973
|
|
|
868
974
|
let startedAt: number | undefined;
|
|
@@ -935,39 +1041,17 @@ export async function buildSessionTrace(fileParam: string, fingerprint?: TraceFi
|
|
|
935
1041
|
toolStats: [...toolStats.values()].sort((a, b) => b.totalMs - a.totalMs),
|
|
936
1042
|
};
|
|
937
1043
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
// neither the trace nor the key.
|
|
941
|
-
const childFingerprint = await childTranscriptsFingerprint(resolved);
|
|
942
|
-
// Root rewritten mid-build: same no-cache rule as the child race below.
|
|
943
|
-
const rootChanged = (await fs.stat(resolved).catch(() => null))?.mtimeMs !== rootMtimeMs;
|
|
944
|
-
const raced = childFingerprint !== preChildFingerprint;
|
|
945
|
-
// Raced builds keep the PRE-build fingerprint as their ETag: the parsed
|
|
946
|
-
// bytes predate the racy write, so claiming the post-write state would
|
|
947
|
-
// 304 stale forever when that write was the child's last. The pre-build
|
|
948
|
-
// key guarantees the next conditional poll misses (filesystem moved on)
|
|
949
|
-
// and rebuilds — at most one stale response per race, never an
|
|
950
|
-
// indefinitely cached one. Unraced builds use the post-build key, which
|
|
951
|
-
// equals the pre-build key by construction.
|
|
952
|
-
const etagFingerprint = raced || rootChanged ? preChildFingerprint : childFingerprint;
|
|
953
|
-
const trace: SessionTrace = {
|
|
954
|
-
file: resolved,
|
|
1044
|
+
return {
|
|
1045
|
+
file: rootScan.track.file,
|
|
955
1046
|
title: rootScan.title,
|
|
956
1047
|
cwd: rootScan.cwd,
|
|
957
1048
|
startedAt: start,
|
|
958
1049
|
endedAt: end,
|
|
959
1050
|
mtimeMs: rootMtimeMs,
|
|
960
|
-
etag
|
|
1051
|
+
etag,
|
|
961
1052
|
tracks,
|
|
962
1053
|
summary,
|
|
963
1054
|
};
|
|
964
|
-
if (!raced && !rootChanged) {
|
|
965
|
-
traceMemo = {
|
|
966
|
-
file: resolved,
|
|
967
|
-
entry: { mtimeMs: rootMtimeMs, childFingerprint, trace },
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
return trace;
|
|
971
1055
|
}
|
|
972
1056
|
|
|
973
1057
|
/** Fetch one full journal entry by id from a transcript, for the span drawer. */
|
package/src/types.ts
CHANGED
|
@@ -231,12 +231,17 @@ export interface ToolCallStats {
|
|
|
231
231
|
callsInTurn: number;
|
|
232
232
|
/** Serialized argument characters */
|
|
233
233
|
argsChars: number;
|
|
234
|
+
/** Result text characters, when the same parse also read the call's `toolResult` */
|
|
235
|
+
resultChars?: number;
|
|
236
|
+
/** Result error flag, set together with `resultChars` */
|
|
237
|
+
isError?: boolean;
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
/**
|
|
237
241
|
* Result linkage emitted when the parser sees a `toolResult` message entry.
|
|
238
242
|
* Applied as an UPDATE on the persisted tool-call row — results can land in a
|
|
239
|
-
* later incremental sync pass than the call that produced them
|
|
243
|
+
* later incremental sync pass than the call that produced them (calls parsed
|
|
244
|
+
* in the same pass already carry the result, so their UPDATE is a no-op).
|
|
240
245
|
*/
|
|
241
246
|
export interface ToolResultLink {
|
|
242
247
|
sessionFile: string;
|