@tonyclaw/agent-inspector 2.0.42 → 2.1.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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-Cfqxlo-U.js → CompareDrawer-VDpcSNGM.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-fsXSjqtm.js +117 -0
- package/.output/public/assets/{ReplayDialog-daRidZo_.js → ReplayDialog-CoDgTzHR.js} +1 -1
- package/.output/public/assets/RequestAnatomy-t3plgkPE.js +1 -0
- package/.output/public/assets/ResponseView-BYuKiNeG.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-nK-0LpbM.js → StreamingChunkSequence-DOdU38Is.js} +1 -1
- package/.output/public/assets/_sessionId-ew5QlyZ2.js +1 -0
- package/.output/public/assets/index-DFICWD6o.js +1 -0
- package/.output/public/assets/index-zLLCkAnF.css +1 -0
- package/.output/public/assets/{main-DOy_Q96H.js → main-DmzLt6ti.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +219 -178
- package/.output/server/{_sessionId-DtYRZHlM.mjs → _sessionId-CCN1UoEf.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-fy1ARwtx.mjs → CompareDrawer-DkSY3vxP.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-9GJtCWCq.mjs → ProxyViewerContainer-OTh1V_Kh.mjs} +1034 -448
- package/.output/server/_ssr/{ReplayDialog-BrS7syOE.mjs → ReplayDialog-DhI6yxrR.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-BtWt1cWY.mjs → RequestAnatomy-DN1SJVVL.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-CkKGYE6m.mjs → ResponseView-DW-kpJL5.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-C7rB3osr.mjs → StreamingChunkSequence-BAE57Fae.mjs} +4 -4
- package/.output/server/_ssr/{index-D24WforP.mjs → index-C9ryJEna.mjs} +3 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CVqVjBzJ.mjs → router-DIIJGyML.mjs} +458 -10
- package/.output/server/{_tanstack-start-manifest_v-DSikvwru.mjs → _tanstack-start-manifest_v-C_v9-E5d.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +74 -68
- package/src/components/ProxyViewer.tsx +942 -297
- package/src/components/ProxyViewerContainer.tsx +199 -5
- package/src/components/groups/GroupsDialog.tsx +11 -10
- package/src/components/proxy-viewer/LogEntry.tsx +107 -56
- package/src/components/proxy-viewer/LogEntryHeader.tsx +12 -6
- package/src/components/ui/crab-logo.tsx +0 -50
- package/src/components/ui/dialog.tsx +1 -1
- package/src/proxy/logIndex.ts +188 -1
- package/src/proxy/store.ts +405 -3
- package/src/routes/api/logs.ts +36 -0
- package/.output/public/assets/ProxyViewerContainer-CE9pAX4c.js +0 -117
- package/.output/public/assets/RequestAnatomy-CJ7EPQGQ.js +0 -1
- package/.output/public/assets/ResponseView-xBmr54hB.js +0 -1
- package/.output/public/assets/_sessionId-CbWEG5ej.js +0 -1
- package/.output/public/assets/index-BfGJEb-2.css +0 -1
- package/.output/public/assets/index-D1MkoT4l.js +0 -1
package/src/proxy/store.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { getChunksDir } from "./chunkStorage";
|
|
22
22
|
import {
|
|
23
23
|
addToIndex,
|
|
24
|
+
createLogIndexEntryMetadata,
|
|
24
25
|
findInIndex,
|
|
25
26
|
flushIndex,
|
|
26
27
|
getNextLogId,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
listIndexEntries,
|
|
29
30
|
rebuildIndex,
|
|
30
31
|
type LogIndexEntry,
|
|
32
|
+
type LogIndexSummary,
|
|
31
33
|
} from "./logIndex";
|
|
32
34
|
import { writeChunks } from "./chunkStorage";
|
|
33
35
|
import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
|
|
@@ -92,6 +94,26 @@ export type ListLogsPageResult = {
|
|
|
92
94
|
limit: number;
|
|
93
95
|
};
|
|
94
96
|
|
|
97
|
+
export type ListLogsCursorPageOptions = {
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
model?: string;
|
|
100
|
+
limit: number;
|
|
101
|
+
includeBodies?: boolean;
|
|
102
|
+
beforeLogId?: number;
|
|
103
|
+
afterLogId?: number;
|
|
104
|
+
anchor?: "newest" | "oldest";
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type ListLogsCursorPageResult = {
|
|
108
|
+
logs: CapturedLog[];
|
|
109
|
+
total: number;
|
|
110
|
+
limit: number;
|
|
111
|
+
hasOlder: boolean;
|
|
112
|
+
hasNewer: boolean;
|
|
113
|
+
oldestLogId: number | null;
|
|
114
|
+
newestLogId: number | null;
|
|
115
|
+
};
|
|
116
|
+
|
|
95
117
|
export type GetSessionInfoOptions = {
|
|
96
118
|
baseUrl: string;
|
|
97
119
|
includeHistory?: boolean;
|
|
@@ -100,6 +122,74 @@ export type GetSessionInfoOptions = {
|
|
|
100
122
|
|
|
101
123
|
// Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
|
|
102
124
|
const memoryCache: Map<number, CapturedLog> = new Map();
|
|
125
|
+
const MAX_CURSOR_PAGE_INDEXES = 16;
|
|
126
|
+
|
|
127
|
+
type LogCursorPageIndex = {
|
|
128
|
+
key: string;
|
|
129
|
+
sessionId?: string;
|
|
130
|
+
model?: string;
|
|
131
|
+
ids: number[];
|
|
132
|
+
logsById: Map<number, CapturedLog>;
|
|
133
|
+
entriesById: Map<number, LogIndexEntry>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const logCursorPageIndexes: Map<string, LogCursorPageIndex> = new Map();
|
|
137
|
+
let logCursorPageIndexBuildCount = 0;
|
|
138
|
+
|
|
139
|
+
function cursorPageIndexKey(
|
|
140
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
141
|
+
): string {
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
sessionId: options.sessionId ?? null,
|
|
144
|
+
model: options.model ?? null,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function matchesCursorIndexFilters(log: CapturedLog, index: LogCursorPageIndex): boolean {
|
|
149
|
+
if (index.sessionId !== undefined && getLogSessionId(log) !== index.sessionId) return false;
|
|
150
|
+
if (index.model !== undefined && log.model !== index.model) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function insertSortedLogId(ids: number[], id: number): void {
|
|
155
|
+
if (ids.includes(id)) return;
|
|
156
|
+
const insertionIndex = ids.findIndex((candidate) => candidate > id);
|
|
157
|
+
if (insertionIndex === -1) {
|
|
158
|
+
ids.push(id);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
ids.splice(insertionIndex, 0, id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function upsertLogCursorPageIndexes(log: CapturedLog): void {
|
|
165
|
+
for (const index of logCursorPageIndexes.values()) {
|
|
166
|
+
if (!matchesCursorIndexFilters(log, index)) {
|
|
167
|
+
if (index.logsById.delete(log.id)) {
|
|
168
|
+
index.ids = index.ids.filter((id) => id !== log.id);
|
|
169
|
+
}
|
|
170
|
+
index.entriesById.delete(log.id);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
index.logsById.set(log.id, log);
|
|
174
|
+
insertSortedLogId(index.ids, log.id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function removeLogsFromCursorPageIndexes(ids: ReadonlySet<number>): void {
|
|
179
|
+
if (ids.size === 0) return;
|
|
180
|
+
for (const index of logCursorPageIndexes.values()) {
|
|
181
|
+
for (const id of ids) {
|
|
182
|
+
index.logsById.delete(id);
|
|
183
|
+
index.entriesById.delete(id);
|
|
184
|
+
}
|
|
185
|
+
index.ids = index.ids.filter((id) => !ids.has(id));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function clearLogCursorPageIndexes(): void {
|
|
190
|
+
logCursorPageIndexes.clear();
|
|
191
|
+
logCursorPageIndexBuildCount = 0;
|
|
192
|
+
}
|
|
103
193
|
|
|
104
194
|
function evictOldestIfNeeded(): void {
|
|
105
195
|
while (memoryCache.size > MAX_MEMORY_CACHE) {
|
|
@@ -125,11 +215,13 @@ function addToCache(log: CapturedLog): void {
|
|
|
125
215
|
memoryCache.delete(cachedLog.id);
|
|
126
216
|
}
|
|
127
217
|
memoryCache.set(cachedLog.id, cachedLog);
|
|
218
|
+
upsertLogCursorPageIndexes(cachedLog);
|
|
128
219
|
evictOldestIfNeeded();
|
|
129
220
|
}
|
|
130
221
|
|
|
131
222
|
function removeFromCache(id: number): void {
|
|
132
223
|
memoryCache.delete(id);
|
|
224
|
+
removeLogsFromCursorPageIndexes(new Set([id]));
|
|
133
225
|
}
|
|
134
226
|
|
|
135
227
|
function textByteLength(value: string | null): number | null {
|
|
@@ -151,6 +243,45 @@ export function compactLogForList(log: CapturedLog): CapturedLog {
|
|
|
151
243
|
};
|
|
152
244
|
}
|
|
153
245
|
|
|
246
|
+
function compactLogFromIndexSummary(summary: LogIndexSummary): CapturedLog {
|
|
247
|
+
return normalizeLog({
|
|
248
|
+
id: summary.id,
|
|
249
|
+
timestamp: summary.timestamp,
|
|
250
|
+
method: summary.method,
|
|
251
|
+
path: summary.path,
|
|
252
|
+
model: summary.model,
|
|
253
|
+
sessionId: summary.sessionId,
|
|
254
|
+
rawRequestBody: null,
|
|
255
|
+
responseStatus: summary.responseStatus,
|
|
256
|
+
responseText: null,
|
|
257
|
+
inputTokens: summary.inputTokens,
|
|
258
|
+
outputTokens: summary.outputTokens,
|
|
259
|
+
cacheCreationInputTokens: summary.cacheCreationInputTokens,
|
|
260
|
+
cacheReadInputTokens: summary.cacheReadInputTokens,
|
|
261
|
+
elapsedMs: summary.elapsedMs,
|
|
262
|
+
firstChunkMs: summary.firstChunkMs,
|
|
263
|
+
totalStreamMs: summary.totalStreamMs,
|
|
264
|
+
tokensPerSecond: summary.tokensPerSecond,
|
|
265
|
+
streaming: summary.streaming,
|
|
266
|
+
userAgent: summary.userAgent,
|
|
267
|
+
origin: summary.origin,
|
|
268
|
+
apiFormat: summary.apiFormat,
|
|
269
|
+
isTest: summary.isTest,
|
|
270
|
+
replayOfLogId: summary.replayOfLogId,
|
|
271
|
+
providerName: summary.providerName,
|
|
272
|
+
clientPort: summary.clientPort,
|
|
273
|
+
clientPid: summary.clientPid,
|
|
274
|
+
clientCwd: summary.clientCwd,
|
|
275
|
+
clientProjectFolder: summary.clientProjectFolder,
|
|
276
|
+
streamingChunksPath: summary.streamingChunksPath,
|
|
277
|
+
rawRequestBodyBytes: summary.rawRequestBodyBytes,
|
|
278
|
+
responseTextBytes: summary.responseTextBytes,
|
|
279
|
+
bodyContentMode: "compact",
|
|
280
|
+
warnings: summary.warnings,
|
|
281
|
+
error: summary.error,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
154
285
|
function prepareLogForList(log: CapturedLog, includeBodies: boolean): CapturedLog {
|
|
155
286
|
return includeBodies ? log : compactLogForList(log);
|
|
156
287
|
}
|
|
@@ -187,7 +318,7 @@ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<C
|
|
|
187
318
|
await runWithLogWriteLock(async () => {
|
|
188
319
|
const logFile = getCurrentLogFile();
|
|
189
320
|
appendLogEntry(log);
|
|
190
|
-
await addToIndex(id, logFile, -1, -1);
|
|
321
|
+
await addToIndex(id, logFile, -1, -1, createLogIndexEntryMetadata(log));
|
|
191
322
|
});
|
|
192
323
|
|
|
193
324
|
addToCache(log);
|
|
@@ -258,7 +389,7 @@ export async function createLog(
|
|
|
258
389
|
await runWithLogWriteLock(async () => {
|
|
259
390
|
const logFile = getCurrentLogFile();
|
|
260
391
|
appendLogEntry(log);
|
|
261
|
-
await addToIndex(id, logFile, -1, -1);
|
|
392
|
+
await addToIndex(id, logFile, -1, -1, createLogIndexEntryMetadata(log));
|
|
262
393
|
});
|
|
263
394
|
|
|
264
395
|
// Add to memory cache
|
|
@@ -522,6 +653,234 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
|
|
|
522
653
|
};
|
|
523
654
|
}
|
|
524
655
|
|
|
656
|
+
function matchesCursorWindow(id: number, options: ListLogsCursorPageOptions): boolean {
|
|
657
|
+
if (options.beforeLogId !== undefined && id >= options.beforeLogId) return false;
|
|
658
|
+
if (options.afterLogId !== undefined && id <= options.afterLogId) return false;
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function selectCursorPageLogs(
|
|
663
|
+
ids: readonly number[],
|
|
664
|
+
options: ListLogsCursorPageOptions,
|
|
665
|
+
): number[] {
|
|
666
|
+
const matchingWindow = ids.filter((id) => matchesCursorWindow(id, options));
|
|
667
|
+
if (options.afterLogId !== undefined || options.anchor === "oldest") {
|
|
668
|
+
return matchingWindow.slice(0, options.limit);
|
|
669
|
+
}
|
|
670
|
+
return matchingWindow.slice(Math.max(0, matchingWindow.length - options.limit));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function rememberLogCursorPageIndex(index: LogCursorPageIndex): LogCursorPageIndex {
|
|
674
|
+
if (logCursorPageIndexes.has(index.key)) {
|
|
675
|
+
logCursorPageIndexes.delete(index.key);
|
|
676
|
+
}
|
|
677
|
+
logCursorPageIndexes.set(index.key, index);
|
|
678
|
+
while (logCursorPageIndexes.size > MAX_CURSOR_PAGE_INDEXES) {
|
|
679
|
+
const oldestKey = logCursorPageIndexes.keys().next().value;
|
|
680
|
+
if (oldestKey === undefined) break;
|
|
681
|
+
logCursorPageIndexes.delete(oldestKey);
|
|
682
|
+
}
|
|
683
|
+
return index;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function hasNullableStringField(entry: LogIndexEntry, field: "sessionId" | "model"): boolean {
|
|
687
|
+
const desc = Object.getOwnPropertyDescriptor(entry, field);
|
|
688
|
+
if (desc === undefined) return false;
|
|
689
|
+
return typeof desc.value === "string" || desc.value === null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function hasLogIndexFilterMetadata(entry: LogIndexEntry): boolean {
|
|
693
|
+
return hasNullableStringField(entry, "sessionId") && hasNullableStringField(entry, "model");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function hasLogIndexSummary(entry: LogIndexEntry): boolean {
|
|
697
|
+
return entry.summary !== undefined;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function matchesLogIndexEntryFilters(
|
|
701
|
+
entry: LogIndexEntry,
|
|
702
|
+
sessionId: string | undefined,
|
|
703
|
+
model: string | undefined,
|
|
704
|
+
): boolean {
|
|
705
|
+
if (sessionId !== undefined && entry.sessionId !== sessionId) return false;
|
|
706
|
+
if (model !== undefined && entry.model !== model) return false;
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function buildLogCursorPageIndexFromLogIndex(
|
|
711
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
712
|
+
allowRebuild: boolean,
|
|
713
|
+
): Promise<LogCursorPageIndex | null> {
|
|
714
|
+
let entries = await listIndexEntries();
|
|
715
|
+
if (entries.length === 0 && allowRebuild) {
|
|
716
|
+
await rebuildIndex();
|
|
717
|
+
entries = await listIndexEntries();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (entries.some((entry) => !hasLogIndexFilterMetadata(entry))) {
|
|
721
|
+
if (!allowRebuild) return null;
|
|
722
|
+
await rebuildIndex();
|
|
723
|
+
return await buildLogCursorPageIndexFromLogIndex(options, false);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const matchingEntries = entries.filter((entry) =>
|
|
727
|
+
matchesLogIndexEntryFilters(entry, options.sessionId, options.model),
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
if (matchingEntries.some((entry) => !hasLogIndexSummary(entry))) {
|
|
731
|
+
if (!allowRebuild) return null;
|
|
732
|
+
await rebuildIndex();
|
|
733
|
+
return await buildLogCursorPageIndexFromLogIndex(options, false);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (matchingEntries.some((entry) => !hasUsableIndexOffset(entry) && !memoryCache.has(entry.id))) {
|
|
737
|
+
if (!allowRebuild) return null;
|
|
738
|
+
await rebuildIndex();
|
|
739
|
+
return await buildLogCursorPageIndexFromLogIndex(options, false);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const idsById = new Set<number>();
|
|
743
|
+
const entriesById = new Map<number, LogIndexEntry>();
|
|
744
|
+
for (const entry of matchingEntries) {
|
|
745
|
+
idsById.add(entry.id);
|
|
746
|
+
entriesById.set(entry.id, entry);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const logsById = new Map<number, CapturedLog>();
|
|
750
|
+
for (const log of memoryCache.values()) {
|
|
751
|
+
if (!matchesLogFilters(log, options.sessionId, options.model)) continue;
|
|
752
|
+
idsById.add(log.id);
|
|
753
|
+
logsById.set(log.id, log);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
key: cursorPageIndexKey(options),
|
|
758
|
+
sessionId: options.sessionId,
|
|
759
|
+
model: options.model,
|
|
760
|
+
ids: [...idsById].sort((left, right) => left - right),
|
|
761
|
+
logsById,
|
|
762
|
+
entriesById,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function buildLogCursorPageIndex(
|
|
767
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
768
|
+
): Promise<LogCursorPageIndex> {
|
|
769
|
+
logCursorPageIndexBuildCount += 1;
|
|
770
|
+
const indexed = await buildLogCursorPageIndexFromLogIndex(options, true);
|
|
771
|
+
if (indexed !== null) return indexed;
|
|
772
|
+
|
|
773
|
+
const key = cursorPageIndexKey(options);
|
|
774
|
+
const logsById = new Map<number, CapturedLog>();
|
|
775
|
+
|
|
776
|
+
const visitLog = (log: CapturedLog): void => {
|
|
777
|
+
if (!matchesLogFilters(log, options.sessionId, options.model)) return;
|
|
778
|
+
logsById.set(log.id, log);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
await visitPersistedLogs((log) => {
|
|
782
|
+
observeSessionLog(log);
|
|
783
|
+
visitLog(log);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
for (const log of memoryCache.values()) {
|
|
787
|
+
visitLog(log);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
key,
|
|
792
|
+
sessionId: options.sessionId,
|
|
793
|
+
model: options.model,
|
|
794
|
+
ids: [...logsById.keys()].sort((left, right) => left - right),
|
|
795
|
+
logsById,
|
|
796
|
+
entriesById: new Map<number, LogIndexEntry>(),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function getLogCursorPageIndex(
|
|
801
|
+
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model">,
|
|
802
|
+
): Promise<LogCursorPageIndex> {
|
|
803
|
+
const key = cursorPageIndexKey(options);
|
|
804
|
+
const cached = logCursorPageIndexes.get(key);
|
|
805
|
+
if (cached !== undefined) {
|
|
806
|
+
logCursorPageIndexes.delete(key);
|
|
807
|
+
logCursorPageIndexes.set(key, cached);
|
|
808
|
+
return cached;
|
|
809
|
+
}
|
|
810
|
+
return rememberLogCursorPageIndex(await buildLogCursorPageIndex(options));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function hydrateCursorPageLogs(
|
|
814
|
+
index: LogCursorPageIndex,
|
|
815
|
+
pageIds: readonly number[],
|
|
816
|
+
): Promise<CapturedLog[]> {
|
|
817
|
+
const logs: CapturedLog[] = [];
|
|
818
|
+
for (const id of pageIds) {
|
|
819
|
+
const cached = index.logsById.get(id);
|
|
820
|
+
if (cached !== undefined) {
|
|
821
|
+
logs.push(cached);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const log = await getLogById(id);
|
|
826
|
+
if (log === null) continue;
|
|
827
|
+
index.logsById.set(id, log);
|
|
828
|
+
logs.push(log);
|
|
829
|
+
}
|
|
830
|
+
return logs;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function hydrateCompactCursorPageLogs(
|
|
834
|
+
index: LogCursorPageIndex,
|
|
835
|
+
pageIds: readonly number[],
|
|
836
|
+
): Promise<CapturedLog[]> {
|
|
837
|
+
const logs: CapturedLog[] = [];
|
|
838
|
+
for (const id of pageIds) {
|
|
839
|
+
const cached = index.logsById.get(id);
|
|
840
|
+
if (cached !== undefined) {
|
|
841
|
+
logs.push(compactLogForList(cached));
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const entry = index.entriesById.get(id);
|
|
846
|
+
if (entry !== undefined && entry.summary !== undefined) {
|
|
847
|
+
logs.push(compactLogFromIndexSummary(entry.summary));
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const log = await getLogById(id);
|
|
852
|
+
if (log === null) continue;
|
|
853
|
+
index.logsById.set(id, log);
|
|
854
|
+
logs.push(compactLogForList(log));
|
|
855
|
+
}
|
|
856
|
+
return logs;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function listLogsCursorPage(
|
|
860
|
+
options: ListLogsCursorPageOptions,
|
|
861
|
+
): Promise<ListLogsCursorPageResult> {
|
|
862
|
+
const includeBodies = options.includeBodies !== false;
|
|
863
|
+
const index = await getLogCursorPageIndex(options);
|
|
864
|
+
const pageIds = selectCursorPageLogs(index.ids, options);
|
|
865
|
+
const pageLogs = includeBodies
|
|
866
|
+
? await hydrateCursorPageLogs(index, pageIds)
|
|
867
|
+
: await hydrateCompactCursorPageLogs(index, pageIds);
|
|
868
|
+
const oldestLogId = pageIds[0] ?? null;
|
|
869
|
+
const newestLogId = pageIds.at(-1) ?? null;
|
|
870
|
+
const oldestIndex = oldestLogId === null ? -1 : index.ids.indexOf(oldestLogId);
|
|
871
|
+
const newestIndex = newestLogId === null ? -1 : index.ids.indexOf(newestLogId);
|
|
872
|
+
|
|
873
|
+
return {
|
|
874
|
+
logs: pageLogs.map((log) => prepareLogForList(log, includeBodies)),
|
|
875
|
+
total: index.ids.length,
|
|
876
|
+
limit: options.limit,
|
|
877
|
+
hasOlder: oldestIndex > 0,
|
|
878
|
+
hasNewer: newestIndex >= 0 && newestIndex < index.ids.length - 1,
|
|
879
|
+
oldestLogId,
|
|
880
|
+
newestLogId,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
525
884
|
function findSessionSnapshot(sessionId: string): SessionSnapshot | null {
|
|
526
885
|
for (const snapshot of getSessionSnapshots()) {
|
|
527
886
|
if (snapshot.id === sessionId) return snapshot;
|
|
@@ -539,7 +898,12 @@ async function collectSessionLogSummaries(
|
|
|
539
898
|
summariesById.set(log.id, buildSessionLogSummary(log));
|
|
540
899
|
};
|
|
541
900
|
|
|
542
|
-
|
|
901
|
+
const indexedSummaries = await collectIndexedSessionLogSummaries(sessionId);
|
|
902
|
+
for (const summary of indexedSummaries) {
|
|
903
|
+
summariesById.set(summary.id, summary);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (includeHistory && summariesById.size === 0) {
|
|
543
907
|
await visitPersistedLogs((log) => {
|
|
544
908
|
observeSessionLog(log);
|
|
545
909
|
observeMatchingLog(log);
|
|
@@ -553,6 +917,30 @@ async function collectSessionLogSummaries(
|
|
|
553
917
|
return [...summariesById.values()];
|
|
554
918
|
}
|
|
555
919
|
|
|
920
|
+
async function collectIndexedSessionLogSummaries(sessionId: string): Promise<SessionLogSummary[]> {
|
|
921
|
+
let entries = await listIndexEntries();
|
|
922
|
+
if (entries.length === 0) return [];
|
|
923
|
+
|
|
924
|
+
const needsMetadataRebuild = entries.some((entry) => !hasLogIndexFilterMetadata(entry));
|
|
925
|
+
const needsSummaryRebuild = entries.some((entry) => {
|
|
926
|
+
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined)) return false;
|
|
927
|
+
return !hasLogIndexSummary(entry);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
if (needsMetadataRebuild || needsSummaryRebuild) {
|
|
931
|
+
await rebuildIndex();
|
|
932
|
+
entries = await listIndexEntries();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const summaries: SessionLogSummary[] = [];
|
|
936
|
+
for (const entry of entries) {
|
|
937
|
+
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined)) continue;
|
|
938
|
+
if (entry.summary === undefined) continue;
|
|
939
|
+
summaries.push(buildSessionLogSummary(compactLogFromIndexSummary(entry.summary)));
|
|
940
|
+
}
|
|
941
|
+
return summaries;
|
|
942
|
+
}
|
|
943
|
+
|
|
556
944
|
async function includeSnapshotLastLogSummary(
|
|
557
945
|
sessionId: string,
|
|
558
946
|
snapshot: SessionSnapshot | null,
|
|
@@ -678,6 +1066,7 @@ export async function loadLogsIntoMemory(): Promise<void> {
|
|
|
678
1066
|
}
|
|
679
1067
|
|
|
680
1068
|
rebuildSessionRegistry(memoryCache.values());
|
|
1069
|
+
clearLogCursorPageIndexes();
|
|
681
1070
|
}
|
|
682
1071
|
|
|
683
1072
|
function rememberRecentCompletedLog(
|
|
@@ -706,6 +1095,7 @@ function findSmallestLogId(logsById: ReadonlyMap<number, CapturedLog>): number |
|
|
|
706
1095
|
export function clearAllLogs(): { cleared: number } {
|
|
707
1096
|
const count = memoryCache.size;
|
|
708
1097
|
memoryCache.clear();
|
|
1098
|
+
clearLogCursorPageIndexes();
|
|
709
1099
|
clearSessionRegistry();
|
|
710
1100
|
return { cleared: count };
|
|
711
1101
|
}
|
|
@@ -967,6 +1357,7 @@ export async function clearPersistedLogsByIds(
|
|
|
967
1357
|
]);
|
|
968
1358
|
|
|
969
1359
|
await rebuildIndex();
|
|
1360
|
+
clearLogCursorPageIndexes();
|
|
970
1361
|
|
|
971
1362
|
const clearedIds = new Set<number>(rewriteResult.removedIds);
|
|
972
1363
|
if (result.cleared > 0) {
|
|
@@ -996,10 +1387,21 @@ export function clearLogsByIds(ids: readonly number[]): { cleared: number } {
|
|
|
996
1387
|
for (const id of unique) {
|
|
997
1388
|
removeFromCache(id);
|
|
998
1389
|
}
|
|
1390
|
+
removeLogsFromCursorPageIndexes(unique);
|
|
999
1391
|
rebuildSessionRegistry(memoryCache.values());
|
|
1000
1392
|
return { cleared: unique.size };
|
|
1001
1393
|
}
|
|
1002
1394
|
|
|
1395
|
+
export function _getLogCursorPageIndexStatsForTests(): {
|
|
1396
|
+
indexCount: number;
|
|
1397
|
+
buildCount: number;
|
|
1398
|
+
} {
|
|
1399
|
+
return {
|
|
1400
|
+
indexCount: logCursorPageIndexes.size,
|
|
1401
|
+
buildCount: logCursorPageIndexBuildCount,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1003
1405
|
// SSE event system for real-time log updates
|
|
1004
1406
|
type LogUpdateHandler = (log: CapturedLog) => void;
|
|
1005
1407
|
const sseHandlers: Set<LogUpdateHandler> = new Set();
|
package/src/routes/api/logs.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
clearPersistedLogsByIds,
|
|
6
6
|
getLogStorageStats,
|
|
7
7
|
getReplayLogsForSource,
|
|
8
|
+
listLogsCursorPage,
|
|
8
9
|
listLogsPage,
|
|
9
10
|
} from "../../proxy/store";
|
|
10
11
|
import {
|
|
@@ -33,6 +34,15 @@ function parsePositiveInt(value: string | null, fallback: number): number {
|
|
|
33
34
|
return parsed;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
type OptionalPositiveIntResult = { ok: true; value: number | null } | { ok: false };
|
|
38
|
+
|
|
39
|
+
function parseOptionalPositiveInt(value: string | null): OptionalPositiveIntResult {
|
|
40
|
+
if (value === null) return { ok: true, value: null };
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed < 1) return { ok: false };
|
|
43
|
+
return { ok: true, value: parsed };
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export const Route = createFileRoute("/api/logs")({
|
|
37
47
|
server: {
|
|
38
48
|
handlers: {
|
|
@@ -67,6 +77,32 @@ export const Route = createFileRoute("/api/logs")({
|
|
|
67
77
|
const limit = parsePositiveInt(url.searchParams.get("limit"), 50);
|
|
68
78
|
const includeBodies = url.searchParams.get("compact") !== "1";
|
|
69
79
|
|
|
80
|
+
if (url.searchParams.get("cursor") === "1" || url.searchParams.get("cursor") === "true") {
|
|
81
|
+
const beforeLogId = parseOptionalPositiveInt(url.searchParams.get("beforeLogId"));
|
|
82
|
+
const afterLogId = parseOptionalPositiveInt(url.searchParams.get("afterLogId"));
|
|
83
|
+
if (!beforeLogId.ok || !afterLogId.ok) {
|
|
84
|
+
return Response.json({ error: "Invalid cursor log ID" }, { status: 400 });
|
|
85
|
+
}
|
|
86
|
+
if (beforeLogId.value !== null && afterLogId.value !== null) {
|
|
87
|
+
return Response.json(
|
|
88
|
+
{ error: "Use beforeLogId or afterLogId, not both" },
|
|
89
|
+
{ status: 400 },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const anchor = url.searchParams.get("anchor") === "oldest" ? "oldest" : "newest";
|
|
93
|
+
return Response.json(
|
|
94
|
+
await listLogsCursorPage({
|
|
95
|
+
sessionId,
|
|
96
|
+
model,
|
|
97
|
+
limit,
|
|
98
|
+
includeBodies,
|
|
99
|
+
beforeLogId: beforeLogId.value ?? undefined,
|
|
100
|
+
afterLogId: afterLogId.value ?? undefined,
|
|
101
|
+
anchor,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
70
106
|
return Response.json(
|
|
71
107
|
await listLogsPage({ sessionId, model, offset, limit, includeBodies }),
|
|
72
108
|
);
|