@tonyclaw/agent-inspector 3.1.20 → 3.1.22

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 (57) hide show
  1. package/.output/backend/nitro.json +1 -1
  2. package/.output/cli.js +137 -20
  3. package/.output/server/_ssr/index.mjs +2 -2
  4. package/.output/server/_ssr/{router-DpXchCXm.mjs → router-DKFaWvAr.mjs} +2371 -1171
  5. package/.output/server/_tanstack-start-manifest_v-ClTaMkEj.mjs +4 -0
  6. package/.output/server/index.mjs +1 -1
  7. package/.output/ui/assets/{CompareDrawer-4i1QHaQf.js → CompareDrawer-pPcsyvc6.js} +1 -1
  8. package/.output/ui/assets/{InspectorPet-Cw6VJLr7.js → InspectorPet-DR3JEqze.js} +1 -1
  9. package/.output/ui/assets/ProxyViewerContainer-DOyYRnb-.js +60 -0
  10. package/.output/ui/assets/{ReplayDialog-DTs575iW.js → ReplayDialog-Z-1-gUDm.js} +1 -1
  11. package/.output/ui/assets/{RequestAnatomy-Cl6Es3gv.js → RequestAnatomy-BsnZjE5G.js} +1 -1
  12. package/.output/ui/assets/{ResponseView-DNCCLkzG.js → ResponseView-HY64_ZY2.js} +1 -1
  13. package/.output/ui/assets/{StreamingChunkSequence-DKaxRL9U.js → StreamingChunkSequence-CfQUFbTA.js} +1 -1
  14. package/.output/ui/assets/{_sessionId-rkR3WgRq.js → _sessionId-Dq97lD86.js} +1 -1
  15. package/.output/ui/assets/{_sessionId-C-YjWz50.js → _sessionId-_F9wwa3a.js} +1 -1
  16. package/.output/ui/assets/index-B-Qod-aA.css +1 -0
  17. package/.output/ui/assets/{index-DxiecWHw.js → index-B2yLwOEu.js} +1 -1
  18. package/.output/ui/assets/{index-D0d-V67x.js → index-CN-gZDqV.js} +1 -1
  19. package/.output/ui/assets/{index-DYbzZTDM.js → index-DOAGzrq2.js} +1 -1
  20. package/.output/ui/assets/{index-B48RD1-5.js → index-DYnqmBBx.js} +2 -2
  21. package/.output/ui/assets/{json-viewer-CjAwZhHA.js → json-viewer-B2SQN0Lg.js} +1 -1
  22. package/.output/ui/assets/{jszip.min-DEGEN1HP.js → jszip.min-Dpks67u2.js} +1 -1
  23. package/.output/ui/index.html +2 -2
  24. package/.output/workers/logFinalizer.worker.js +258 -0
  25. package/.output/workers/sessionWorkerEntry.js +258 -0
  26. package/package.json +13 -6
  27. package/src/backend/routes/api/logs.$id.replay.ts +6 -1
  28. package/src/backend/routes/api/logs.stream.ts +7 -0
  29. package/src/backend/routes/api/providers.$providerId.model-metadata.ts +15 -4
  30. package/src/backend/routes/api/storage.ts +56 -0
  31. package/src/backend/routes/metrics.ts +14 -0
  32. package/src/cli/doctor.ts +135 -2
  33. package/src/cli.ts +7 -4
  34. package/src/components/ProxyViewerContainer.tsx +41 -24
  35. package/src/components/providers/SettingsDialog.tsx +261 -70
  36. package/src/components/proxy-viewer/LogEntry.tsx +19 -5
  37. package/src/components/proxy-viewer/bodyHydration.ts +67 -0
  38. package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +78 -0
  39. package/src/knowledge/openclawClient.ts +11 -2
  40. package/src/knowledge/openclawGatewayClient.ts +6 -1
  41. package/src/lib/resourceLimits.ts +191 -0
  42. package/src/lib/safeFetch.ts +428 -20
  43. package/src/proxy/ecosystemTasks.ts +20 -0
  44. package/src/proxy/handler.ts +17 -4
  45. package/src/proxy/identityProxy.ts +36 -16
  46. package/src/proxy/logIndex.ts +38 -6
  47. package/src/proxy/logger.ts +4 -0
  48. package/src/proxy/rawStreamCapture.ts +16 -1
  49. package/src/proxy/runtimeHealth.ts +4 -2
  50. package/src/proxy/runtimeMetrics.ts +149 -0
  51. package/src/proxy/sessionRuntime.ts +38 -0
  52. package/src/proxy/sqliteLogIndex.ts +51 -0
  53. package/src/proxy/storageLifecycle.ts +432 -0
  54. package/src/proxy/store.ts +62 -12
  55. package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +0 -4
  56. package/.output/ui/assets/ProxyViewerContainer-BST0B7lY.js +0 -60
  57. package/.output/ui/assets/index-IBEDJoV_.css +0 -1
