@tonyclaw/agent-inspector 3.1.19 → 3.1.21
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 +140 -20
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-0qlvqhVh.mjs → router-Be6BLuut.mjs} +2454 -1135
- package/.output/server/_tanstack-start-manifest_v-ClTaMkEj.mjs +4 -0
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-BOk9hd-Z.js → CompareDrawer-CGk2HFI1.js} +1 -1
- package/.output/ui/assets/{InspectorPet-DhHDBpk7.js → InspectorPet-CDM7SdOT.js} +1 -1
- package/.output/ui/assets/ProxyViewerContainer-CaWD78tw.js +60 -0
- package/.output/ui/assets/{ReplayDialog-DJhKtuNB.js → ReplayDialog-DqaeV1He.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-hP0tsqDC.js → RequestAnatomy-BtHl9P0k.js} +1 -1
- package/.output/ui/assets/{ResponseView-BSiUkwCb.js → ResponseView-C6t1vpxs.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-CzQqwv4X.js → StreamingChunkSequence-R4mgd3GQ.js} +1 -1
- package/.output/ui/assets/{_sessionId-BU-i6h0V.js → _sessionId-CL3hdLlQ.js} +1 -1
- package/.output/ui/assets/{_sessionId-C2Pq4Jb0.js → _sessionId-HS666S72.js} +1 -1
- package/.output/ui/assets/index-B-Qod-aA.css +1 -0
- package/.output/ui/assets/{index-D8C-t-2U.js → index-Bd9jPL4n.js} +1 -1
- package/.output/ui/assets/{index-zHpVF0jk.js → index-C4NHrclw.js} +1 -1
- package/.output/ui/assets/{index-UUnRwbOJ.js → index-UF1PJEcq.js} +1 -1
- package/.output/ui/assets/{index-B7NeIBDf.js → index-tX38BNV2.js} +2 -2
- package/.output/ui/assets/{json-viewer-DOzS5rUT.js → json-viewer-KSgzN_Ov.js} +1 -1
- package/.output/ui/assets/{jszip.min-CCCY7qNk.js → jszip.min-BiMDxghN.js} +1 -1
- package/.output/ui/index.html +2 -2
- package/.output/workers/logFinalizer.worker.js +261 -0
- package/.output/workers/sessionWorkerEntry.js +261 -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/contracts/index.ts +2 -0
- package/src/contracts/log.ts +5 -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/lib/stopReason.ts +66 -39
- 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 +57 -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/schemas.ts +2 -0
- package/src/proxy/sessionRuntime.ts +38 -0
- package/src/proxy/sqliteLogIndex.ts +78 -2
- package/src/proxy/storageLifecycle.ts +432 -0
- package/src/proxy/store.ts +69 -13
- package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +0 -4
- package/.output/ui/assets/ProxyViewerContainer-BRYf987G.js +0 -59
- package/.output/ui/assets/index-IBEDJoV_.css +0 -1
|
@@ -150,28 +150,72 @@ export function SettingsDialog(): JSX.Element {
|
|
|
150
150
|
|
|
151
151
|
type LogStorageStats = {
|
|
152
152
|
memoryCount: number;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
inventory: {
|
|
154
|
+
checkedAt: string;
|
|
155
|
+
totalFiles: number;
|
|
156
|
+
totalBytes: number;
|
|
157
|
+
artifacts: StorageArtifactSummary[];
|
|
158
|
+
};
|
|
159
|
+
policy: {
|
|
160
|
+
maxAgeMs: number;
|
|
161
|
+
maxTotalBytes: number;
|
|
162
|
+
};
|
|
163
|
+
reclaimableBytes: number;
|
|
164
|
+
lastGc: {
|
|
165
|
+
checkedAt: string;
|
|
166
|
+
filesDeleted: number;
|
|
167
|
+
bytesDeleted: number;
|
|
168
|
+
errors: number;
|
|
169
|
+
beforeTotalBytes: number;
|
|
170
|
+
afterTotalBytes: number;
|
|
171
|
+
} | null;
|
|
162
172
|
};
|
|
163
173
|
|
|
174
|
+
type StorageArtifactSummary = {
|
|
175
|
+
kind: string;
|
|
176
|
+
root: string;
|
|
177
|
+
sensitivity: string;
|
|
178
|
+
cleanupBehavior: string;
|
|
179
|
+
fileCount: number;
|
|
180
|
+
bytes: number;
|
|
181
|
+
oldestModifiedAt: string | null;
|
|
182
|
+
newestModifiedAt: string | null;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const StorageArtifactSummarySchema = z.object({
|
|
186
|
+
kind: z.string(),
|
|
187
|
+
root: z.string(),
|
|
188
|
+
sensitivity: z.string(),
|
|
189
|
+
cleanupBehavior: z.string(),
|
|
190
|
+
fileCount: z.number(),
|
|
191
|
+
bytes: z.number(),
|
|
192
|
+
oldestModifiedAt: z.string().nullable(),
|
|
193
|
+
newestModifiedAt: z.string().nullable(),
|
|
194
|
+
});
|
|
195
|
+
|
|
164
196
|
const LogStorageStatsSchema = z.object({
|
|
165
197
|
memoryCount: z.number(),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
inventory: z.object({
|
|
199
|
+
checkedAt: z.string(),
|
|
200
|
+
totalFiles: z.number(),
|
|
201
|
+
totalBytes: z.number(),
|
|
202
|
+
artifacts: z.array(StorageArtifactSummarySchema),
|
|
203
|
+
}),
|
|
204
|
+
policy: z.object({
|
|
205
|
+
maxAgeMs: z.number(),
|
|
206
|
+
maxTotalBytes: z.number(),
|
|
207
|
+
}),
|
|
208
|
+
reclaimableBytes: z.number(),
|
|
209
|
+
lastGc: z
|
|
210
|
+
.object({
|
|
211
|
+
checkedAt: z.string(),
|
|
212
|
+
filesDeleted: z.number(),
|
|
213
|
+
bytesDeleted: z.number(),
|
|
214
|
+
errors: z.number(),
|
|
215
|
+
beforeTotalBytes: z.number(),
|
|
216
|
+
afterTotalBytes: z.number(),
|
|
217
|
+
})
|
|
218
|
+
.nullable(),
|
|
175
219
|
});
|
|
176
220
|
|
|
177
221
|
const STORAGE_STATS_FETCH_TIMEOUT_MS = 10_000;
|
|
@@ -183,10 +227,16 @@ function formatBytes(bytes: number): string {
|
|
|
183
227
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GiB`;
|
|
184
228
|
}
|
|
185
229
|
|
|
230
|
+
function formatRetention(maxAgeMs: number): string {
|
|
231
|
+
const days = Math.round(maxAgeMs / 1000 / 60 / 60 / 24);
|
|
232
|
+
return `${days.toLocaleString()} days`;
|
|
233
|
+
}
|
|
234
|
+
|
|
186
235
|
function StorageSettingsTab(): JSX.Element {
|
|
187
236
|
const [stats, setStats] = useState<LogStorageStats | null>(null);
|
|
188
237
|
const [error, setError] = useState<string | null>(null);
|
|
189
238
|
const [loading, setLoading] = useState(false);
|
|
239
|
+
const [gcLoading, setGcLoading] = useState(false);
|
|
190
240
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
191
241
|
|
|
192
242
|
const refresh = useCallback(async () => {
|
|
@@ -194,7 +244,7 @@ function StorageSettingsTab(): JSX.Element {
|
|
|
194
244
|
setError(null);
|
|
195
245
|
try {
|
|
196
246
|
const data = await fetchJsonWithTimeout(
|
|
197
|
-
"/api/
|
|
247
|
+
"/api/storage",
|
|
198
248
|
LogStorageStatsSchema,
|
|
199
249
|
STORAGE_STATS_FETCH_TIMEOUT_MS,
|
|
200
250
|
undefined,
|
|
@@ -220,68 +270,98 @@ function StorageSettingsTab(): JSX.Element {
|
|
|
220
270
|
});
|
|
221
271
|
}, []);
|
|
222
272
|
|
|
273
|
+
const handleRunGc = useCallback(async () => {
|
|
274
|
+
setGcLoading(true);
|
|
275
|
+
setError(null);
|
|
276
|
+
try {
|
|
277
|
+
const data = await fetchJsonWithTimeout(
|
|
278
|
+
"/api/storage",
|
|
279
|
+
LogStorageStatsSchema,
|
|
280
|
+
STORAGE_STATS_FETCH_TIMEOUT_MS,
|
|
281
|
+
{ method: "POST" },
|
|
282
|
+
(response) => `Failed to run storage GC: HTTP ${String(response.status)}`,
|
|
283
|
+
);
|
|
284
|
+
setStats(data);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
287
|
+
} finally {
|
|
288
|
+
setGcLoading(false);
|
|
289
|
+
}
|
|
290
|
+
}, []);
|
|
291
|
+
|
|
223
292
|
return (
|
|
224
293
|
<div className="space-y-4">
|
|
225
294
|
<div className="flex items-center justify-between gap-3">
|
|
226
295
|
<div className="min-w-0">
|
|
227
|
-
<h3 className="text-sm font-semibold">
|
|
296
|
+
<h3 className="text-sm font-semibold">Storage lifecycle</h3>
|
|
228
297
|
<p className="text-xs text-muted-foreground">
|
|
229
|
-
Inspect local
|
|
298
|
+
Inspect local captured-data footprint, retention policy, and safe reclaim actions.
|
|
230
299
|
</p>
|
|
231
300
|
</div>
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
301
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
302
|
+
<Button
|
|
303
|
+
type="button"
|
|
304
|
+
variant="outline"
|
|
305
|
+
size="sm"
|
|
306
|
+
onClick={() => void handleRunGc()}
|
|
307
|
+
disabled={gcLoading}
|
|
308
|
+
>
|
|
309
|
+
{gcLoading ? "Running..." : "Run GC now"}
|
|
310
|
+
</Button>
|
|
311
|
+
<Button type="button" variant="outline" size="sm" onClick={() => void refresh()}>
|
|
312
|
+
{loading ? "Refreshing..." : "Refresh"}
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
235
315
|
</div>
|
|
236
316
|
|
|
237
317
|
{stats !== null && (
|
|
238
318
|
<div className="grid gap-2 text-xs">
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
<div className="rounded-md bg-muted/20 px-3 py-2">
|
|
279
|
-
<div className="font-medium">Session archives</div>
|
|
280
|
-
<div className="mt-1 font-mono text-muted-foreground">
|
|
281
|
-
{stats.sessionArchiveFileCount.toLocaleString()} files /{" "}
|
|
282
|
-
{formatBytes(stats.sessionArchiveBytes)}
|
|
283
|
-
</div>
|
|
319
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
320
|
+
<StorageMetricCard
|
|
321
|
+
label="Persisted footprint"
|
|
322
|
+
value={`${stats.inventory.totalFiles.toLocaleString()} files / ${formatBytes(
|
|
323
|
+
stats.inventory.totalBytes,
|
|
324
|
+
)}`}
|
|
325
|
+
detail={`Checked ${new Date(stats.inventory.checkedAt).toLocaleString()}`}
|
|
326
|
+
/>
|
|
327
|
+
<StorageMetricCard
|
|
328
|
+
label="Auto-reclaimable"
|
|
329
|
+
value={formatBytes(stats.reclaimableBytes)}
|
|
330
|
+
detail={`Retention ${formatRetention(stats.policy.maxAgeMs)} / max ${formatBytes(
|
|
331
|
+
stats.policy.maxTotalBytes,
|
|
332
|
+
)}`}
|
|
333
|
+
/>
|
|
334
|
+
<StorageMetricCard
|
|
335
|
+
label="In-memory logs"
|
|
336
|
+
value={`${stats.memoryCount.toLocaleString()} loaded`}
|
|
337
|
+
detail="Recent completed entries kept hot for the UI"
|
|
338
|
+
/>
|
|
339
|
+
<StorageMetricCard
|
|
340
|
+
label="Last GC"
|
|
341
|
+
value={
|
|
342
|
+
stats.lastGc === null
|
|
343
|
+
? "Not run yet"
|
|
344
|
+
: `${stats.lastGc.filesDeleted.toLocaleString()} files / ${formatBytes(
|
|
345
|
+
stats.lastGc.bytesDeleted,
|
|
346
|
+
)}`
|
|
347
|
+
}
|
|
348
|
+
detail={
|
|
349
|
+
stats.lastGc === null
|
|
350
|
+
? "Runs at startup, on schedule, or manually here"
|
|
351
|
+
: `${new Date(stats.lastGc.checkedAt).toLocaleString()} · ${
|
|
352
|
+
stats.lastGc.errors
|
|
353
|
+
} errors`
|
|
354
|
+
}
|
|
355
|
+
/>
|
|
284
356
|
</div>
|
|
357
|
+
{stats.inventory.artifacts.map((artifact) => (
|
|
358
|
+
<StorageArtifactRow
|
|
359
|
+
key={`${artifact.kind}:${artifact.root}`}
|
|
360
|
+
artifact={artifact}
|
|
361
|
+
copiedId={copiedId}
|
|
362
|
+
onCopy={handleCopy}
|
|
363
|
+
/>
|
|
364
|
+
))}
|
|
285
365
|
</div>
|
|
286
366
|
)}
|
|
287
367
|
|
|
@@ -290,6 +370,81 @@ function StorageSettingsTab(): JSX.Element {
|
|
|
290
370
|
);
|
|
291
371
|
}
|
|
292
372
|
|
|
373
|
+
function cleanupBehaviorLabel(cleanupBehavior: string): string {
|
|
374
|
+
switch (cleanupBehavior) {
|
|
375
|
+
case "retention-gc":
|
|
376
|
+
return "Retention GC";
|
|
377
|
+
case "orphan-reclaim":
|
|
378
|
+
return "Orphan reclaim";
|
|
379
|
+
case "rotation":
|
|
380
|
+
return "Rotation-owned";
|
|
381
|
+
case "manual-owner-delete":
|
|
382
|
+
return "Manual only";
|
|
383
|
+
default:
|
|
384
|
+
return cleanupBehavior;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function StorageMetricCard({
|
|
389
|
+
label,
|
|
390
|
+
value,
|
|
391
|
+
detail,
|
|
392
|
+
}: {
|
|
393
|
+
label: string;
|
|
394
|
+
value: string;
|
|
395
|
+
detail: string;
|
|
396
|
+
}): JSX.Element {
|
|
397
|
+
return (
|
|
398
|
+
<div className="rounded-md bg-muted/20 px-3 py-2">
|
|
399
|
+
<div className="font-medium">{label}</div>
|
|
400
|
+
<div className="mt-1 font-mono text-muted-foreground">{value}</div>
|
|
401
|
+
<div className="mt-1 text-[11px] text-muted-foreground">{detail}</div>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function StorageArtifactRow({
|
|
407
|
+
artifact,
|
|
408
|
+
copiedId,
|
|
409
|
+
onCopy,
|
|
410
|
+
}: {
|
|
411
|
+
artifact: StorageArtifactSummary;
|
|
412
|
+
copiedId: string | null;
|
|
413
|
+
onCopy: (id: string, value: string) => void;
|
|
414
|
+
}): JSX.Element {
|
|
415
|
+
const id = `storage-${artifact.kind}`;
|
|
416
|
+
const oldest = artifact.oldestModifiedAt === null ? "none" : new Date(artifact.oldestModifiedAt);
|
|
417
|
+
const newest = artifact.newestModifiedAt === null ? "none" : new Date(artifact.newestModifiedAt);
|
|
418
|
+
return (
|
|
419
|
+
<div className="rounded-md border border-border/45 bg-muted/15 px-3 py-2">
|
|
420
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
421
|
+
<div>
|
|
422
|
+
<div className="font-medium">{artifact.kind}</div>
|
|
423
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
424
|
+
{cleanupBehaviorLabel(artifact.cleanupBehavior)} · {artifact.sensitivity}
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div className="text-right font-mono text-muted-foreground">
|
|
428
|
+
{artifact.fileCount.toLocaleString()} files / {formatBytes(artifact.bytes)}
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
<div className="mt-2 grid gap-1 text-[11px] text-muted-foreground sm:grid-cols-2">
|
|
432
|
+
<div>Oldest: {oldest instanceof Date ? oldest.toLocaleString() : oldest}</div>
|
|
433
|
+
<div>Newest: {newest instanceof Date ? newest.toLocaleString() : newest}</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div className="mt-2">
|
|
436
|
+
<CopyableSetupValue
|
|
437
|
+
id={id}
|
|
438
|
+
label="Root path"
|
|
439
|
+
value={artifact.root}
|
|
440
|
+
copiedId={copiedId}
|
|
441
|
+
onCopy={onCopy}
|
|
442
|
+
/>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
293
448
|
function CopyableSetupValue({
|
|
294
449
|
id,
|
|
295
450
|
label,
|
|
@@ -330,7 +485,7 @@ function CopyableSetupValue({
|
|
|
330
485
|
);
|
|
331
486
|
}
|
|
332
487
|
|
|
333
|
-
function OnboardingSettingsTab(): JSX.Element {
|
|
488
|
+
export function OnboardingSettingsTab(): JSX.Element {
|
|
334
489
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
335
490
|
const [endpoints, setEndpoints] = useState<CodingAgentEndpoints | null>(null);
|
|
336
491
|
useEffect(() => subscribeCodingAgentEndpoints(setEndpoints), []);
|
|
@@ -349,6 +504,12 @@ function OnboardingSettingsTab(): JSX.Element {
|
|
|
349
504
|
value: "agent-inspector onboard --uninstall",
|
|
350
505
|
},
|
|
351
506
|
{ id: "claude", label: "Claude Code command", value: "/agent-inspector:onboard" },
|
|
507
|
+
{ id: "doctor", label: "Backend doctor", value: "agent-inspector doctor --verbose" },
|
|
508
|
+
{
|
|
509
|
+
id: "support-bundle",
|
|
510
|
+
label: "Redacted support bundle",
|
|
511
|
+
value: "agent-inspector doctor --support-bundle",
|
|
512
|
+
},
|
|
352
513
|
{
|
|
353
514
|
id: "codex",
|
|
354
515
|
label: "Codex prompt",
|
|
@@ -432,6 +593,18 @@ function OnboardingSettingsTab(): JSX.Element {
|
|
|
432
593
|
/>
|
|
433
594
|
))}
|
|
434
595
|
</div>
|
|
596
|
+
<div className="grid gap-2 rounded-md border border-status-info/25 bg-status-info/10 px-3 py-2 text-xs">
|
|
597
|
+
<div className="font-medium text-foreground">Verified first trace flow</div>
|
|
598
|
+
<ol className="ml-4 list-decimal space-y-1 text-muted-foreground">
|
|
599
|
+
<li>Start Agent Inspector and confirm the application bar shows Live.</li>
|
|
600
|
+
<li>Copy the Proxy URL or MCP URL that matches your coding agent.</li>
|
|
601
|
+
<li>Run a tiny coding-agent prompt such as “say hello and stop”.</li>
|
|
602
|
+
<li>
|
|
603
|
+
Return to Logs and confirm one new request appears with request, response, tokens, and
|
|
604
|
+
tool activity when present.
|
|
605
|
+
</li>
|
|
606
|
+
</ol>
|
|
607
|
+
</div>
|
|
435
608
|
<div className="grid gap-2 rounded-md bg-muted/15 px-3 py-2 text-xs text-muted-foreground">
|
|
436
609
|
<div className="flex items-center gap-2">
|
|
437
610
|
<Check className="size-3.5 text-status-success" />
|
|
@@ -494,6 +667,24 @@ function OnboardingSettingsTab(): JSX.Element {
|
|
|
494
667
|
reachable from that tool and verify the network/firewall path.
|
|
495
668
|
</span>
|
|
496
669
|
</div>
|
|
670
|
+
<div className="flex items-start gap-2">
|
|
671
|
+
<Check className="mt-0.5 size-3.5 text-status-success" />
|
|
672
|
+
<span>
|
|
673
|
+
If the first trace does not appear, run{" "}
|
|
674
|
+
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-foreground">
|
|
675
|
+
agent-inspector doctor --verbose
|
|
676
|
+
</code>{" "}
|
|
677
|
+
and compare the agent base URL with the current Proxy URL. A base-path mismatch such as{" "}
|
|
678
|
+
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-foreground">
|
|
679
|
+
/
|
|
680
|
+
</code>{" "}
|
|
681
|
+
versus{" "}
|
|
682
|
+
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-foreground">
|
|
683
|
+
/inspector
|
|
684
|
+
</code>{" "}
|
|
685
|
+
means the tool is talking to a different Inspector surface.
|
|
686
|
+
</span>
|
|
687
|
+
</div>
|
|
497
688
|
</div>
|
|
498
689
|
</div>
|
|
499
690
|
);
|
|
@@ -29,9 +29,12 @@ import {
|
|
|
29
29
|
import type { AnatomySegment } from "./anatomy/types";
|
|
30
30
|
import { useAnatomyJump } from "./anatomy/useAnatomyJump";
|
|
31
31
|
import {
|
|
32
|
+
deleteHydratedLogFromCache,
|
|
32
33
|
describeFullBodyHydrationError,
|
|
33
34
|
formatBodyHydrationFootprint,
|
|
34
35
|
formatBytes,
|
|
36
|
+
getOrFetchHydratedLog,
|
|
37
|
+
readHydratedLogFromCache,
|
|
35
38
|
type FullBodyHydrationError,
|
|
36
39
|
} from "./bodyHydration";
|
|
37
40
|
import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
|
|
@@ -874,11 +877,12 @@ export const LogEntry = memo(function ({
|
|
|
874
877
|
|
|
875
878
|
const retryFullBodyHydration = useCallback(() => {
|
|
876
879
|
if (bodyHydrationInFlightRef.current) return;
|
|
880
|
+
deleteHydratedLogFromCache(log.id);
|
|
877
881
|
setHydratedLog(null);
|
|
878
882
|
setBodyHydrationStatus("idle");
|
|
879
883
|
setBodyHydrationError(null);
|
|
880
884
|
setBodyHydrationRetryNonce((current) => current + 1);
|
|
881
|
-
}, []);
|
|
885
|
+
}, [log.id]);
|
|
882
886
|
|
|
883
887
|
const handleToggleExpanded = useCallback(() => {
|
|
884
888
|
const nextExpanded = !expanded;
|
|
@@ -931,14 +935,24 @@ export const LogEntry = memo(function ({
|
|
|
931
935
|
if (!shouldHydrateLogBody(log)) return;
|
|
932
936
|
if (hydratedLog !== null || bodyHydrationInFlightRef.current) return;
|
|
933
937
|
|
|
938
|
+
const cached = readHydratedLogFromCache(log.id);
|
|
939
|
+
if (cached !== null) {
|
|
940
|
+
setHydratedLog(cached);
|
|
941
|
+
setBodyHydrationStatus("idle");
|
|
942
|
+
setBodyHydrationError(null);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
934
946
|
let cancelled = false;
|
|
935
947
|
bodyHydrationInFlightRef.current = true;
|
|
936
948
|
setBodyHydrationStatus("loading");
|
|
937
949
|
setBodyHydrationError(null);
|
|
938
|
-
void
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
950
|
+
void getOrFetchHydratedLog(log.id, () =>
|
|
951
|
+
fetchJsonWithTimeout(
|
|
952
|
+
`/api/logs/${String(log.id)}`,
|
|
953
|
+
CapturedLogSchema,
|
|
954
|
+
FULL_LOG_HYDRATION_TIMEOUT_MS,
|
|
955
|
+
),
|
|
942
956
|
)
|
|
943
957
|
.then((fullLog) => {
|
|
944
958
|
if (cancelled) return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiTimeoutError } from "../../lib/apiClient";
|
|
2
|
+
import type { CapturedLog } from "../../contracts";
|
|
2
3
|
|
|
3
4
|
export type FullBodyHydrationError = {
|
|
4
5
|
title: string;
|
|
@@ -7,6 +8,10 @@ export type FullBodyHydrationError = {
|
|
|
7
8
|
|
|
8
9
|
const DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL =
|
|
9
10
|
"The compact log metadata is still available. Retry if the local server was restarting or the body store was briefly unavailable.";
|
|
11
|
+
const DEFAULT_DETAIL_CACHE_LIMIT = 64;
|
|
12
|
+
|
|
13
|
+
const hydratedLogCache = new Map<number, CapturedLog>();
|
|
14
|
+
const hydratedLogInFlight = new Map<number, Promise<CapturedLog>>();
|
|
10
15
|
|
|
11
16
|
export function formatBytes(bytes: number): string {
|
|
12
17
|
if (bytes < 1024) return `${bytes.toLocaleString()} B`;
|
|
@@ -54,3 +59,65 @@ export function describeFullBodyHydrationError(error: unknown): FullBodyHydratio
|
|
|
54
59
|
detail: DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL,
|
|
55
60
|
};
|
|
56
61
|
}
|
|
62
|
+
|
|
63
|
+
function detailCacheLimit(): number {
|
|
64
|
+
const raw = process.env["AGENT_INSPECTOR_UI_DETAIL_CACHE_LIMIT"];
|
|
65
|
+
if (raw === undefined || raw === "") return DEFAULT_DETAIL_CACHE_LIMIT;
|
|
66
|
+
const parsed = Number(raw);
|
|
67
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1000) {
|
|
68
|
+
return DEFAULT_DETAIL_CACHE_LIMIT;
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function rememberHydratedLog(log: CapturedLog): CapturedLog {
|
|
74
|
+
hydratedLogCache.delete(log.id);
|
|
75
|
+
hydratedLogCache.set(log.id, log);
|
|
76
|
+
while (hydratedLogCache.size > detailCacheLimit()) {
|
|
77
|
+
const oldest = hydratedLogCache.keys().next().value;
|
|
78
|
+
if (typeof oldest !== "number") break;
|
|
79
|
+
hydratedLogCache.delete(oldest);
|
|
80
|
+
}
|
|
81
|
+
return log;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readHydratedLogFromCache(logId: number): CapturedLog | null {
|
|
85
|
+
const cached = hydratedLogCache.get(logId);
|
|
86
|
+
if (cached === undefined) return null;
|
|
87
|
+
hydratedLogCache.delete(logId);
|
|
88
|
+
hydratedLogCache.set(logId, cached);
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deleteHydratedLogFromCache(logId: number): void {
|
|
93
|
+
hydratedLogCache.delete(logId);
|
|
94
|
+
hydratedLogInFlight.delete(logId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function clearHydratedLogCacheForTests(): void {
|
|
98
|
+
hydratedLogCache.clear();
|
|
99
|
+
hydratedLogInFlight.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getHydratedLogCacheSizeForTests(): number {
|
|
103
|
+
return hydratedLogCache.size;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getOrFetchHydratedLog(
|
|
107
|
+
logId: number,
|
|
108
|
+
fetcher: () => Promise<CapturedLog>,
|
|
109
|
+
): Promise<CapturedLog> {
|
|
110
|
+
const cached = readHydratedLogFromCache(logId);
|
|
111
|
+
if (cached !== null) return cached;
|
|
112
|
+
|
|
113
|
+
const existing = hydratedLogInFlight.get(logId);
|
|
114
|
+
if (existing !== undefined) return await existing;
|
|
115
|
+
|
|
116
|
+
const pending = fetcher()
|
|
117
|
+
.then(rememberHydratedLog)
|
|
118
|
+
.finally(() => {
|
|
119
|
+
hydratedLogInFlight.delete(logId);
|
|
120
|
+
});
|
|
121
|
+
hydratedLogInFlight.set(logId, pending);
|
|
122
|
+
return await pending;
|
|
123
|
+
}
|
|
@@ -31,6 +31,84 @@ export function mergeLogsById(
|
|
|
31
31
|
return [...byId.values()].toSorted((left, right) => left.id - right.id);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export type IncrementalLogMergeOptions = {
|
|
35
|
+
recentLimit: number;
|
|
36
|
+
retainSessionId?: string;
|
|
37
|
+
retainIds?: ReadonlySet<number>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type IncrementalLogMergeResult = {
|
|
41
|
+
logs: CapturedLog[];
|
|
42
|
+
index: Map<number, number>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function shouldRetainLog(log: CapturedLog, options: IncrementalLogMergeOptions): boolean {
|
|
46
|
+
if (options.retainIds?.has(log.id) === true) return true;
|
|
47
|
+
if (options.retainSessionId !== undefined && log.sessionId === options.retainSessionId) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function trimBoundedLogs(logs: CapturedLog[], options: IncrementalLogMergeOptions): CapturedLog[] {
|
|
54
|
+
const unretainedCount = logs.reduce(
|
|
55
|
+
(count, log) => count + (shouldRetainLog(log, options) ? 0 : 1),
|
|
56
|
+
0,
|
|
57
|
+
);
|
|
58
|
+
if (unretainedCount <= options.recentLimit) return logs;
|
|
59
|
+
|
|
60
|
+
let unretainedToDrop = unretainedCount - options.recentLimit;
|
|
61
|
+
const retained: CapturedLog[] = [];
|
|
62
|
+
for (const log of logs) {
|
|
63
|
+
if (unretainedToDrop > 0 && !shouldRetainLog(log, options)) {
|
|
64
|
+
unretainedToDrop -= 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
retained.push(log);
|
|
68
|
+
}
|
|
69
|
+
return retained;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildLogIndexById(logs: readonly CapturedLog[]): Map<number, number> {
|
|
73
|
+
const index = new Map<number, number>();
|
|
74
|
+
for (let i = 0; i < logs.length; i += 1) {
|
|
75
|
+
const log = logs[i];
|
|
76
|
+
if (log !== undefined) index.set(log.id, i);
|
|
77
|
+
}
|
|
78
|
+
return index;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function mergeLogsIncrementally(
|
|
82
|
+
previousLogs: readonly CapturedLog[],
|
|
83
|
+
previousIndex: ReadonlyMap<number, number>,
|
|
84
|
+
incomingLogs: readonly CapturedLog[],
|
|
85
|
+
options: IncrementalLogMergeOptions,
|
|
86
|
+
): IncrementalLogMergeResult {
|
|
87
|
+
if (incomingLogs.length === 0) {
|
|
88
|
+
const logs = trimBoundedLogs([...previousLogs], options);
|
|
89
|
+
return { logs, index: buildLogIndexById(logs) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const next = [...previousLogs];
|
|
93
|
+
let lastId = next.length === 0 ? Number.NEGATIVE_INFINITY : (next.at(-1)?.id ?? 0);
|
|
94
|
+
let sorted = true;
|
|
95
|
+
|
|
96
|
+
for (const log of incomingLogs) {
|
|
97
|
+
const existingIndex = previousIndex.get(log.id);
|
|
98
|
+
if (existingIndex !== undefined && existingIndex >= 0 && existingIndex < next.length) {
|
|
99
|
+
next[existingIndex] = log;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
next.push(log);
|
|
103
|
+
if (log.id < lastId) sorted = false;
|
|
104
|
+
lastId = Math.max(lastId, log.id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sortedLogs = sorted ? next : next.toSorted((left, right) => left.id - right.id);
|
|
108
|
+
const logs = trimBoundedLogs(sortedLogs, options);
|
|
109
|
+
return { logs, index: buildLogIndexById(logs) };
|
|
110
|
+
}
|
|
111
|
+
|
|
34
112
|
export function mergeSessionIds(
|
|
35
113
|
previousSessions: readonly string[],
|
|
36
114
|
incomingSessions: readonly string[],
|
package/src/contracts/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
LogBodyPartSchema,
|
|
10
10
|
parseLogId,
|
|
11
11
|
StreamingChunkSchema,
|
|
12
|
+
StopReasonSchema,
|
|
12
13
|
ToolTraceEventSchema,
|
|
13
14
|
} from "./log";
|
|
14
15
|
export type {
|
|
@@ -19,6 +20,7 @@ export type {
|
|
|
19
20
|
LogId,
|
|
20
21
|
LogBodyPart,
|
|
21
22
|
StreamingChunk,
|
|
23
|
+
StopReason,
|
|
22
24
|
ToolTraceEvent,
|
|
23
25
|
TokenUsage,
|
|
24
26
|
} from "./log";
|
package/src/contracts/log.ts
CHANGED
|
@@ -54,6 +54,10 @@ export const ToolTraceEventSchema = z.object({
|
|
|
54
54
|
|
|
55
55
|
export type ToolTraceEvent = z.infer<typeof ToolTraceEventSchema>;
|
|
56
56
|
|
|
57
|
+
const StopReasonValueSchema = z.enum(["end_turn", "tool_use", "stop", "length"]);
|
|
58
|
+
export const StopReasonSchema = StopReasonValueSchema.nullable();
|
|
59
|
+
export type StopReason = z.infer<typeof StopReasonSchema>;
|
|
60
|
+
|
|
57
61
|
const StreamingChunksArraySchema = z.object({
|
|
58
62
|
chunks: z.array(StreamingChunkSchema),
|
|
59
63
|
truncated: z.boolean().optional().default(false),
|
|
@@ -120,6 +124,7 @@ export const CapturedLogSchema = z.object({
|
|
|
120
124
|
captureIncompleteReason: CaptureIncompleteReasonSchema.nullable().optional(),
|
|
121
125
|
warnings: z.array(z.string()).optional(),
|
|
122
126
|
toolTraceEvents: z.array(ToolTraceEventSchema).optional(),
|
|
127
|
+
stopReason: StopReasonSchema.optional(),
|
|
123
128
|
/** Error message from streaming response (e.g., SSE error event) */
|
|
124
129
|
error: z.string().nullable().optional(),
|
|
125
130
|
});
|