@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.
- package/.output/backend/nitro.json +1 -1
- package/.output/cli.js +137 -20
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DpXchCXm.mjs → router-DKFaWvAr.mjs} +2371 -1171
- package/.output/server/_tanstack-start-manifest_v-ClTaMkEj.mjs +4 -0
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-4i1QHaQf.js → CompareDrawer-pPcsyvc6.js} +1 -1
- package/.output/ui/assets/{InspectorPet-Cw6VJLr7.js → InspectorPet-DR3JEqze.js} +1 -1
- package/.output/ui/assets/ProxyViewerContainer-DOyYRnb-.js +60 -0
- package/.output/ui/assets/{ReplayDialog-DTs575iW.js → ReplayDialog-Z-1-gUDm.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-Cl6Es3gv.js → RequestAnatomy-BsnZjE5G.js} +1 -1
- package/.output/ui/assets/{ResponseView-DNCCLkzG.js → ResponseView-HY64_ZY2.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-DKaxRL9U.js → StreamingChunkSequence-CfQUFbTA.js} +1 -1
- package/.output/ui/assets/{_sessionId-rkR3WgRq.js → _sessionId-Dq97lD86.js} +1 -1
- package/.output/ui/assets/{_sessionId-C-YjWz50.js → _sessionId-_F9wwa3a.js} +1 -1
- package/.output/ui/assets/index-B-Qod-aA.css +1 -0
- package/.output/ui/assets/{index-DxiecWHw.js → index-B2yLwOEu.js} +1 -1
- package/.output/ui/assets/{index-D0d-V67x.js → index-CN-gZDqV.js} +1 -1
- package/.output/ui/assets/{index-DYbzZTDM.js → index-DOAGzrq2.js} +1 -1
- package/.output/ui/assets/{index-B48RD1-5.js → index-DYnqmBBx.js} +2 -2
- package/.output/ui/assets/{json-viewer-CjAwZhHA.js → json-viewer-B2SQN0Lg.js} +1 -1
- package/.output/ui/assets/{jszip.min-DEGEN1HP.js → jszip.min-Dpks67u2.js} +1 -1
- package/.output/ui/index.html +2 -2
- package/.output/workers/logFinalizer.worker.js +258 -0
- package/.output/workers/sessionWorkerEntry.js +258 -0
- package/package.json +13 -6
- package/src/backend/routes/api/logs.$id.replay.ts +6 -1
- package/src/backend/routes/api/logs.stream.ts +7 -0
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +15 -4
- package/src/backend/routes/api/storage.ts +56 -0
- package/src/backend/routes/metrics.ts +14 -0
- package/src/cli/doctor.ts +135 -2
- package/src/cli.ts +7 -4
- package/src/components/ProxyViewerContainer.tsx +41 -24
- package/src/components/providers/SettingsDialog.tsx +261 -70
- package/src/components/proxy-viewer/LogEntry.tsx +19 -5
- package/src/components/proxy-viewer/bodyHydration.ts +67 -0
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +78 -0
- package/src/knowledge/openclawClient.ts +11 -2
- package/src/knowledge/openclawGatewayClient.ts +6 -1
- package/src/lib/resourceLimits.ts +191 -0
- package/src/lib/safeFetch.ts +428 -20
- package/src/proxy/ecosystemTasks.ts +20 -0
- package/src/proxy/handler.ts +17 -4
- package/src/proxy/identityProxy.ts +36 -16
- package/src/proxy/logIndex.ts +38 -6
- package/src/proxy/logger.ts +4 -0
- package/src/proxy/rawStreamCapture.ts +16 -1
- package/src/proxy/runtimeHealth.ts +4 -2
- package/src/proxy/runtimeMetrics.ts +149 -0
- package/src/proxy/sessionRuntime.ts +38 -0
- package/src/proxy/sqliteLogIndex.ts +51 -0
- package/src/proxy/storageLifecycle.ts +432 -0
- package/src/proxy/store.ts +62 -12
- package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +0 -4
- package/.output/ui/assets/ProxyViewerContainer-BST0B7lY.js +0 -60
- 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
|
+
}
|
package/src/proxy/store.ts
CHANGED
|
@@ -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
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
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
|
|
914
|
+
return ids.slice(windowStart, Math.min(windowEnd, windowStart + options.limit));
|
|
885
915
|
}
|
|
886
|
-
return
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
|
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
|
-
};
|