@@ -0,0 +1,432 @@
1
+ import { existsSync, lstatSync, readdirSync, unlinkSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import { getChunksDir } from "./chunkStorage";
4
+ import { getDataDir } from "./dataDir";
5
+ import { resolveApplicationLogPath, resolveLogDir } from "./logger";
6
+ import { getRawStreamDir } from "./rawStreamCapture";
7
+ import { getSessionArchiveRoot } from "./sessionArchive";
8
+
9
+ export type StorageArtifactKind =
10
+ | "captured-log-jsonl"
11
+ | "sqlite-log-index"
12
+ | "session-archive"
13
+ | "streaming-chunk"
14
+ | "raw-stream-temp"
15
+ | "application-log"
16
+ | "evidence"
17
+ | "backup";
18
+
19
+ export type StorageArtifactSensitivity = "captured-data" | "secret-adjacent" | "operational";
20
+
21
+ export type StorageArtifactCleanupBehavior =
22
+ | "retention-gc"
23
+ | "orphan-reclaim"
24
+ | "rotation"
25
+ | "manual-owner-delete";
26
+
27
+ export type StorageArtifactInventoryRule = {
28
+ kind: StorageArtifactKind;
29
+ root: string;
30
+ sensitivity: StorageArtifactSensitivity;
31
+ cleanupBehavior: StorageArtifactCleanupBehavior;
32
+ shouldCount: (fileName: string, path: string) => boolean;
33
+ };
34
+
35
+ export type StorageArtifactSummary = {
36
+ kind: StorageArtifactKind;
37
+ root: string;
38
+ sensitivity: StorageArtifactSensitivity;
39
+ cleanupBehavior: StorageArtifactCleanupBehavior;
40
+ fileCount: number;
41
+ bytes: number;
42
+ oldestModifiedAt: string | null;
43
+ newestModifiedAt: string | null;
44
+ };
45
+
46
+ export type StorageLifecycleInventory = {
47
+ checkedAt: string;
48
+ totalFiles: number;
49
+ totalBytes: number;
50
+ artifacts: StorageArtifactSummary[];
51
+ };
52
+
53
+ export type StorageLifecycleQuotaPolicy = {
54
+ maxAgeMs: number;
55
+ maxTotalBytes: number;
56
+ };
57
+
58
+ export type StorageLifecycleGcError = {
59
+ kind: StorageArtifactKind;
60
+ path: string;
61
+ message: string;
62
+ };
63
+
64
+ export type StorageLifecycleGcResult = {
65
+ checkedAt: string;
66
+ policy: StorageLifecycleQuotaPolicy;
67
+ before: StorageLifecycleInventory;
68
+ after: StorageLifecycleInventory;
69
+ filesScanned: number;
70
+ bytesScanned: number;
71
+ filesDeleted: number;
72
+ bytesDeleted: number;
73
+ reclaimableBytes: number;
74
+ errors: StorageLifecycleGcError[];
75
+ };
76
+
77
+ type MutableDirectoryStats = {
78
+ fileCount: number;
79
+ bytes: number;
80
+ oldestModifiedMs: number | null;
81
+ newestModifiedMs: number | null;
82
+ };
83
+
84
+ type StorageArtifactFile = {
85
+ rule: StorageArtifactInventoryRule;
86
+ path: string;
87
+ bytes: number;
88
+ modifiedMs: number;
89
+ };
90
+
91
+ const MAX_INVENTORY_DEPTH = 8;
92
+ const DAY_MS = 24 * 60 * 60 * 1000;
93
+ const DEFAULT_RETENTION_DAYS = 7;
94
+ const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
95
+ const DEFAULT_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000;
96
+
97
+ let maintenanceTimer: ReturnType<typeof setInterval> | null = null;
98
+ let lastStorageLifecycleGcResult: StorageLifecycleGcResult | null = null;
99
+
100
+ function emptyStats(): MutableDirectoryStats {
101
+ return {
102
+ fileCount: 0,
103
+ bytes: 0,
104
+ oldestModifiedMs: null,
105
+ newestModifiedMs: null,
106
+ };
107
+ }
108
+
109
+ function recordFile(stats: MutableDirectoryStats, bytes: number, modifiedMs: number): void {
110
+ stats.fileCount += 1;
111
+ stats.bytes += bytes;
112
+ stats.oldestModifiedMs =
113
+ stats.oldestModifiedMs === null ? modifiedMs : Math.min(stats.oldestModifiedMs, modifiedMs);
114
+ stats.newestModifiedMs =
115
+ stats.newestModifiedMs === null ? modifiedMs : Math.max(stats.newestModifiedMs, modifiedMs);
116
+ }
117
+
118
+ function isoFromMs(value: number | null): string | null {
119
+ return value === null ? null : new Date(value).toISOString();
120
+ }
121
+
122
+ function isPathInsideRoot(root: string, path: string): boolean {
123
+ const relativePath = relative(resolve(root), resolve(path));
124
+ return (
125
+ relativePath.length === 0 ||
126
+ (!relativePath.startsWith("..") &&
127
+ !relativePath.startsWith("/") &&
128
+ !relativePath.startsWith("\\"))
129
+ );
130
+ }
131
+
132
+ function collectRuleFiles(rule: StorageArtifactInventoryRule): StorageArtifactFile[] {
133
+ const files: StorageArtifactFile[] = [];
134
+ if (!existsSync(rule.root)) return files;
135
+
136
+ const visit = (currentDir: string, depth: number): void => {
137
+ if (depth > MAX_INVENTORY_DEPTH) return;
138
+ let entries: string[];
139
+ try {
140
+ entries = readdirSync(currentDir);
141
+ } catch {
142
+ return;
143
+ }
144
+
145
+ for (const entry of entries) {
146
+ const path = join(currentDir, entry);
147
+ if (!isPathInsideRoot(rule.root, path)) continue;
148
+ try {
149
+ const fileStats = lstatSync(path);
150
+ if (fileStats.isDirectory()) {
151
+ visit(path, depth + 1);
152
+ continue;
153
+ }
154
+ if (!fileStats.isFile() || !rule.shouldCount(entry, path)) continue;
155
+ files.push({ rule, path, bytes: fileStats.size, modifiedMs: fileStats.mtimeMs });
156
+ } catch {
157
+ // Files may disappear while inventory is collected; keep inventory best-effort and safe.
158
+ }
159
+ }
160
+ };
161
+
162
+ visit(rule.root, 0);
163
+ return files;
164
+ }
165
+
166
+ function collectRuleStats(rule: StorageArtifactInventoryRule): MutableDirectoryStats {
167
+ const stats = emptyStats();
168
+ for (const file of collectRuleFiles(rule)) {
169
+ recordFile(stats, file.bytes, file.modifiedMs);
170
+ }
171
+ return stats;
172
+ }
173
+
174
+ function isAutoDeletableCleanupBehavior(behavior: StorageArtifactCleanupBehavior): boolean {
175
+ switch (behavior) {
176
+ case "retention-gc":
177
+ case "orphan-reclaim":
178
+ return true;
179
+ case "rotation":
180
+ case "manual-owner-delete":
181
+ return false;
182
+ }
183
+ }
184
+
185
+ function summarizeRule(rule: StorageArtifactInventoryRule): StorageArtifactSummary {
186
+ const stats = collectRuleStats(rule);
187
+ return {
188
+ kind: rule.kind,
189
+ root: rule.root,
190
+ sensitivity: rule.sensitivity,
191
+ cleanupBehavior: rule.cleanupBehavior,
192
+ fileCount: stats.fileCount,
193
+ bytes: stats.bytes,
194
+ oldestModifiedAt: isoFromMs(stats.oldestModifiedMs),
195
+ newestModifiedAt: isoFromMs(stats.newestModifiedMs),
196
+ };
197
+ }
198
+
199
+ function isSessionArchiveFile(fileName: string): boolean {
200
+ return fileName === "session.sqlite" || fileName.startsWith("session.sqlite-");
201
+ }
202
+
203
+ function isSqliteLogIndexFile(fileName: string): boolean {
204
+ return fileName === "inspector.sqlite" || fileName.startsWith("inspector.sqlite-");
205
+ }
206
+
207
+ function isBackupFile(fileName: string): boolean {
208
+ return fileName.endsWith(".bak") || fileName.endsWith(".backup");
209
+ }
210
+
211
+ function applicationLogMatcher(logPath: string): (fileName: string, path: string) => boolean {
212
+ const directory = dirname(logPath);
213
+ const baseName = basename(logPath);
214
+ return (fileName, path) => {
215
+ if (dirname(path) !== directory) return false;
216
+ if (fileName === baseName) return true;
217
+ return fileName.startsWith(`${baseName}.`) && /^\d+$/.test(fileName.slice(baseName.length + 1));
218
+ };
219
+ }
220
+
221
+ function parsePositiveIntegerEnv(name: string, fallback: number): number {
222
+ const value = process.env[name];
223
+ if (value === undefined) return fallback;
224
+ const parsed = Number.parseInt(value, 10);
225
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
226
+ return parsed;
227
+ }
228
+
229
+ export function resolveStorageLifecycleQuotaPolicy(): StorageLifecycleQuotaPolicy {
230
+ const retentionDays = parsePositiveIntegerEnv(
231
+ "AGENT_INSPECTOR_STORAGE_RETENTION_DAYS",
232
+ parsePositiveIntegerEnv("LOG_RETENTION_DAYS", DEFAULT_RETENTION_DAYS),
233
+ );
234
+ return {
235
+ maxAgeMs: retentionDays * DAY_MS,
236
+ maxTotalBytes: parsePositiveIntegerEnv(
237
+ "AGENT_INSPECTOR_STORAGE_MAX_BYTES",
238
+ DEFAULT_MAX_TOTAL_BYTES,
239
+ ),
240
+ };
241
+ }
242
+
243
+ export function getStorageLifecycleInventoryRules(): StorageArtifactInventoryRule[] {
244
+ const applicationLogPath = resolveApplicationLogPath();
245
+ return [
246
+ {
247
+ kind: "captured-log-jsonl",
248
+ root: resolveLogDir(),
249
+ sensitivity: "captured-data",
250
+ cleanupBehavior: "retention-gc",
251
+ shouldCount: (fileName) => fileName.endsWith(".jsonl"),
252
+ },
253
+ {
254
+ kind: "sqlite-log-index",
255
+ root: resolveLogDir(),
256
+ sensitivity: "captured-data",
257
+ cleanupBehavior: "orphan-reclaim",
258
+ shouldCount: isSqliteLogIndexFile,
259
+ },
260
+ {
261
+ kind: "session-archive",
262
+ root: getSessionArchiveRoot(),
263
+ sensitivity: "captured-data",
264
+ cleanupBehavior: "retention-gc",
265
+ shouldCount: isSessionArchiveFile,
266
+ },
267
+ {
268
+ kind: "streaming-chunk",
269
+ root: getChunksDir(),
270
+ sensitivity: "captured-data",
271
+ cleanupBehavior: "retention-gc",
272
+ shouldCount: (fileName) => fileName.endsWith(".json"),
273
+ },
274
+ {
275
+ kind: "raw-stream-temp",
276
+ root: getRawStreamDir(),
277
+ sensitivity: "captured-data",
278
+ cleanupBehavior: "orphan-reclaim",
279
+ shouldCount: () => true,
280
+ },
281
+ {
282
+ kind: "application-log",
283
+ root: dirname(applicationLogPath),
284
+ sensitivity: "operational",
285
+ cleanupBehavior: "rotation",
286
+ shouldCount: applicationLogMatcher(applicationLogPath),
287
+ },
288
+ {
289
+ kind: "evidence",
290
+ root: join(getDataDir(), "evidence"),
291
+ sensitivity: "captured-data",
292
+ cleanupBehavior: "manual-owner-delete",
293
+ shouldCount: () => true,
294
+ },
295
+ {
296
+ kind: "backup",
297
+ root: getDataDir(),
298
+ sensitivity: "secret-adjacent",
299
+ cleanupBehavior: "orphan-reclaim",
300
+ shouldCount: isBackupFile,
301
+ },
302
+ ];
303
+ }
304
+
305
+ export function getStorageLifecycleInventory(
306
+ rules: readonly StorageArtifactInventoryRule[] = getStorageLifecycleInventoryRules(),
307
+ ): StorageLifecycleInventory {
308
+ const artifacts = rules.map(summarizeRule);
309
+ return {
310
+ checkedAt: new Date().toISOString(),
311
+ totalFiles: artifacts.reduce((sum, artifact) => sum + artifact.fileCount, 0),
312
+ totalBytes: artifacts.reduce((sum, artifact) => sum + artifact.bytes, 0),
313
+ artifacts,
314
+ };
315
+ }
316
+
317
+ function collectLifecycleFiles(
318
+ rules: readonly StorageArtifactInventoryRule[] = getStorageLifecycleInventoryRules(),
319
+ ): StorageArtifactFile[] {
320
+ return rules.flatMap((rule) => collectRuleFiles(rule));
321
+ }
322
+
323
+ function selectGcCandidates(
324
+ files: readonly StorageArtifactFile[],
325
+ policy: StorageLifecycleQuotaPolicy,
326
+ nowMs: number,
327
+ ): StorageArtifactFile[] {
328
+ const autoDeletableFiles = files.filter((file) =>
329
+ isAutoDeletableCleanupBehavior(file.rule.cleanupBehavior),
330
+ );
331
+ const expiredFiles = autoDeletableFiles.filter(
332
+ (file) => nowMs - file.modifiedMs > policy.maxAgeMs,
333
+ );
334
+ const expiredPaths = new Set(expiredFiles.map((file) => file.path));
335
+ const candidates = [...expiredFiles];
336
+ let totalBytes =
337
+ files.reduce((sum, file) => sum + file.bytes, 0) -
338
+ expiredFiles.reduce((sum, file) => sum + file.bytes, 0);
339
+
340
+ if (totalBytes <= policy.maxTotalBytes) return candidates;
341
+
342
+ const quotaCandidates = autoDeletableFiles
343
+ .filter((file) => !expiredPaths.has(file.path))
344
+ .sort((a, b) => a.modifiedMs - b.modifiedMs);
345
+
346
+ for (const file of quotaCandidates) {
347
+ if (totalBytes <= policy.maxTotalBytes) break;
348
+ candidates.push(file);
349
+ totalBytes -= file.bytes;
350
+ }
351
+
352
+ return candidates;
353
+ }
354
+
355
+ function deleteLifecycleFile(file: StorageArtifactFile): StorageLifecycleGcError | null {
356
+ if (!isPathInsideRoot(file.rule.root, file.path)) {
357
+ return { kind: file.rule.kind, path: file.path, message: "path outside lifecycle root" };
358
+ }
359
+ try {
360
+ unlinkSync(file.path);
361
+ return null;
362
+ } catch (err) {
363
+ if (err instanceof Error && err.message.includes("ENOENT")) return null;
364
+ return { kind: file.rule.kind, path: file.path, message: String(err) };
365
+ }
366
+ }
367
+
368
+ export function runStorageLifecycleGc(
369
+ policy: StorageLifecycleQuotaPolicy = resolveStorageLifecycleQuotaPolicy(),
370
+ rules: readonly StorageArtifactInventoryRule[] = getStorageLifecycleInventoryRules(),
371
+ nowMs: number = Date.now(),
372
+ ): StorageLifecycleGcResult {
373
+ const before = getStorageLifecycleInventory(rules);
374
+ const files = collectLifecycleFiles(rules);
375
+ const candidates = selectGcCandidates(files, policy, nowMs);
376
+ const errors: StorageLifecycleGcError[] = [];
377
+ let filesDeleted = 0;
378
+ let bytesDeleted = 0;
379
+
380
+ for (const file of candidates) {
381
+ const error = deleteLifecycleFile(file);
382
+ if (error !== null) {
383
+ errors.push(error);
384
+ continue;
385
+ }
386
+ filesDeleted += 1;
387
+ bytesDeleted += file.bytes;
388
+ }
389
+
390
+ const after = getStorageLifecycleInventory(rules);
391
+ const autoDeletableBytes = after.artifacts
392
+ .filter((artifact) => isAutoDeletableCleanupBehavior(artifact.cleanupBehavior))
393
+ .reduce((sum, artifact) => sum + artifact.bytes, 0);
394
+
395
+ const result = {
396
+ checkedAt: new Date(nowMs).toISOString(),
397
+ policy,
398
+ before,
399
+ after,
400
+ filesScanned: files.length,
401
+ bytesScanned: files.reduce((sum, file) => sum + file.bytes, 0),
402
+ filesDeleted,
403
+ bytesDeleted,
404
+ reclaimableBytes: autoDeletableBytes,
405
+ errors,
406
+ };
407
+ lastStorageLifecycleGcResult = result;
408
+ return result;
409
+ }
410
+
411
+ export function getLastStorageLifecycleGcResult(): StorageLifecycleGcResult | null {
412
+ return lastStorageLifecycleGcResult;
413
+ }
414
+
415
+ export function installStorageLifecycleMaintenance(): void {
416
+ if (maintenanceTimer !== null) return;
417
+ if (process.env["NODE_ENV"] === "test") return;
418
+ const intervalMs = parsePositiveIntegerEnv(
419
+ "AGENT_INSPECTOR_STORAGE_GC_INTERVAL_MS",
420
+ DEFAULT_MAINTENANCE_INTERVAL_MS,
421
+ );
422
+ maintenanceTimer = setInterval(() => {
423
+ runStorageLifecycleGc();
424
+ }, intervalMs);
425
+ maintenanceTimer.unref();
426
+ }
427
+
428
+ export function stopStorageLifecycleMaintenanceForTests(): void {
429
+ if (maintenanceTimer === null) return;
430
+ clearInterval(maintenanceTimer);
431
+ maintenanceTimer = null;
432
+ }
@@ -34,7 +34,7 @@ import {
34
34
  type LogIndexSummary,
35
35
  } from "./logIndex";
36
36
  import { writeChunks } from "./chunkStorage";
37
- import { closeSqliteLogIndex } from "./sqliteLogIndex";
37
+ import { closeSqliteLogIndex, listSqliteLogCursorEntries } from "./sqliteLogIndex";
38
38
  import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
39
39
  import { extractToolTraceEvents } from "../lib/toolTrace";
40
40
  import { extractStopReason } from "../lib/stopReason";
@@ -72,6 +72,7 @@ import {
72
72
  type SessionClientInfo,
73
73
  type SessionSnapshot,
74
74
  } from "./sessionSupervisor";
75
+ import { installStorageLifecycleMaintenance, runStorageLifecycleGc } from "./storageLifecycle";
75
76
 
76
77
  export type { CapturedLog };
77
78
  export { getNextLogId };
@@ -869,21 +870,50 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
869
870
  };
870
871
  }
871
872
 
872
- function matchesCursorWindow(id: number, options: ListLogsCursorPageOptions): boolean {
873
- if (options.beforeLogId !== undefined && id >= options.beforeLogId) return false;
874
- if (options.afterLogId !== undefined && id <= options.afterLogId) return false;
875
- return true;
873
+ function lowerBoundLogId(ids: readonly number[], target: number): number {
874
+ let low = 0;
875
+ let high = ids.length;
876
+ while (low < high) {
877
+ const middle = Math.floor((low + high) / 2);
878
+ const id = ids[middle];
879
+ if (id === undefined || id >= target) {
880
+ high = middle;
881
+ continue;
882
+ }
883
+ low = middle + 1;
884
+ }
885
+ return low;
886
+ }
887
+
888
+ function upperBoundLogId(ids: readonly number[], target: number): number {
889
+ let low = 0;
890
+ let high = ids.length;
891
+ while (low < high) {
892
+ const middle = Math.floor((low + high) / 2);
893
+ const id = ids[middle];
894
+ if (id === undefined || id > target) {
895
+ high = middle;
896
+ continue;
897
+ }
898
+ low = middle + 1;
899
+ }
900
+ return low;
876
901
  }
877
902
 
878
903
  function selectCursorPageLogs(
879
904
  ids: readonly number[],
880
905
  options: ListLogsCursorPageOptions,
881
906
  ): number[] {
882
- const matchingWindow = ids.filter((id) => matchesCursorWindow(id, options));
907
+ const windowStart =
908
+ options.afterLogId === undefined ? 0 : upperBoundLogId(ids, options.afterLogId);
909
+ const windowEnd =
910
+ options.beforeLogId === undefined ? ids.length : lowerBoundLogId(ids, options.beforeLogId);
911
+ if (windowStart >= windowEnd) return [];
912
+
883
913
  if (options.afterLogId !== undefined || options.anchor === "oldest") {
884
- return matchingWindow.slice(0, options.limit);
914
+ return ids.slice(windowStart, Math.min(windowEnd, windowStart + options.limit));
885
915
  }
886
- return matchingWindow.slice(Math.max(0, matchingWindow.length - options.limit));
916
+ return ids.slice(Math.max(windowStart, windowEnd - options.limit), windowEnd);
887
917
  }
888
918
 
889
919
  function rememberLogCursorPageIndex(index: LogCursorPageIndex): LogCursorPageIndex {
@@ -929,12 +959,18 @@ async function buildLogCursorPageIndexFromLogIndex(
929
959
  options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
930
960
  allowRebuild: boolean,
931
961
  ): Promise<LogCursorPageIndex | null> {
932
- if (options.clientPid !== undefined) return null;
933
-
934
- let entries = await listFilteredIndexEntries(options);
962
+ let entries = await listSqliteLogCursorEntries(options);
963
+ if (entries === null) {
964
+ if (options.clientPid !== undefined) return null;
965
+ entries = await listFilteredIndexEntries(options);
966
+ }
935
967
  if (entries.length === 0 && allowRebuild) {
936
968
  await rebuildIndex();
937
- entries = await listFilteredIndexEntries(options);
969
+ entries = await listSqliteLogCursorEntries(options);
970
+ if (entries === null) {
971
+ if (options.clientPid !== undefined) return null;
972
+ entries = await listFilteredIndexEntries(options);
973
+ }
938
974
  }
939
975
 
940
976
  if (entries.some((entry) => !hasLogIndexFilterMetadata(entry))) {
@@ -1314,6 +1350,20 @@ export function getLogStorageStats(): LogStorageStats {
1314
1350
  * then only retain the newest MAX_MEMORY_CACHE entries in cache.
1315
1351
  */
1316
1352
  export async function loadLogsIntoMemory(): Promise<void> {
1353
+ try {
1354
+ const gcResult = runStorageLifecycleGc();
1355
+ if (gcResult.filesDeleted > 0 || gcResult.errors.length > 0) {
1356
+ logger.info(
1357
+ "[store] Storage lifecycle GC completed:",
1358
+ `${gcResult.filesDeleted} files deleted, ${gcResult.bytesDeleted} bytes reclaimed,`,
1359
+ `${gcResult.errors.length} errors`,
1360
+ );
1361
+ }
1362
+ } catch (err) {
1363
+ logger.warn("[store] Storage lifecycle GC failed:", String(err));
1364
+ }
1365
+ installStorageLifecycleMaintenance();
1366
+
1317
1367
  const logDir = resolveLogDir();
1318
1368
  if (!existsSync(logDir)) return;
1319
1369
 
@@ -1,4 +0,0 @@
1
- const tsrStartManifest = () => ({ routes: { __root__: { filePath: "C:/Users/claw/workspace/agent-inspector/src/backend/routes/__root.ts", children: ["/$", "/livez", "/readyz", "/api/alerts", "/api/config", "/api/groups", "/api/health", "/api/instances", "/api/logs", "/api/mcp", "/api/models", "/api/pi-agent", "/api/providers", "/api/runs", "/api/runtime", "/api/sessions", "/proxy/$", "/api/ecosystem/packages", "/api/ecosystem/tasks", "/api/knowledge/candidates", "/api/knowledge/project-context", "/api/knowledge/search", "/api/ecosystem/recipes/$recipeId/run", "/api/knowledge/sessions/$sessionId/candidates"], preloads: ["/assets/index-D4LX-sw-.js"], scripts: [{ attrs: { type: "module", async: true, src: "/assets/index-D4LX-sw-.js" } }] } } });
2
- export {
3
- tsrStartManifest
4
- };