@tonyclaw/agent-inspector 4.0.1 → 4.0.3
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 +193 -110
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-CTHqmroT.mjs → router-ChILzhmz.mjs} +92 -4
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-L3n4J-Or.js → CompareDrawer-Co3cUF_I.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D5o_3OZS.js → InspectorPet-jFcrL37X.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Ocagx75S.js → ProxyViewerContainer-BPzt_VpM.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-Bk7x1h-y.js → ReplayDialog-C6O_zuwI.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-U4lzL7Sc.js → RequestAnatomy-Ca6NvB7L.js} +1 -1
- package/.output/ui/assets/{ResponseView-LOWfvBLK.js → ResponseView-BzqKcveJ.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-DPfsrrXQ.js → StreamingChunkSequence-9pCZIsS_.js} +1 -1
- package/.output/ui/assets/{_sessionId-0MlWS-CO.js → _sessionId-BMP3KVGR.js} +1 -1
- package/.output/ui/assets/{_sessionId-OdX2Jvt1.js → _sessionId-y4eRoT1H.js} +1 -1
- package/.output/ui/assets/{index-Ea-c8hPg.js → index-CH5w-haG.js} +1 -1
- package/.output/ui/assets/{index-zVVS53Hd.js → index-CmocEhyU.js} +1 -1
- package/.output/ui/assets/{index-Du4qNOOt.js → index-DT8sMJ_n.js} +2 -2
- package/.output/ui/assets/{index-gvFj_9K6.js → index-bfsbIRZa.js} +1 -1
- package/.output/ui/assets/{json-viewer-C1WAMzg3.js → json-viewer-BfFOSDG5.js} +1 -1
- package/.output/ui/assets/{jszip.min-B8AKu2zm.js → jszip.min-BkbVkOsp.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +5 -10
- package/src/assets/IDE/claudecode-color.svg +0 -1
- package/src/assets/IDE/codex-color.svg +0 -1
- package/src/assets/IDE/opencode.svg +0 -1
- package/src/assets/IDE/xiaomimimo.svg +0 -1
- package/src/assets/favicon.svg +0 -21
- package/src/assets/logos/alibaba.svg +0 -1
- package/src/assets/logos/anthropic.svg +0 -1
- package/src/assets/logos/claude-code.svg +0 -4
- package/src/assets/logos/deepseek.svg +0 -1
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +0 -1
- package/src/assets/logos/opencode.svg +0 -4
- package/src/assets/logos/qwen.webp +0 -0
- package/src/assets/logos/zhipuai.svg +0 -219
- package/src/backend/router.tsx +0 -36
- package/src/backend/routes/$.ts +0 -26
- package/src/backend/routes/__root.ts +0 -4
- package/src/backend/routes/api/-instances.ts +0 -7
- package/src/backend/routes/api/alerts.summary.ts +0 -28
- package/src/backend/routes/api/alerts.ts +0 -70
- package/src/backend/routes/api/config.paths.ts +0 -14
- package/src/backend/routes/api/config.ts +0 -73
- package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.ts +0 -21
- package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
- package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
- package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
- package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
- package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
- package/src/backend/routes/api/groups.$groupId.ts +0 -93
- package/src/backend/routes/api/groups.ts +0 -79
- package/src/backend/routes/api/health.ts +0 -22
- package/src/backend/routes/api/instances.$name.connection.ts +0 -12
- package/src/backend/routes/api/instances.$name.restart.ts +0 -23
- package/src/backend/routes/api/instances.$name.start.ts +0 -18
- package/src/backend/routes/api/instances.$name.stop.ts +0 -23
- package/src/backend/routes/api/instances.$name.ts +0 -12
- package/src/backend/routes/api/instances.ts +0 -16
- package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
- package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
- package/src/backend/routes/api/knowledge.candidates.ts +0 -10
- package/src/backend/routes/api/knowledge.project-context.ts +0 -29
- package/src/backend/routes/api/knowledge.search.ts +0 -62
- package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
- package/src/backend/routes/api/logs.$id.body.ts +0 -58
- package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
- package/src/backend/routes/api/logs.$id.replay.ts +0 -297
- package/src/backend/routes/api/logs.$id.ts +0 -23
- package/src/backend/routes/api/logs.import.ts +0 -39
- package/src/backend/routes/api/logs.stream.ts +0 -174
- package/src/backend/routes/api/logs.ts +0 -214
- package/src/backend/routes/api/mcp.ts +0 -25
- package/src/backend/routes/api/models.ts +0 -10
- package/src/backend/routes/api/pi-agent.ts +0 -29
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
- package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
- package/src/backend/routes/api/providers.$providerId.ts +0 -111
- package/src/backend/routes/api/providers.export.ts +0 -44
- package/src/backend/routes/api/providers.import.ts +0 -81
- package/src/backend/routes/api/providers.scan.ts +0 -28
- package/src/backend/routes/api/providers.ts +0 -102
- package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
- package/src/backend/routes/api/runs.$runId.ts +0 -50
- package/src/backend/routes/api/runs.ts +0 -62
- package/src/backend/routes/api/runtime.ts +0 -46
- package/src/backend/routes/api/sessions.ts +0 -51
- package/src/backend/routes/api/storage.ts +0 -56
- package/src/backend/routes/livez.ts +0 -13
- package/src/backend/routes/metrics.ts +0 -14
- package/src/backend/routes/proxy/$.ts +0 -15
- package/src/backend/routes/readyz.ts +0 -18
- package/src/backend/start.ts +0 -10
- package/src/cli/alias.ts +0 -431
- package/src/cli/detect-tools.ts +0 -123
- package/src/cli/doctor.ts +0 -836
- package/src/cli/instance.ts +0 -1071
- package/src/cli/instanceApi.ts +0 -213
- package/src/cli/instanceArgs.ts +0 -301
- package/src/cli/instanceControl.ts +0 -191
- package/src/cli/instanceModel.ts +0 -276
- package/src/cli/instanceRegistry.ts +0 -731
- package/src/cli/networkHints.ts +0 -170
- package/src/cli/onboard.ts +0 -1668
- package/src/cli/onboardTarget.ts +0 -228
- package/src/cli/rustBackendPackage.ts +0 -458
- package/src/cli/startupOutput.ts +0 -16
- package/src/cli/templates/codex-skill-onboard.ts +0 -320
- package/src/cli/templates/command-onboard.ts +0 -25
- package/src/cli/templates/skill-onboard.ts +0 -625
- package/src/cli.ts +0 -1433
- package/src/components/OnboardingBanner.tsx +0 -115
- package/src/components/ProxyViewer.tsx +0 -2259
- package/src/components/ProxyViewerContainer.tsx +0 -1076
- package/src/components/alerts/AlertsDialog.tsx +0 -615
- package/src/components/clients/ClientLogo.tsx +0 -175
- package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
- package/src/components/errors/SafeErrorBoundary.tsx +0 -201
- package/src/components/groups/GroupsDialog.tsx +0 -1304
- package/src/components/inspector-pet/InspectorPet.tsx +0 -398
- package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
- package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
- package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
- package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
- package/src/components/providers/ImportWizardDialog.tsx +0 -281
- package/src/components/providers/ProviderCard.tsx +0 -347
- package/src/components/providers/ProviderForm.tsx +0 -1183
- package/src/components/providers/ProviderLogo.tsx +0 -122
- package/src/components/providers/ProviderTestResultsView.tsx +0 -734
- package/src/components/providers/ProvidersPanel.tsx +0 -714
- package/src/components/providers/SettingsDialog.tsx +0 -1042
- package/src/components/providers/providerFormCapabilities.ts +0 -36
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
- package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
- package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
- package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
- package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
- package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
- package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
- package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
- package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
- package/src/components/proxy-viewer/ResponseView.tsx +0 -202
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
- package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
- package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
- package/src/components/proxy-viewer/accessibility.ts +0 -8
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
- package/src/components/proxy-viewer/anatomy/types.ts +0 -41
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
- package/src/components/proxy-viewer/bodyHydration.ts +0 -123
- package/src/components/proxy-viewer/cacheTrend.ts +0 -50
- package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
- package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
- package/src/components/proxy-viewer/diff/index.ts +0 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
- package/src/components/proxy-viewer/formats/index.tsx +0 -47
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
- package/src/components/proxy-viewer/index.ts +0 -9
- package/src/components/proxy-viewer/lazy.ts +0 -41
- package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
- package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
- package/src/components/proxy-viewer/log-formats/index.ts +0 -23
- package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
- package/src/components/proxy-viewer/log-formats/types.ts +0 -40
- package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
- package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
- package/src/components/proxy-viewer/logFocus.ts +0 -54
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
- package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
- package/src/components/proxy-viewer/replayComparison.ts +0 -131
- package/src/components/proxy-viewer/requestDiff.ts +0 -277
- package/src/components/proxy-viewer/requestTools.ts +0 -220
- package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
- package/src/components/proxy-viewer/viewerState.ts +0 -176
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/collapsible.tsx +0 -21
- package/src/components/ui/confirm-dialog.tsx +0 -51
- package/src/components/ui/crab-logo.tsx +0 -45
- package/src/components/ui/crab-variants.tsx +0 -467
- package/src/components/ui/dialog.tsx +0 -130
- package/src/components/ui/icon-trigger.ts +0 -2
- package/src/components/ui/json-viewer-bulk.ts +0 -132
- package/src/components/ui/json-viewer.tsx +0 -508
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/select.tsx +0 -178
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -88
- package/src/components/ui/tooltip.tsx +0 -51
- package/src/components/ui/transient-toast.tsx +0 -105
- package/src/contracts/anthropic.ts +0 -230
- package/src/contracts/index.ts +0 -59
- package/src/contracts/json.ts +0 -37
- package/src/contracts/log.ts +0 -139
- package/src/contracts/openai.ts +0 -267
- package/src/index.css +0 -11
- package/src/knowledge/candidateStore.ts +0 -94
- package/src/knowledge/distiller.ts +0 -98
- package/src/knowledge/openclawClient.ts +0 -161
- package/src/knowledge/openclawGatewayClient.ts +0 -249
- package/src/knowledge/openclawMarkdown.ts +0 -146
- package/src/knowledge/redactor.ts +0 -80
- package/src/knowledge/types.ts +0 -83
- package/src/lib/alertContract.ts +0 -76
- package/src/lib/apiClient.ts +0 -166
- package/src/lib/apiFormat.ts +0 -29
- package/src/lib/backendImplementation.ts +0 -16
- package/src/lib/basePath.ts +0 -107
- package/src/lib/browserRuntimeContract.ts +0 -124
- package/src/lib/browserRuntimeResolver.ts +0 -333
- package/src/lib/chunkLoadRecovery.ts +0 -83
- package/src/lib/claudeCodeStrip.ts +0 -98
- package/src/lib/clipboard.ts +0 -67
- package/src/lib/codingAgentEndpoints.ts +0 -54
- package/src/lib/ecosystemContract.ts +0 -130
- package/src/lib/export-logs.ts +0 -287
- package/src/lib/groupContract.ts +0 -166
- package/src/lib/instanceContract.ts +0 -239
- package/src/lib/jsonFile.ts +0 -46
- package/src/lib/logImportContract.ts +0 -12
- package/src/lib/managedInstance.ts +0 -112
- package/src/lib/mask.ts +0 -4
- package/src/lib/objectUtils.ts +0 -22
- package/src/lib/piAgentContract.ts +0 -102
- package/src/lib/providerContract.ts +0 -125
- package/src/lib/providerImportContract.ts +0 -27
- package/src/lib/providerModelMetadata.ts +0 -458
- package/src/lib/providerTestContract.ts +0 -170
- package/src/lib/providerTestPrompt.ts +0 -100
- package/src/lib/publicBasePathContext.ts +0 -137
- package/src/lib/resourceLimits.ts +0 -345
- package/src/lib/runContract.ts +0 -164
- package/src/lib/runtimeConfig.ts +0 -43
- package/src/lib/safeDiagnostic.ts +0 -38
- package/src/lib/safeFetch.ts +0 -508
- package/src/lib/serverPort.ts +0 -47
- package/src/lib/sessionInfoContract.ts +0 -78
- package/src/lib/sessionUrl.ts +0 -46
- package/src/lib/ssrfGuard.ts +0 -133
- package/src/lib/stopReason.ts +0 -130
- package/src/lib/themeMode.ts +0 -41
- package/src/lib/timeDisplay.ts +0 -22
- package/src/lib/toolTrace.ts +0 -290
- package/src/lib/uiScale.ts +0 -117
- package/src/lib/unknown.ts +0 -27
- package/src/lib/upstreamUrl.ts +0 -150
- package/src/lib/useAlerts.ts +0 -82
- package/src/lib/useGroupEvidence.ts +0 -37
- package/src/lib/useGroups.ts +0 -32
- package/src/lib/useOnboarding.ts +0 -93
- package/src/lib/useProviders.ts +0 -30
- package/src/lib/useStripConfig.ts +0 -154
- package/src/lib/useThemeMode.ts +0 -40
- package/src/lib/useUiScale.ts +0 -52
- package/src/lib/utils.ts +0 -78
- package/src/mcp/currentContext.ts +0 -65
- package/src/mcp/instanceHandlers.ts +0 -151
- package/src/mcp/loopback.ts +0 -88
- package/src/mcp/mode.ts +0 -75
- package/src/mcp/previewExtractor.ts +0 -233
- package/src/mcp/server.ts +0 -2274
- package/src/mcp/toolHandlers.ts +0 -1533
- package/src/proxy/alerts.ts +0 -664
- package/src/proxy/chunkStorage.ts +0 -123
- package/src/proxy/config.ts +0 -218
- package/src/proxy/constants.ts +0 -66
- package/src/proxy/dataDir.ts +0 -147
- package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
- package/src/proxy/ecosystemExecutionRoute.ts +0 -116
- package/src/proxy/ecosystemRegistry.ts +0 -296
- package/src/proxy/ecosystemTasks.ts +0 -415
- package/src/proxy/evidenceAnalysis.ts +0 -526
- package/src/proxy/evidenceExporter.ts +0 -235
- package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
- package/src/proxy/formats/anthropic/handler.ts +0 -75
- package/src/proxy/formats/anthropic/index.ts +0 -13
- package/src/proxy/formats/anthropic/register.ts +0 -4
- package/src/proxy/formats/anthropic/schemas.ts +0 -6
- package/src/proxy/formats/anthropic/stream.ts +0 -205
- package/src/proxy/formats/handler.ts +0 -46
- package/src/proxy/formats/index.ts +0 -13
- package/src/proxy/formats/jsonSchema.ts +0 -2
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
- package/src/proxy/formats/openai/codexMetadata.ts +0 -61
- package/src/proxy/formats/openai/handler.ts +0 -133
- package/src/proxy/formats/openai/index.ts +0 -33
- package/src/proxy/formats/openai/provider.ts +0 -50
- package/src/proxy/formats/openai/register.ts +0 -4
- package/src/proxy/formats/openai/schemas.ts +0 -25
- package/src/proxy/formats/openai/stream.ts +0 -600
- package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
- package/src/proxy/formats/providerAdapter.ts +0 -50
- package/src/proxy/formats/providerRegistry.ts +0 -51
- package/src/proxy/formats/providers/index.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -65
- package/src/proxy/groupEvidenceExporter.ts +0 -371
- package/src/proxy/groupStore.ts +0 -336
- package/src/proxy/handler.ts +0 -729
- package/src/proxy/identityProxy.ts +0 -1842
- package/src/proxy/jsonlRecovery.ts +0 -554
- package/src/proxy/logBodyChunks.ts +0 -91
- package/src/proxy/logFinalizer.ts +0 -777
- package/src/proxy/logFinalizer.worker.ts +0 -24
- package/src/proxy/logImportUpload.ts +0 -128
- package/src/proxy/logImporter.ts +0 -813
- package/src/proxy/logIndex.ts +0 -663
- package/src/proxy/logSearch.ts +0 -122
- package/src/proxy/logger.ts +0 -537
- package/src/proxy/openaiOrphanToolStrip.ts +0 -142
- package/src/proxy/platformCommands.ts +0 -258
- package/src/proxy/privateDataPath.ts +0 -419
- package/src/proxy/providerImporters.ts +0 -578
- package/src/proxy/providerScanStore.ts +0 -87
- package/src/proxy/providerSecretStore.ts +0 -216
- package/src/proxy/providers.ts +0 -998
- package/src/proxy/rawStreamCapture.ts +0 -342
- package/src/proxy/runFailures.ts +0 -94
- package/src/proxy/runStore.ts +0 -214
- package/src/proxy/runtimeAdmission.ts +0 -55
- package/src/proxy/runtimeHealth.ts +0 -221
- package/src/proxy/runtimeMetrics.ts +0 -149
- package/src/proxy/runtimeShutdown.ts +0 -157
- package/src/proxy/schemas.ts +0 -112
- package/src/proxy/sessionArchive.ts +0 -479
- package/src/proxy/sessionInfo.ts +0 -266
- package/src/proxy/sessionProcess.ts +0 -194
- package/src/proxy/sessionRuntime.ts +0 -121
- package/src/proxy/sessionSupervisor.ts +0 -305
- package/src/proxy/sessionWorkerEntry.ts +0 -26
- package/src/proxy/shutdownCoordinator.ts +0 -90
- package/src/proxy/socketTracker.ts +0 -639
- package/src/proxy/sqliteLogIndex.ts +0 -733
- package/src/proxy/storageLifecycle.ts +0 -432
- package/src/proxy/store.ts +0 -1773
- package/src/proxy/streamTiming.ts +0 -31
- package/src/proxy/toolSchemaWarnings.ts +0 -529
- package/src/proxy/uiAssetServer.ts +0 -106
- package/src/proxy/upstream.ts +0 -127
- package/src/services/alerts.ts +0 -12
- package/src/services/groups.ts +0 -119
- package/src/services/piAgent.ts +0 -421
- package/src/services/piAgentRetrieval.ts +0 -1180
- package/src/services/result.ts +0 -17
- package/src/services/runs.ts +0 -76
- package/src/ui/bootstrap.ts +0 -167
- package/src/ui/index.html +0 -38
- package/src/ui/logsStreamClient.ts +0 -20
- package/src/ui/main.tsx +0 -13
- package/src/ui/public/runtime-config.js +0 -2
- package/src/ui/router.tsx +0 -13
- package/src/ui/routes/__root.tsx +0 -69
- package/src/ui/routes/index.tsx +0 -35
- package/src/ui/routes/session/$sessionId.tsx +0 -50
- package/src/ui/runtimeDiscovery.ts +0 -50
package/src/proxy/store.ts
DELETED
|
@@ -1,1773 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
createReadStream,
|
|
4
|
-
createWriteStream,
|
|
5
|
-
readdirSync,
|
|
6
|
-
statSync,
|
|
7
|
-
type WriteStream,
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import { open, unlink, rename } from "node:fs/promises";
|
|
10
|
-
import { readdir as readdirCallback } from "node:fs/promises";
|
|
11
|
-
import { createInterface } from "node:readline";
|
|
12
|
-
import { Buffer } from "node:buffer";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
appendLogEntry,
|
|
16
|
-
resolveLogDir,
|
|
17
|
-
logger,
|
|
18
|
-
runWithFlushedLogWriteLock,
|
|
19
|
-
runWithLogWriteLock,
|
|
20
|
-
} from "./logger";
|
|
21
|
-
import { getChunksDir } from "./chunkStorage";
|
|
22
|
-
import {
|
|
23
|
-
addToIndex,
|
|
24
|
-
createLogIndexEntryMetadata,
|
|
25
|
-
findInIndex,
|
|
26
|
-
flushIndex,
|
|
27
|
-
getNextLogId,
|
|
28
|
-
getCurrentLogFile,
|
|
29
|
-
listFilteredIndexEntries,
|
|
30
|
-
listIndexEntries,
|
|
31
|
-
rebuildIndex,
|
|
32
|
-
updateLogIndexEntryMetadata,
|
|
33
|
-
type LogIndexEntry,
|
|
34
|
-
type LogIndexSummary,
|
|
35
|
-
} from "./logIndex";
|
|
36
|
-
import { writeChunks } from "./chunkStorage";
|
|
37
|
-
import { closeSqliteLogIndex, listSqliteLogCursorEntries } from "./sqliteLogIndex";
|
|
38
|
-
import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
|
|
39
|
-
import { extractToolTraceEvents } from "../lib/toolTrace";
|
|
40
|
-
import { extractStopReason } from "../lib/stopReason";
|
|
41
|
-
import { textByteLength } from "../lib/unknown";
|
|
42
|
-
import { buildSessionInfo, buildSessionLogSummary } from "./sessionInfo";
|
|
43
|
-
import type { CapturedLog } from "./schemas";
|
|
44
|
-
import { CapturedLogSchema } from "./schemas";
|
|
45
|
-
import { apiFormatForPath } from "./formats/registry";
|
|
46
|
-
import type { ImportedLogDraft } from "./logImporter";
|
|
47
|
-
import {
|
|
48
|
-
archiveSessionLogs,
|
|
49
|
-
clearSessionArchives,
|
|
50
|
-
deleteArchivedLogsByIds,
|
|
51
|
-
findArchivedLogById,
|
|
52
|
-
getSessionArchiveRoot,
|
|
53
|
-
listArchivedSessionLogs,
|
|
54
|
-
listArchivedSessionIds,
|
|
55
|
-
} from "./sessionArchive";
|
|
56
|
-
import {
|
|
57
|
-
ensurePrivateDirectorySync,
|
|
58
|
-
PRIVATE_FILE_MODE,
|
|
59
|
-
securePrivateFileSync,
|
|
60
|
-
} from "./privateDataPath";
|
|
61
|
-
import { recoverCapturedLogStorage } from "./jsonlRecovery";
|
|
62
|
-
import {
|
|
63
|
-
clearSessionRegistry,
|
|
64
|
-
getLogSessionId,
|
|
65
|
-
getSessionSnapshots,
|
|
66
|
-
getSessionIds,
|
|
67
|
-
markSessionFinished,
|
|
68
|
-
markSessionStarted,
|
|
69
|
-
observeSessionLog,
|
|
70
|
-
rebuildSessionRegistry,
|
|
71
|
-
resolveSessionIdentity,
|
|
72
|
-
type SessionClientInfo,
|
|
73
|
-
type SessionSnapshot,
|
|
74
|
-
} from "./sessionSupervisor";
|
|
75
|
-
import { installStorageLifecycleMaintenance, runStorageLifecycleGc } from "./storageLifecycle";
|
|
76
|
-
|
|
77
|
-
export type { CapturedLog };
|
|
78
|
-
export { getNextLogId };
|
|
79
|
-
export { getLogSessionId, getSessionSnapshots } from "./sessionSupervisor";
|
|
80
|
-
|
|
81
|
-
const MAX_MEMORY_CACHE = 100;
|
|
82
|
-
|
|
83
|
-
export type LogStorageStats = {
|
|
84
|
-
memoryCount: number;
|
|
85
|
-
logDir: string;
|
|
86
|
-
logFileCount: number;
|
|
87
|
-
logBytes: number;
|
|
88
|
-
chunkDir: string;
|
|
89
|
-
chunkFileCount: number;
|
|
90
|
-
chunkBytes: number;
|
|
91
|
-
sessionArchiveDir: string;
|
|
92
|
-
sessionArchiveFileCount: number;
|
|
93
|
-
sessionArchiveBytes: number;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export type ClearPersistedLogStorageResult = {
|
|
97
|
-
cleared: number;
|
|
98
|
-
logFilesDeleted: number;
|
|
99
|
-
chunkFilesDeleted: number;
|
|
100
|
-
sessionArchiveFilesDeleted: number;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export type ClearPersistedLogsByIdsResult = {
|
|
104
|
-
cleared: number;
|
|
105
|
-
logFilesRewritten: number;
|
|
106
|
-
chunkFilesDeleted: number;
|
|
107
|
-
sessionArchiveLogsDeleted: number;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export type ListLogsPageOptions = {
|
|
111
|
-
sessionId?: string;
|
|
112
|
-
model?: string;
|
|
113
|
-
clientPid?: number;
|
|
114
|
-
offset: number;
|
|
115
|
-
limit: number;
|
|
116
|
-
includeBodies?: boolean;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export type ListLogsPageResult = {
|
|
120
|
-
logs: CapturedLog[];
|
|
121
|
-
total: number;
|
|
122
|
-
offset: number;
|
|
123
|
-
limit: number;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export type ListLogsCursorPageOptions = {
|
|
127
|
-
sessionId?: string;
|
|
128
|
-
model?: string;
|
|
129
|
-
clientPid?: number;
|
|
130
|
-
limit: number;
|
|
131
|
-
includeBodies?: boolean;
|
|
132
|
-
beforeLogId?: number;
|
|
133
|
-
afterLogId?: number;
|
|
134
|
-
anchor?: "newest" | "oldest";
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export type ListLogsCursorPageResult = {
|
|
138
|
-
logs: CapturedLog[];
|
|
139
|
-
total: number;
|
|
140
|
-
limit: number;
|
|
141
|
-
hasOlder: boolean;
|
|
142
|
-
hasNewer: boolean;
|
|
143
|
-
oldestLogId: number | null;
|
|
144
|
-
newestLogId: number | null;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export type GetSessionInfoOptions = {
|
|
148
|
-
baseUrl: string;
|
|
149
|
-
includeHistory?: boolean;
|
|
150
|
-
latestLogLimit?: number;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
|
|
154
|
-
const memoryCache: Map<number, CapturedLog> = new Map();
|
|
155
|
-
const MAX_CURSOR_PAGE_INDEXES = 16;
|
|
156
|
-
|
|
157
|
-
type LogCursorPageIndex = {
|
|
158
|
-
key: string;
|
|
159
|
-
sessionId?: string;
|
|
160
|
-
model?: string;
|
|
161
|
-
clientPid?: number;
|
|
162
|
-
ids: number[];
|
|
163
|
-
logsById: Map<number, CapturedLog>;
|
|
164
|
-
entriesById: Map<number, LogIndexEntry>;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const logCursorPageIndexes: Map<string, LogCursorPageIndex> = new Map();
|
|
168
|
-
let logCursorPageIndexBuildCount = 0;
|
|
169
|
-
let sessionArchiveQueue: Promise<void> = Promise.resolve();
|
|
170
|
-
const SESSION_ARCHIVE_BATCH_SIZE = 128;
|
|
171
|
-
const SESSION_ARCHIVE_FLUSH_DELAY_MS = 10;
|
|
172
|
-
let sessionArchiveBuffer: CapturedLog[] = [];
|
|
173
|
-
let sessionArchiveFlushTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
174
|
-
|
|
175
|
-
function drainSessionArchiveBuffer(): CapturedLog[] {
|
|
176
|
-
const logs = sessionArchiveBuffer;
|
|
177
|
-
sessionArchiveBuffer = [];
|
|
178
|
-
return logs;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function queueSessionArchiveFlush(): void {
|
|
182
|
-
const logs = drainSessionArchiveBuffer();
|
|
183
|
-
if (logs.length === 0) return;
|
|
184
|
-
sessionArchiveQueue = sessionArchiveQueue
|
|
185
|
-
.catch(() => undefined)
|
|
186
|
-
.then(async () => {
|
|
187
|
-
await archiveSessionLogs(logs);
|
|
188
|
-
})
|
|
189
|
-
.catch((err: unknown) => {
|
|
190
|
-
logger.warn("[store] Failed to archive session logs:", String(err));
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function cancelSessionArchiveFlushTimer(): void {
|
|
195
|
-
if (sessionArchiveFlushTimeout === null) return;
|
|
196
|
-
clearTimeout(sessionArchiveFlushTimeout);
|
|
197
|
-
sessionArchiveFlushTimeout = null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function scheduleSessionArchiveFlush(): void {
|
|
201
|
-
if (sessionArchiveFlushTimeout !== null) return;
|
|
202
|
-
sessionArchiveFlushTimeout = setTimeout(() => {
|
|
203
|
-
sessionArchiveFlushTimeout = null;
|
|
204
|
-
queueSessionArchiveFlush();
|
|
205
|
-
}, SESSION_ARCHIVE_FLUSH_DELAY_MS);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function cursorPageIndexKey(
|
|
209
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
210
|
-
): string {
|
|
211
|
-
return JSON.stringify({
|
|
212
|
-
sessionId: options.sessionId ?? null,
|
|
213
|
-
model: options.model ?? null,
|
|
214
|
-
clientPid: options.clientPid ?? null,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function matchesCursorIndexFilters(log: CapturedLog, index: LogCursorPageIndex): boolean {
|
|
219
|
-
if (index.sessionId !== undefined && getLogSessionId(log) !== index.sessionId) return false;
|
|
220
|
-
if (index.model !== undefined && log.model !== index.model) return false;
|
|
221
|
-
if (index.clientPid !== undefined && log.clientPid !== index.clientPid) return false;
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function insertSortedLogId(ids: number[], id: number): void {
|
|
226
|
-
if (ids.includes(id)) return;
|
|
227
|
-
const insertionIndex = ids.findIndex((candidate) => candidate > id);
|
|
228
|
-
if (insertionIndex === -1) {
|
|
229
|
-
ids.push(id);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
ids.splice(insertionIndex, 0, id);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function upsertLogCursorPageIndexes(log: CapturedLog): void {
|
|
236
|
-
for (const index of logCursorPageIndexes.values()) {
|
|
237
|
-
if (!matchesCursorIndexFilters(log, index)) {
|
|
238
|
-
if (index.logsById.delete(log.id)) {
|
|
239
|
-
index.ids = index.ids.filter((id) => id !== log.id);
|
|
240
|
-
}
|
|
241
|
-
index.entriesById.delete(log.id);
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
index.logsById.set(log.id, log);
|
|
245
|
-
insertSortedLogId(index.ids, log.id);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function removeLogsFromCursorPageIndexes(ids: ReadonlySet<number>): void {
|
|
250
|
-
if (ids.size === 0) return;
|
|
251
|
-
for (const index of logCursorPageIndexes.values()) {
|
|
252
|
-
for (const id of ids) {
|
|
253
|
-
index.logsById.delete(id);
|
|
254
|
-
index.entriesById.delete(id);
|
|
255
|
-
}
|
|
256
|
-
index.ids = index.ids.filter((id) => !ids.has(id));
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function clearLogCursorPageIndexes(): void {
|
|
261
|
-
logCursorPageIndexes.clear();
|
|
262
|
-
logCursorPageIndexBuildCount = 0;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function evictOldestIfNeeded(): void {
|
|
266
|
-
while (memoryCache.size > MAX_MEMORY_CACHE) {
|
|
267
|
-
// Map preserves insertion order; first key = oldest
|
|
268
|
-
const oldestKey = memoryCache.keys().next().value;
|
|
269
|
-
if (oldestKey === undefined) break;
|
|
270
|
-
memoryCache.delete(oldestKey);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function normalizeLog(log: CapturedLog): CapturedLog {
|
|
275
|
-
const sessionId = getLogSessionId(log);
|
|
276
|
-
const pathFormat = apiFormatForPath(log.path);
|
|
277
|
-
const apiFormat = pathFormat === "unknown" ? log.apiFormat : pathFormat;
|
|
278
|
-
const captureIncomplete = log.captureIncomplete ?? false;
|
|
279
|
-
const droppedChunks = log.droppedChunks ?? 0;
|
|
280
|
-
const droppedBytes = log.droppedBytes ?? 0;
|
|
281
|
-
const captureIncompleteReason = log.captureIncompleteReason ?? null;
|
|
282
|
-
const stopReason = log.stopReason ?? extractStopReason(log);
|
|
283
|
-
if (
|
|
284
|
-
sessionId === log.sessionId &&
|
|
285
|
-
apiFormat === log.apiFormat &&
|
|
286
|
-
log.captureIncomplete !== undefined &&
|
|
287
|
-
log.droppedChunks !== undefined &&
|
|
288
|
-
log.droppedBytes !== undefined &&
|
|
289
|
-
log.captureIncompleteReason !== undefined &&
|
|
290
|
-
log.stopReason !== undefined
|
|
291
|
-
) {
|
|
292
|
-
return log;
|
|
293
|
-
}
|
|
294
|
-
return {
|
|
295
|
-
...log,
|
|
296
|
-
sessionId,
|
|
297
|
-
apiFormat,
|
|
298
|
-
captureIncomplete,
|
|
299
|
-
droppedChunks,
|
|
300
|
-
droppedBytes,
|
|
301
|
-
captureIncompleteReason,
|
|
302
|
-
stopReason,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function addToCache(log: CapturedLog): void {
|
|
307
|
-
const cachedLog = normalizeLog(log);
|
|
308
|
-
// If updating an existing entry, remove first to reset insertion order
|
|
309
|
-
if (memoryCache.has(cachedLog.id)) {
|
|
310
|
-
memoryCache.delete(cachedLog.id);
|
|
311
|
-
}
|
|
312
|
-
memoryCache.set(cachedLog.id, cachedLog);
|
|
313
|
-
upsertLogCursorPageIndexes(cachedLog);
|
|
314
|
-
evictOldestIfNeeded();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function archiveLogSnapshot(log: CapturedLog): void {
|
|
318
|
-
sessionArchiveBuffer.push(normalizeLog(log));
|
|
319
|
-
if (sessionArchiveBuffer.length >= SESSION_ARCHIVE_BATCH_SIZE) {
|
|
320
|
-
cancelSessionArchiveFlushTimer();
|
|
321
|
-
queueSessionArchiveFlush();
|
|
322
|
-
} else if (sessionArchiveBuffer.length === 1) {
|
|
323
|
-
scheduleSessionArchiveFlush();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export async function flushSessionArchiveWrites(): Promise<void> {
|
|
328
|
-
cancelSessionArchiveFlushTimer();
|
|
329
|
-
if (sessionArchiveBuffer.length > 0) queueSessionArchiveFlush();
|
|
330
|
-
await sessionArchiveQueue;
|
|
331
|
-
if (sessionArchiveBuffer.length > 0) await flushSessionArchiveWrites();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function removeFromCache(id: number): void {
|
|
335
|
-
memoryCache.delete(id);
|
|
336
|
-
removeLogsFromCursorPageIndexes(new Set([id]));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function compactLogForList(log: CapturedLog): CapturedLog {
|
|
340
|
-
return {
|
|
341
|
-
...log,
|
|
342
|
-
rawRequestBody: null,
|
|
343
|
-
responseText: null,
|
|
344
|
-
rawHeaders: undefined,
|
|
345
|
-
headers: undefined,
|
|
346
|
-
streamingChunks: undefined,
|
|
347
|
-
stopReason: log.stopReason ?? extractStopReason(log),
|
|
348
|
-
toolTraceEvents: log.toolTraceEvents ?? extractToolTraceEvents(log),
|
|
349
|
-
rawRequestBodyBytes: log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody),
|
|
350
|
-
responseTextBytes: log.responseTextBytes ?? textByteLength(log.responseText),
|
|
351
|
-
bodyContentMode: "compact",
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function compactLogFromIndexSummary(summary: LogIndexSummary): CapturedLog {
|
|
356
|
-
return normalizeLog({
|
|
357
|
-
id: summary.id,
|
|
358
|
-
timestamp: summary.timestamp,
|
|
359
|
-
method: summary.method,
|
|
360
|
-
path: summary.path,
|
|
361
|
-
model: summary.model,
|
|
362
|
-
sessionId: summary.sessionId,
|
|
363
|
-
rawRequestBody: null,
|
|
364
|
-
responseStatus: summary.responseStatus,
|
|
365
|
-
responseText: null,
|
|
366
|
-
inputTokens: summary.inputTokens,
|
|
367
|
-
outputTokens: summary.outputTokens,
|
|
368
|
-
cacheCreationInputTokens: summary.cacheCreationInputTokens,
|
|
369
|
-
cacheReadInputTokens: summary.cacheReadInputTokens,
|
|
370
|
-
elapsedMs: summary.elapsedMs,
|
|
371
|
-
firstChunkMs: summary.firstChunkMs,
|
|
372
|
-
totalStreamMs: summary.totalStreamMs,
|
|
373
|
-
tokensPerSecond: summary.tokensPerSecond,
|
|
374
|
-
streaming: summary.streaming,
|
|
375
|
-
userAgent: summary.userAgent,
|
|
376
|
-
origin: summary.origin,
|
|
377
|
-
apiFormat: summary.apiFormat,
|
|
378
|
-
isTest: summary.isTest,
|
|
379
|
-
replayOfLogId: summary.replayOfLogId,
|
|
380
|
-
providerName: summary.providerName,
|
|
381
|
-
clientPort: summary.clientPort,
|
|
382
|
-
clientPid: summary.clientPid,
|
|
383
|
-
clientCwd: summary.clientCwd,
|
|
384
|
-
clientProjectFolder: summary.clientProjectFolder,
|
|
385
|
-
streamingChunksPath: summary.streamingChunksPath,
|
|
386
|
-
rawRequestBodyBytes: summary.rawRequestBodyBytes,
|
|
387
|
-
responseTextBytes: summary.responseTextBytes,
|
|
388
|
-
bodyContentMode: "compact",
|
|
389
|
-
captureIncomplete: summary.captureIncomplete,
|
|
390
|
-
droppedChunks: summary.droppedChunks,
|
|
391
|
-
droppedBytes: summary.droppedBytes,
|
|
392
|
-
captureIncompleteReason: summary.captureIncompleteReason,
|
|
393
|
-
warnings: summary.warnings,
|
|
394
|
-
stopReason: summary.stopReason,
|
|
395
|
-
toolTraceEvents: summary.toolTraceEvents,
|
|
396
|
-
error: summary.error,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function prepareLogForList(log: CapturedLog, includeBodies: boolean): CapturedLog {
|
|
401
|
-
return includeBodies ? log : compactLogForList(log);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Add a test log entry to the in-memory store and persistent log file.
|
|
406
|
-
* This is used by the provider test endpoint to seed provider-test sessions.
|
|
407
|
-
*/
|
|
408
|
-
export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
|
|
409
|
-
const id = await getNextLogId();
|
|
410
|
-
|
|
411
|
-
// Persist streaming chunks to disk if present
|
|
412
|
-
let streamingChunksPath: string | null = null;
|
|
413
|
-
if (entry.streamingChunks !== undefined && entry.streamingChunks.chunks.length > 0) {
|
|
414
|
-
streamingChunksPath = writeChunks(
|
|
415
|
-
id,
|
|
416
|
-
entry.streamingChunks.chunks,
|
|
417
|
-
entry.streamingChunks.truncated,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const session = resolveSessionIdentity({
|
|
422
|
-
explicitSessionId: entry.sessionId,
|
|
423
|
-
isTest: entry.isTest,
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const log: CapturedLog = normalizeLog({
|
|
427
|
-
id,
|
|
428
|
-
...entry,
|
|
429
|
-
sessionId: session.id,
|
|
430
|
-
streamingChunksPath,
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
await runWithLogWriteLock(async () => {
|
|
434
|
-
const logFile = getCurrentLogFile();
|
|
435
|
-
appendLogEntry(log);
|
|
436
|
-
await addToIndex(id, logFile, -1, -1, createLogIndexEntryMetadata(log));
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
addToCache(log);
|
|
440
|
-
archiveLogSnapshot(log);
|
|
441
|
-
observeSessionLog(log, session.source);
|
|
442
|
-
emitLogUpdate(log);
|
|
443
|
-
return log;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export async function importCapturedLogs(
|
|
447
|
-
entries: readonly ImportedLogDraft[],
|
|
448
|
-
sessionId: string,
|
|
449
|
-
): Promise<CapturedLog[]> {
|
|
450
|
-
const imported: CapturedLog[] = [];
|
|
451
|
-
for (const entry of entries) {
|
|
452
|
-
const log = await addTestLogEntry({
|
|
453
|
-
...entry,
|
|
454
|
-
sessionId,
|
|
455
|
-
isTest: false,
|
|
456
|
-
replayOfLogId: null,
|
|
457
|
-
});
|
|
458
|
-
imported.push(log);
|
|
459
|
-
}
|
|
460
|
-
return imported;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
type ClientInfo = SessionClientInfo;
|
|
464
|
-
|
|
465
|
-
export async function createLog(
|
|
466
|
-
method: string,
|
|
467
|
-
path: string,
|
|
468
|
-
requestBody: string | null,
|
|
469
|
-
headers: Headers,
|
|
470
|
-
clientInfo?: ClientInfo,
|
|
471
|
-
rawHeaders?: Record<string, string>,
|
|
472
|
-
upstreamHeaders?: Record<string, string>,
|
|
473
|
-
apiFormat: "anthropic" | "openai" | "unknown" = "unknown",
|
|
474
|
-
model: string | null = null,
|
|
475
|
-
sessionId: string | null = null,
|
|
476
|
-
/** Pre-acquired log id; if omitted, `getNextLogId` is awaited internally. */
|
|
477
|
-
preAcquiredId?: number,
|
|
478
|
-
): Promise<CapturedLog> {
|
|
479
|
-
const userAgent = headers.get("user-agent");
|
|
480
|
-
const origin = headers.get("origin");
|
|
481
|
-
|
|
482
|
-
const id = preAcquiredId ?? (await getNextLogId());
|
|
483
|
-
const session = resolveSessionIdentity({
|
|
484
|
-
explicitSessionId: sessionId,
|
|
485
|
-
clientInfo,
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const log: CapturedLog = normalizeLog({
|
|
489
|
-
id,
|
|
490
|
-
timestamp: new Date().toISOString(),
|
|
491
|
-
method,
|
|
492
|
-
path,
|
|
493
|
-
model,
|
|
494
|
-
sessionId: session.id,
|
|
495
|
-
rawRequestBody: requestBody,
|
|
496
|
-
responseStatus: null,
|
|
497
|
-
responseText: null,
|
|
498
|
-
inputTokens: null,
|
|
499
|
-
outputTokens: null,
|
|
500
|
-
cacheCreationInputTokens: null,
|
|
501
|
-
cacheReadInputTokens: null,
|
|
502
|
-
elapsedMs: null,
|
|
503
|
-
firstChunkMs: null,
|
|
504
|
-
totalStreamMs: null,
|
|
505
|
-
tokensPerSecond: null,
|
|
506
|
-
streaming: false,
|
|
507
|
-
userAgent,
|
|
508
|
-
origin,
|
|
509
|
-
rawHeaders: rawHeaders,
|
|
510
|
-
headers: upstreamHeaders,
|
|
511
|
-
apiFormat,
|
|
512
|
-
isTest: false,
|
|
513
|
-
providerName: null,
|
|
514
|
-
clientPort: clientInfo?.port ?? null,
|
|
515
|
-
clientPid: clientInfo?.pid ?? null,
|
|
516
|
-
clientCwd: clientInfo?.cwd ?? null,
|
|
517
|
-
clientProjectFolder: clientInfo?.projectFolder ?? null,
|
|
518
|
-
streamingChunksPath: null,
|
|
519
|
-
captureIncomplete: false,
|
|
520
|
-
droppedChunks: 0,
|
|
521
|
-
droppedBytes: 0,
|
|
522
|
-
captureIncompleteReason: null,
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// Write to disk and update index
|
|
526
|
-
await runWithLogWriteLock(async () => {
|
|
527
|
-
const logFile = getCurrentLogFile();
|
|
528
|
-
appendLogEntry(log);
|
|
529
|
-
await addToIndex(id, logFile, -1, -1, createLogIndexEntryMetadata(log));
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
// Add to memory cache
|
|
533
|
-
addToCache(log);
|
|
534
|
-
archiveLogSnapshot(log);
|
|
535
|
-
markSessionStarted(log, session.source);
|
|
536
|
-
emitLogUpdate(log);
|
|
537
|
-
|
|
538
|
-
return log;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function finalizeLogUpdate(log: CapturedLog): void {
|
|
542
|
-
addToCache(log);
|
|
543
|
-
archiveLogSnapshot(log);
|
|
544
|
-
markSessionFinished(log);
|
|
545
|
-
emitLogUpdate(log);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Persist the terminal cache/session state and refresh both JSON and SQLite index summaries. */
|
|
549
|
-
export async function persistFinalizedLogUpdate(log: CapturedLog): Promise<void> {
|
|
550
|
-
finalizeLogUpdate(log);
|
|
551
|
-
await updateLogIndexEntryMetadata(log);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
async function getArchivedLogByIdForStore(id: number): Promise<CapturedLog | null> {
|
|
555
|
-
await flushSessionArchiveWrites();
|
|
556
|
-
const archivedLog = await findArchivedLogById(id);
|
|
557
|
-
if (archivedLog === null) return null;
|
|
558
|
-
const normalized = normalizeLog(archivedLog);
|
|
559
|
-
addToCache(normalized);
|
|
560
|
-
observeSessionLog(normalized);
|
|
561
|
-
return normalized;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Get a log by ID, checking memory cache first then disk.
|
|
566
|
-
* Uses byte-offset index for efficient single-line reads.
|
|
567
|
-
*/
|
|
568
|
-
export async function getLogById(id: number): Promise<CapturedLog | null> {
|
|
569
|
-
// Check memory cache first
|
|
570
|
-
const cached = memoryCache.get(id);
|
|
571
|
-
if (cached !== undefined) {
|
|
572
|
-
return cached;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Look up in index and load from disk
|
|
576
|
-
const entry = await findInIndex(id);
|
|
577
|
-
if (entry === null) {
|
|
578
|
-
return await getArchivedLogByIdForStore(id);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Load from disk using byte-offset index
|
|
582
|
-
try {
|
|
583
|
-
const filePath = join(resolveLogDir(), entry.file);
|
|
584
|
-
if (!existsSync(filePath)) {
|
|
585
|
-
return await getArchivedLogByIdForStore(id);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Use byte offset for direct read when available
|
|
589
|
-
if (entry.byteOffset >= 0 && entry.byteLength > 0) {
|
|
590
|
-
const fh = await open(filePath, "r");
|
|
591
|
-
try {
|
|
592
|
-
const buffer = Buffer.alloc(entry.byteLength);
|
|
593
|
-
const { bytesRead } = await fh.read(buffer, 0, entry.byteLength, entry.byteOffset);
|
|
594
|
-
if (bytesRead === 0) return null;
|
|
595
|
-
const line = buffer.toString("utf-8", 0, bytesRead).trimEnd();
|
|
596
|
-
if (line === "") return null;
|
|
597
|
-
const parsed: unknown = JSON.parse(line);
|
|
598
|
-
const result = CapturedLogSchema.safeParse(parsed);
|
|
599
|
-
if (result.success) {
|
|
600
|
-
const log = normalizeLog(result.data);
|
|
601
|
-
addToCache(log);
|
|
602
|
-
observeSessionLog(log);
|
|
603
|
-
return log;
|
|
604
|
-
}
|
|
605
|
-
} finally {
|
|
606
|
-
await fh.close();
|
|
607
|
-
}
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const lastMatch = await scanLogFileForId(filePath, id);
|
|
612
|
-
if (lastMatch !== null) {
|
|
613
|
-
addToCache(lastMatch);
|
|
614
|
-
observeSessionLog(lastMatch);
|
|
615
|
-
return lastMatch;
|
|
616
|
-
}
|
|
617
|
-
} catch (err) {
|
|
618
|
-
logger.error("[store] Failed to read log from disk:", String(err));
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
return await getArchivedLogByIdForStore(id);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
async function scanLogFileForId(filePath: string, id: number): Promise<CapturedLog | null> {
|
|
625
|
-
let lastMatch: CapturedLog | null = null;
|
|
626
|
-
const fileStream = createInterface({
|
|
627
|
-
input: createReadStream(filePath),
|
|
628
|
-
crlfDelay: Infinity,
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
let lineNumber = 0;
|
|
632
|
-
for await (const line of fileStream) {
|
|
633
|
-
lineNumber += 1;
|
|
634
|
-
if (line.trim() === "") continue;
|
|
635
|
-
let parsed: unknown;
|
|
636
|
-
try {
|
|
637
|
-
parsed = JSON.parse(line);
|
|
638
|
-
} catch (error) {
|
|
639
|
-
return await Promise.reject(
|
|
640
|
-
new Error(
|
|
641
|
-
`Malformed JSONL record in ${filePath} at line ${String(lineNumber)}: ${String(error)}`,
|
|
642
|
-
),
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
const result = CapturedLogSchema.safeParse(parsed);
|
|
646
|
-
if (!result.success) {
|
|
647
|
-
return await Promise.reject(
|
|
648
|
-
new Error(`Invalid captured-log record in ${filePath} at line ${String(lineNumber)}`),
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
if (result.data.id === id) {
|
|
652
|
-
lastMatch = normalizeLog(result.data);
|
|
653
|
-
if (result.data.responseStatus !== null) {
|
|
654
|
-
return lastMatch;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return lastMatch;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
export function getFilteredLogs(
|
|
663
|
-
sessionId?: string,
|
|
664
|
-
model?: string,
|
|
665
|
-
clientPid?: number,
|
|
666
|
-
): CapturedLog[] {
|
|
667
|
-
// Cache maintains insertion order (sorted by ID since logs are added in ID order)
|
|
668
|
-
// Use spread instead of Array.from for slightly better performance
|
|
669
|
-
return [...memoryCache.values()].filter((l) => {
|
|
670
|
-
return matchesLogFilters(l, sessionId, model, clientPid);
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function matchesLogFilters(
|
|
675
|
-
log: CapturedLog,
|
|
676
|
-
sessionId?: string,
|
|
677
|
-
model?: string,
|
|
678
|
-
clientPid?: number,
|
|
679
|
-
): boolean {
|
|
680
|
-
if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return false;
|
|
681
|
-
if (model !== undefined && log.model !== model) return false;
|
|
682
|
-
if (clientPid !== undefined && log.clientPid !== clientPid) return false;
|
|
683
|
-
return true;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
type PersistedLogVisitor = (log: CapturedLog) => void;
|
|
687
|
-
|
|
688
|
-
async function visitPersistedLogs(visitor: PersistedLogVisitor): Promise<void> {
|
|
689
|
-
const logDir = resolveLogDir();
|
|
690
|
-
if (!existsSync(logDir)) return;
|
|
691
|
-
|
|
692
|
-
const recovery = await recoverCapturedLogStorage(logDir);
|
|
693
|
-
if (recovery.level === "unavailable") {
|
|
694
|
-
return await Promise.reject(
|
|
695
|
-
new Error(recovery.diagnostic ?? "Captured-log recovery blocked persisted-log reads"),
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
try {
|
|
700
|
-
const entries = await readdirCallback(logDir);
|
|
701
|
-
const files = entries.filter((file) => file.endsWith(".jsonl")).sort();
|
|
702
|
-
for (const file of files) {
|
|
703
|
-
await visitPersistedLogFile(join(logDir, file), visitor);
|
|
704
|
-
}
|
|
705
|
-
} catch (err) {
|
|
706
|
-
logger.error("[store] Failed to list persisted logs:", String(err));
|
|
707
|
-
return await Promise.reject(
|
|
708
|
-
err instanceof Error ? err : new Error(`Failed to list persisted logs: ${String(err)}`),
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
async function visitPersistedLogFile(
|
|
714
|
-
filePath: string,
|
|
715
|
-
visitor: PersistedLogVisitor,
|
|
716
|
-
): Promise<void> {
|
|
717
|
-
if (!existsSync(filePath)) return;
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
const fileStream = createInterface({
|
|
721
|
-
input: createReadStream(filePath),
|
|
722
|
-
crlfDelay: Infinity,
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
let lineNumber = 0;
|
|
726
|
-
for await (const line of fileStream) {
|
|
727
|
-
lineNumber += 1;
|
|
728
|
-
if (line.trim() === "") continue;
|
|
729
|
-
let parsed: unknown;
|
|
730
|
-
try {
|
|
731
|
-
parsed = JSON.parse(line);
|
|
732
|
-
} catch (error) {
|
|
733
|
-
return await Promise.reject(
|
|
734
|
-
new Error(
|
|
735
|
-
`Malformed JSONL record in ${filePath} at line ${String(lineNumber)}: ${String(error)}`,
|
|
736
|
-
),
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
const result = CapturedLogSchema.safeParse(parsed);
|
|
740
|
-
if (!result.success) {
|
|
741
|
-
return await Promise.reject(
|
|
742
|
-
new Error(`Invalid captured-log record in ${filePath} at line ${String(lineNumber)}`),
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
const log = normalizeLog(result.data);
|
|
746
|
-
visitor(log);
|
|
747
|
-
}
|
|
748
|
-
} catch (err) {
|
|
749
|
-
logger.error("[store] Failed to read persisted log file:", filePath, String(err));
|
|
750
|
-
return await Promise.reject(
|
|
751
|
-
err instanceof Error
|
|
752
|
-
? err
|
|
753
|
-
: new Error(`Failed to read persisted log file ${filePath}: ${String(err)}`),
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
async function visitArchivedSessionLogs(
|
|
759
|
-
sessionId: string | undefined,
|
|
760
|
-
visitor: PersistedLogVisitor,
|
|
761
|
-
): Promise<void> {
|
|
762
|
-
if (sessionId === undefined) return;
|
|
763
|
-
await flushSessionArchiveWrites();
|
|
764
|
-
const logs = await listArchivedSessionLogs(sessionId);
|
|
765
|
-
for (const archivedLog of logs) {
|
|
766
|
-
const log = normalizeLog(archivedLog);
|
|
767
|
-
observeSessionLog(log);
|
|
768
|
-
visitor(log);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function canUseIndexedListPage(options: ListLogsPageOptions): boolean {
|
|
773
|
-
return (
|
|
774
|
-
options.sessionId === undefined &&
|
|
775
|
-
options.model === undefined &&
|
|
776
|
-
options.clientPid === undefined
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function hasUsableIndexOffset(entry: LogIndexEntry): boolean {
|
|
781
|
-
return entry.byteOffset >= 0 && entry.byteLength > 0;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
async function listLogsPageFromIndex(
|
|
785
|
-
options: ListLogsPageOptions,
|
|
786
|
-
allowRebuild: boolean,
|
|
787
|
-
): Promise<ListLogsPageResult | null> {
|
|
788
|
-
if (!canUseIndexedListPage(options)) return null;
|
|
789
|
-
|
|
790
|
-
let entries = await listIndexEntries();
|
|
791
|
-
if (entries.length === 0 && allowRebuild) {
|
|
792
|
-
await rebuildIndex();
|
|
793
|
-
entries = await listIndexEntries();
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const pageStart = options.offset;
|
|
797
|
-
const pageEnd = options.offset + options.limit;
|
|
798
|
-
const pageEntries = entries.slice(pageStart, pageEnd);
|
|
799
|
-
const hasIncompleteOffset = pageEntries.some((entry) => !hasUsableIndexOffset(entry));
|
|
800
|
-
|
|
801
|
-
if (hasIncompleteOffset) {
|
|
802
|
-
if (!allowRebuild) return null;
|
|
803
|
-
await rebuildIndex();
|
|
804
|
-
return await listLogsPageFromIndex(options, false);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const logs: CapturedLog[] = [];
|
|
808
|
-
const includeBodies = options.includeBodies !== false;
|
|
809
|
-
for (const entry of pageEntries) {
|
|
810
|
-
const log = await getLogById(entry.id);
|
|
811
|
-
if (log === null) return null;
|
|
812
|
-
logs.push(prepareLogForList(log, includeBodies));
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
return {
|
|
816
|
-
logs,
|
|
817
|
-
total: entries.length,
|
|
818
|
-
offset: options.offset,
|
|
819
|
-
limit: options.limit,
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLogsPageResult> {
|
|
824
|
-
const indexedPage = await listLogsPageFromIndex(options, true);
|
|
825
|
-
if (indexedPage !== null) return indexedPage;
|
|
826
|
-
|
|
827
|
-
const seenIds = new Set<number>();
|
|
828
|
-
const pageById = new Map<number, CapturedLog>();
|
|
829
|
-
const pageStart = options.offset;
|
|
830
|
-
const pageEnd = options.offset + options.limit;
|
|
831
|
-
const includeBodies = options.includeBodies !== false;
|
|
832
|
-
let total = 0;
|
|
833
|
-
|
|
834
|
-
const visitLog = (log: CapturedLog): void => {
|
|
835
|
-
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
|
|
836
|
-
|
|
837
|
-
if (!seenIds.has(log.id)) {
|
|
838
|
-
seenIds.add(log.id);
|
|
839
|
-
const position = total;
|
|
840
|
-
total += 1;
|
|
841
|
-
if (position >= pageStart && position < pageEnd) {
|
|
842
|
-
pageById.set(log.id, prepareLogForList(log, includeBodies));
|
|
843
|
-
}
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (pageById.has(log.id)) {
|
|
848
|
-
pageById.set(log.id, prepareLogForList(log, includeBodies));
|
|
849
|
-
}
|
|
850
|
-
};
|
|
851
|
-
|
|
852
|
-
await visitPersistedLogs((log) => {
|
|
853
|
-
observeSessionLog(log);
|
|
854
|
-
visitLog(log);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
await visitArchivedSessionLogs(options.sessionId, visitLog);
|
|
858
|
-
|
|
859
|
-
for (const log of memoryCache.values()) {
|
|
860
|
-
visitLog(log);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const logs = [...pageById.values()].sort((left, right) => left.id - right.id);
|
|
864
|
-
|
|
865
|
-
return {
|
|
866
|
-
logs,
|
|
867
|
-
total,
|
|
868
|
-
offset: options.offset,
|
|
869
|
-
limit: options.limit,
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
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;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function selectCursorPageLogs(
|
|
904
|
-
ids: readonly number[],
|
|
905
|
-
options: ListLogsCursorPageOptions,
|
|
906
|
-
): number[] {
|
|
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
|
-
|
|
913
|
-
if (options.afterLogId !== undefined || options.anchor === "oldest") {
|
|
914
|
-
return ids.slice(windowStart, Math.min(windowEnd, windowStart + options.limit));
|
|
915
|
-
}
|
|
916
|
-
return ids.slice(Math.max(windowStart, windowEnd - options.limit), windowEnd);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function rememberLogCursorPageIndex(index: LogCursorPageIndex): LogCursorPageIndex {
|
|
920
|
-
if (logCursorPageIndexes.has(index.key)) {
|
|
921
|
-
logCursorPageIndexes.delete(index.key);
|
|
922
|
-
}
|
|
923
|
-
logCursorPageIndexes.set(index.key, index);
|
|
924
|
-
while (logCursorPageIndexes.size > MAX_CURSOR_PAGE_INDEXES) {
|
|
925
|
-
const oldestKey = logCursorPageIndexes.keys().next().value;
|
|
926
|
-
if (oldestKey === undefined) break;
|
|
927
|
-
logCursorPageIndexes.delete(oldestKey);
|
|
928
|
-
}
|
|
929
|
-
return index;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function hasNullableStringField(entry: LogIndexEntry, field: "sessionId" | "model"): boolean {
|
|
933
|
-
const desc = Object.getOwnPropertyDescriptor(entry, field);
|
|
934
|
-
if (desc === undefined) return false;
|
|
935
|
-
return typeof desc.value === "string" || desc.value === null;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function hasLogIndexFilterMetadata(entry: LogIndexEntry): boolean {
|
|
939
|
-
return hasNullableStringField(entry, "sessionId") && hasNullableStringField(entry, "model");
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function hasLogIndexSummary(entry: LogIndexEntry): boolean {
|
|
943
|
-
return entry.summary !== undefined;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function matchesLogIndexEntryFilters(
|
|
947
|
-
entry: LogIndexEntry,
|
|
948
|
-
sessionId: string | undefined,
|
|
949
|
-
model: string | undefined,
|
|
950
|
-
clientPid: number | undefined,
|
|
951
|
-
): boolean {
|
|
952
|
-
if (sessionId !== undefined && entry.sessionId !== sessionId) return false;
|
|
953
|
-
if (model !== undefined && entry.model !== model) return false;
|
|
954
|
-
if (clientPid !== undefined && entry.summary?.clientPid !== clientPid) return false;
|
|
955
|
-
return true;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
async function buildLogCursorPageIndexFromLogIndex(
|
|
959
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
960
|
-
allowRebuild: boolean,
|
|
961
|
-
): Promise<LogCursorPageIndex | null> {
|
|
962
|
-
let entries = await listSqliteLogCursorEntries(options);
|
|
963
|
-
if (entries === null) {
|
|
964
|
-
if (options.clientPid !== undefined) return null;
|
|
965
|
-
entries = await listFilteredIndexEntries(options);
|
|
966
|
-
}
|
|
967
|
-
if (entries.length === 0 && allowRebuild) {
|
|
968
|
-
await rebuildIndex();
|
|
969
|
-
entries = await listSqliteLogCursorEntries(options);
|
|
970
|
-
if (entries === null) {
|
|
971
|
-
if (options.clientPid !== undefined) return null;
|
|
972
|
-
entries = await listFilteredIndexEntries(options);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (entries.some((entry) => !hasLogIndexFilterMetadata(entry))) {
|
|
977
|
-
if (!allowRebuild) return null;
|
|
978
|
-
await rebuildIndex();
|
|
979
|
-
return await buildLogCursorPageIndexFromLogIndex(options, false);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const matchingEntries = entries.filter((entry) =>
|
|
983
|
-
matchesLogIndexEntryFilters(entry, options.sessionId, options.model, options.clientPid),
|
|
984
|
-
);
|
|
985
|
-
|
|
986
|
-
if (matchingEntries.some((entry) => !hasLogIndexSummary(entry))) {
|
|
987
|
-
if (!allowRebuild) return null;
|
|
988
|
-
await rebuildIndex();
|
|
989
|
-
return await buildLogCursorPageIndexFromLogIndex(options, false);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
if (matchingEntries.some((entry) => !hasUsableIndexOffset(entry) && !memoryCache.has(entry.id))) {
|
|
993
|
-
if (!allowRebuild) return null;
|
|
994
|
-
await rebuildIndex();
|
|
995
|
-
return await buildLogCursorPageIndexFromLogIndex(options, false);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const idsById = new Set<number>();
|
|
999
|
-
const entriesById = new Map<number, LogIndexEntry>();
|
|
1000
|
-
for (const entry of matchingEntries) {
|
|
1001
|
-
idsById.add(entry.id);
|
|
1002
|
-
entriesById.set(entry.id, entry);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const logsById = new Map<number, CapturedLog>();
|
|
1006
|
-
for (const log of memoryCache.values()) {
|
|
1007
|
-
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
|
|
1008
|
-
idsById.add(log.id);
|
|
1009
|
-
logsById.set(log.id, log);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (options.sessionId !== undefined) {
|
|
1013
|
-
await flushSessionArchiveWrites();
|
|
1014
|
-
const archivedLogs = await listArchivedSessionLogs(options.sessionId);
|
|
1015
|
-
for (const archivedLog of archivedLogs) {
|
|
1016
|
-
const log = normalizeLog(archivedLog);
|
|
1017
|
-
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
|
|
1018
|
-
observeSessionLog(log);
|
|
1019
|
-
idsById.add(log.id);
|
|
1020
|
-
logsById.set(log.id, log);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
return {
|
|
1025
|
-
key: cursorPageIndexKey(options),
|
|
1026
|
-
sessionId: options.sessionId,
|
|
1027
|
-
model: options.model,
|
|
1028
|
-
clientPid: options.clientPid,
|
|
1029
|
-
ids: [...idsById].sort((left, right) => left - right),
|
|
1030
|
-
logsById,
|
|
1031
|
-
entriesById,
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
async function buildLogCursorPageIndex(
|
|
1036
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
1037
|
-
): Promise<LogCursorPageIndex> {
|
|
1038
|
-
logCursorPageIndexBuildCount += 1;
|
|
1039
|
-
const indexed = await buildLogCursorPageIndexFromLogIndex(options, true);
|
|
1040
|
-
if (indexed !== null) return indexed;
|
|
1041
|
-
|
|
1042
|
-
const key = cursorPageIndexKey(options);
|
|
1043
|
-
const logsById = new Map<number, CapturedLog>();
|
|
1044
|
-
|
|
1045
|
-
const visitLog = (log: CapturedLog): void => {
|
|
1046
|
-
if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
|
|
1047
|
-
logsById.set(log.id, log);
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
await visitPersistedLogs((log) => {
|
|
1051
|
-
observeSessionLog(log);
|
|
1052
|
-
visitLog(log);
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
await visitArchivedSessionLogs(options.sessionId, visitLog);
|
|
1056
|
-
|
|
1057
|
-
for (const log of memoryCache.values()) {
|
|
1058
|
-
visitLog(log);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return {
|
|
1062
|
-
key,
|
|
1063
|
-
sessionId: options.sessionId,
|
|
1064
|
-
model: options.model,
|
|
1065
|
-
clientPid: options.clientPid,
|
|
1066
|
-
ids: [...logsById.keys()].sort((left, right) => left - right),
|
|
1067
|
-
logsById,
|
|
1068
|
-
entriesById: new Map<number, LogIndexEntry>(),
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
async function getLogCursorPageIndex(
|
|
1073
|
-
options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
|
|
1074
|
-
): Promise<LogCursorPageIndex> {
|
|
1075
|
-
const key = cursorPageIndexKey(options);
|
|
1076
|
-
const cached = logCursorPageIndexes.get(key);
|
|
1077
|
-
if (cached !== undefined) {
|
|
1078
|
-
logCursorPageIndexes.delete(key);
|
|
1079
|
-
logCursorPageIndexes.set(key, cached);
|
|
1080
|
-
return cached;
|
|
1081
|
-
}
|
|
1082
|
-
return rememberLogCursorPageIndex(await buildLogCursorPageIndex(options));
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
async function hydrateCursorPageLogs(
|
|
1086
|
-
index: LogCursorPageIndex,
|
|
1087
|
-
pageIds: readonly number[],
|
|
1088
|
-
): Promise<CapturedLog[]> {
|
|
1089
|
-
const logs: CapturedLog[] = [];
|
|
1090
|
-
for (const id of pageIds) {
|
|
1091
|
-
const cached = index.logsById.get(id);
|
|
1092
|
-
if (cached !== undefined) {
|
|
1093
|
-
logs.push(cached);
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const log = await getLogById(id);
|
|
1098
|
-
if (log === null) continue;
|
|
1099
|
-
index.logsById.set(id, log);
|
|
1100
|
-
logs.push(log);
|
|
1101
|
-
}
|
|
1102
|
-
return logs;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
async function hydrateCompactCursorPageLogs(
|
|
1106
|
-
index: LogCursorPageIndex,
|
|
1107
|
-
pageIds: readonly number[],
|
|
1108
|
-
): Promise<CapturedLog[]> {
|
|
1109
|
-
const logs: CapturedLog[] = [];
|
|
1110
|
-
for (const id of pageIds) {
|
|
1111
|
-
const cached = index.logsById.get(id);
|
|
1112
|
-
if (cached !== undefined) {
|
|
1113
|
-
logs.push(compactLogForList(cached));
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const entry = index.entriesById.get(id);
|
|
1118
|
-
if (entry !== undefined && entry.summary !== undefined) {
|
|
1119
|
-
logs.push(compactLogFromIndexSummary(entry.summary));
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
const log = await getLogById(id);
|
|
1124
|
-
if (log === null) continue;
|
|
1125
|
-
index.logsById.set(id, log);
|
|
1126
|
-
logs.push(compactLogForList(log));
|
|
1127
|
-
}
|
|
1128
|
-
return logs;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
export async function listLogsCursorPage(
|
|
1132
|
-
options: ListLogsCursorPageOptions,
|
|
1133
|
-
): Promise<ListLogsCursorPageResult> {
|
|
1134
|
-
const includeBodies = options.includeBodies !== false;
|
|
1135
|
-
const index = await getLogCursorPageIndex(options);
|
|
1136
|
-
const pageIds = selectCursorPageLogs(index.ids, options);
|
|
1137
|
-
const pageLogs = includeBodies
|
|
1138
|
-
? await hydrateCursorPageLogs(index, pageIds)
|
|
1139
|
-
: await hydrateCompactCursorPageLogs(index, pageIds);
|
|
1140
|
-
const oldestLogId = pageIds[0] ?? null;
|
|
1141
|
-
const newestLogId = pageIds.at(-1) ?? null;
|
|
1142
|
-
const oldestIndex = oldestLogId === null ? -1 : index.ids.indexOf(oldestLogId);
|
|
1143
|
-
const newestIndex = newestLogId === null ? -1 : index.ids.indexOf(newestLogId);
|
|
1144
|
-
|
|
1145
|
-
return {
|
|
1146
|
-
logs: pageLogs.map((log) => prepareLogForList(log, includeBodies)),
|
|
1147
|
-
total: index.ids.length,
|
|
1148
|
-
limit: options.limit,
|
|
1149
|
-
hasOlder: oldestIndex > 0,
|
|
1150
|
-
hasNewer: newestIndex >= 0 && newestIndex < index.ids.length - 1,
|
|
1151
|
-
oldestLogId,
|
|
1152
|
-
newestLogId,
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function findSessionSnapshot(sessionId: string): SessionSnapshot | null {
|
|
1157
|
-
for (const snapshot of getSessionSnapshots()) {
|
|
1158
|
-
if (snapshot.id === sessionId) return snapshot;
|
|
1159
|
-
}
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
async function collectSessionLogSummaries(
|
|
1164
|
-
sessionId: string,
|
|
1165
|
-
includeHistory: boolean,
|
|
1166
|
-
): Promise<SessionLogSummary[]> {
|
|
1167
|
-
const summariesById = new Map<number, SessionLogSummary>();
|
|
1168
|
-
const observeMatchingLog = (log: CapturedLog): void => {
|
|
1169
|
-
if (!matchesLogFilters(log, sessionId, undefined)) return;
|
|
1170
|
-
summariesById.set(log.id, buildSessionLogSummary(log));
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
const indexedSummaries = await collectIndexedSessionLogSummaries(sessionId);
|
|
1174
|
-
for (const summary of indexedSummaries) {
|
|
1175
|
-
summariesById.set(summary.id, summary);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
await flushSessionArchiveWrites();
|
|
1179
|
-
const archivedLogs = await listArchivedSessionLogs(sessionId);
|
|
1180
|
-
for (const archivedLog of archivedLogs) {
|
|
1181
|
-
const log = normalizeLog(archivedLog);
|
|
1182
|
-
observeSessionLog(log);
|
|
1183
|
-
if (!summariesById.has(log.id)) {
|
|
1184
|
-
summariesById.set(log.id, buildSessionLogSummary(log));
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
if (includeHistory && summariesById.size === 0) {
|
|
1189
|
-
await visitPersistedLogs((log) => {
|
|
1190
|
-
observeSessionLog(log);
|
|
1191
|
-
observeMatchingLog(log);
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
for (const log of memoryCache.values()) {
|
|
1196
|
-
observeMatchingLog(log);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
return [...summariesById.values()];
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
async function collectIndexedSessionLogSummaries(sessionId: string): Promise<SessionLogSummary[]> {
|
|
1203
|
-
let entries = await listFilteredIndexEntries({ sessionId });
|
|
1204
|
-
if (entries.length === 0) return [];
|
|
1205
|
-
|
|
1206
|
-
const needsMetadataRebuild = entries.some((entry) => !hasLogIndexFilterMetadata(entry));
|
|
1207
|
-
const needsSummaryRebuild = entries.some((entry) => {
|
|
1208
|
-
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) return false;
|
|
1209
|
-
return !hasLogIndexSummary(entry);
|
|
1210
|
-
});
|
|
1211
|
-
|
|
1212
|
-
if (needsMetadataRebuild || needsSummaryRebuild) {
|
|
1213
|
-
await rebuildIndex();
|
|
1214
|
-
entries = await listFilteredIndexEntries({ sessionId });
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const summaries: SessionLogSummary[] = [];
|
|
1218
|
-
for (const entry of entries) {
|
|
1219
|
-
if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) continue;
|
|
1220
|
-
if (entry.summary === undefined) continue;
|
|
1221
|
-
summaries.push(buildSessionLogSummary(compactLogFromIndexSummary(entry.summary)));
|
|
1222
|
-
}
|
|
1223
|
-
return summaries;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
async function includeSnapshotLastLogSummary(
|
|
1227
|
-
sessionId: string,
|
|
1228
|
-
snapshot: SessionSnapshot | null,
|
|
1229
|
-
summaries: SessionLogSummary[],
|
|
1230
|
-
): Promise<SessionLogSummary[]> {
|
|
1231
|
-
if (snapshot === null || snapshot.lastLogId === null) return summaries;
|
|
1232
|
-
for (const summary of summaries) {
|
|
1233
|
-
if (summary.id === snapshot.lastLogId) return summaries;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
const log = await getLogById(snapshot.lastLogId);
|
|
1237
|
-
if (log === null || !matchesLogFilters(log, sessionId, undefined)) return summaries;
|
|
1238
|
-
return [...summaries, buildSessionLogSummary(log)];
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
export async function getSessionInfo(
|
|
1242
|
-
sessionId: string,
|
|
1243
|
-
options: GetSessionInfoOptions,
|
|
1244
|
-
): Promise<SessionInfo | null> {
|
|
1245
|
-
const includeHistory = options.includeHistory === true;
|
|
1246
|
-
const collectedSummaries = await collectSessionLogSummaries(sessionId, includeHistory);
|
|
1247
|
-
const snapshot = findSessionSnapshot(sessionId);
|
|
1248
|
-
const summaries = includeHistory
|
|
1249
|
-
? collectedSummaries
|
|
1250
|
-
: await includeSnapshotLastLogSummary(sessionId, snapshot, collectedSummaries);
|
|
1251
|
-
if (snapshot === null && summaries.length === 0) return null;
|
|
1252
|
-
return buildSessionInfo({
|
|
1253
|
-
sessionId,
|
|
1254
|
-
baseUrl: options.baseUrl,
|
|
1255
|
-
snapshot,
|
|
1256
|
-
summaries,
|
|
1257
|
-
logCoverage: includeHistory ? "history" : "recent",
|
|
1258
|
-
latestLogLimit: options.latestLogLimit,
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
export function getSessions(): string[] {
|
|
1263
|
-
return getSessionIds();
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
export async function listSessions(): Promise<string[]> {
|
|
1267
|
-
await flushSessionArchiveWrites();
|
|
1268
|
-
const sessionIds = new Set<string>(getSessionIds());
|
|
1269
|
-
for (const sessionId of await listArchivedSessionIds()) {
|
|
1270
|
-
sessionIds.add(sessionId);
|
|
1271
|
-
}
|
|
1272
|
-
return [...sessionIds].sort();
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
export function getModels(): string[] {
|
|
1276
|
-
const set = new Set<string>();
|
|
1277
|
-
for (const l of memoryCache.values()) {
|
|
1278
|
-
if (l.model !== null && l.model !== "") set.add(l.model);
|
|
1279
|
-
}
|
|
1280
|
-
return [...set];
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
export function getReplayLogsForSource(sourceLogId: number): CapturedLog[] {
|
|
1284
|
-
return [...memoryCache.values()].filter((log) => log.replayOfLogId === sourceLogId);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function collectDirectoryStats(
|
|
1288
|
-
dir: string,
|
|
1289
|
-
shouldCount: (fileName: string) => boolean,
|
|
1290
|
-
): { fileCount: number; bytes: number } {
|
|
1291
|
-
if (!existsSync(dir)) return { fileCount: 0, bytes: 0 };
|
|
1292
|
-
let fileCount = 0;
|
|
1293
|
-
let bytes = 0;
|
|
1294
|
-
const visit = (currentDir: string, depth: number): void => {
|
|
1295
|
-
if (depth > 8) return;
|
|
1296
|
-
let entries: string[];
|
|
1297
|
-
try {
|
|
1298
|
-
entries = readdirSync(currentDir);
|
|
1299
|
-
} catch {
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
for (const entry of entries) {
|
|
1303
|
-
const path = join(currentDir, entry);
|
|
1304
|
-
try {
|
|
1305
|
-
const stats = statSync(path);
|
|
1306
|
-
if (stats.isDirectory()) {
|
|
1307
|
-
visit(path, depth + 1);
|
|
1308
|
-
} else if (shouldCount(entry)) {
|
|
1309
|
-
fileCount += 1;
|
|
1310
|
-
bytes += stats.size;
|
|
1311
|
-
}
|
|
1312
|
-
} catch {
|
|
1313
|
-
// Ignore files that disappear while stats are being collected.
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
visit(dir, 0);
|
|
1318
|
-
return { fileCount, bytes };
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
export function getLogStorageStats(): LogStorageStats {
|
|
1322
|
-
const logDir = resolveLogDir();
|
|
1323
|
-
const chunkDir = getChunksDir();
|
|
1324
|
-
const sessionArchiveDir = getSessionArchiveRoot();
|
|
1325
|
-
const logStats = collectDirectoryStats(logDir, (fileName) => fileName.endsWith(".jsonl"));
|
|
1326
|
-
const chunkStats = collectDirectoryStats(chunkDir, (fileName) => fileName.endsWith(".json"));
|
|
1327
|
-
const sessionArchiveStats = collectDirectoryStats(
|
|
1328
|
-
sessionArchiveDir,
|
|
1329
|
-
(fileName) => fileName === "session.sqlite" || fileName.startsWith("session.sqlite-"),
|
|
1330
|
-
);
|
|
1331
|
-
return {
|
|
1332
|
-
memoryCount: memoryCache.size,
|
|
1333
|
-
logDir,
|
|
1334
|
-
logFileCount: logStats.fileCount,
|
|
1335
|
-
logBytes: logStats.bytes,
|
|
1336
|
-
chunkDir,
|
|
1337
|
-
chunkFileCount: chunkStats.fileCount,
|
|
1338
|
-
chunkBytes: chunkStats.bytes,
|
|
1339
|
-
sessionArchiveDir,
|
|
1340
|
-
sessionArchiveFileCount: sessionArchiveStats.fileCount,
|
|
1341
|
-
sessionArchiveBytes: sessionArchiveStats.bytes,
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
/**
|
|
1346
|
-
* Load recent completed log entries from all log files into the memory cache.
|
|
1347
|
-
* Called on server startup so the frontend shows existing logs.
|
|
1348
|
-
*
|
|
1349
|
-
* Strategy: stream files forward, keep the LAST completed entry per ID,
|
|
1350
|
-
* then only retain the newest MAX_MEMORY_CACHE entries in cache.
|
|
1351
|
-
*/
|
|
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
|
-
|
|
1367
|
-
const logDir = resolveLogDir();
|
|
1368
|
-
if (!existsSync(logDir)) return;
|
|
1369
|
-
|
|
1370
|
-
const recentCompletedById = new Map<number, CapturedLog>();
|
|
1371
|
-
await visitPersistedLogs((log) => {
|
|
1372
|
-
rememberRecentCompletedLog(log, recentCompletedById);
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
// Sort by id descending, keep only MAX_MEMORY_CACHE newest completed entries
|
|
1376
|
-
const sorted = [...recentCompletedById.values()].sort((a, b) => b.id - a.id);
|
|
1377
|
-
for (const entry of sorted) {
|
|
1378
|
-
addToCache(entry);
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
rebuildSessionRegistry(memoryCache.values());
|
|
1382
|
-
clearLogCursorPageIndexes();
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
function rememberRecentCompletedLog(
|
|
1386
|
-
log: CapturedLog,
|
|
1387
|
-
recentCompletedById: Map<number, CapturedLog>,
|
|
1388
|
-
): void {
|
|
1389
|
-
if (log.responseStatus === null) return;
|
|
1390
|
-
recentCompletedById.set(log.id, log);
|
|
1391
|
-
while (recentCompletedById.size > MAX_MEMORY_CACHE) {
|
|
1392
|
-
const oldestId = findSmallestLogId(recentCompletedById);
|
|
1393
|
-
if (oldestId === null) return;
|
|
1394
|
-
recentCompletedById.delete(oldestId);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
function findSmallestLogId(logsById: ReadonlyMap<number, CapturedLog>): number | null {
|
|
1399
|
-
let smallestId: number | null = null;
|
|
1400
|
-
for (const id of logsById.keys()) {
|
|
1401
|
-
if (smallestId === null || id < smallestId) {
|
|
1402
|
-
smallestId = id;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
return smallestId;
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
export function clearAllLogs(): { cleared: number } {
|
|
1409
|
-
const count = memoryCache.size;
|
|
1410
|
-
memoryCache.clear();
|
|
1411
|
-
clearLogCursorPageIndexes();
|
|
1412
|
-
clearSessionRegistry();
|
|
1413
|
-
closeSqliteLogIndex();
|
|
1414
|
-
return { cleared: count };
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
async function deleteMatchingFiles(
|
|
1418
|
-
dir: string,
|
|
1419
|
-
shouldDelete: (fileName: string) => boolean,
|
|
1420
|
-
): Promise<number> {
|
|
1421
|
-
if (!existsSync(dir)) return 0;
|
|
1422
|
-
|
|
1423
|
-
let deleted = 0;
|
|
1424
|
-
let entries: string[];
|
|
1425
|
-
try {
|
|
1426
|
-
entries = await readdirCallback(dir);
|
|
1427
|
-
} catch (err) {
|
|
1428
|
-
logger.warn("[store] Failed to list log storage directory:", String(err));
|
|
1429
|
-
return deleted;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
for (const entry of entries) {
|
|
1433
|
-
if (!shouldDelete(entry)) continue;
|
|
1434
|
-
try {
|
|
1435
|
-
await unlink(join(dir, entry));
|
|
1436
|
-
deleted += 1;
|
|
1437
|
-
} catch (err) {
|
|
1438
|
-
logger.warn("[store] Failed to delete log storage file:", entry, String(err));
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
return deleted;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
export async function clearPersistedLogStorage(): Promise<ClearPersistedLogStorageResult> {
|
|
1445
|
-
return await runWithFlushedLogWriteLock(async () => {
|
|
1446
|
-
await flushIndex();
|
|
1447
|
-
await flushSessionArchiveWrites();
|
|
1448
|
-
|
|
1449
|
-
const result = clearAllLogs();
|
|
1450
|
-
const [logFilesDeleted, chunkFilesDeleted, sessionArchiveFilesDeleted] = await Promise.all([
|
|
1451
|
-
deleteMatchingFiles(
|
|
1452
|
-
resolveLogDir(),
|
|
1453
|
-
(fileName) =>
|
|
1454
|
-
fileName.endsWith(".jsonl") ||
|
|
1455
|
-
fileName === "logs.idx" ||
|
|
1456
|
-
fileName === "inspector.sqlite" ||
|
|
1457
|
-
fileName.startsWith("inspector.sqlite-"),
|
|
1458
|
-
),
|
|
1459
|
-
deleteMatchingFiles(
|
|
1460
|
-
getChunksDir(),
|
|
1461
|
-
(fileName) =>
|
|
1462
|
-
fileName.endsWith(".json") || (fileName.startsWith(".") && fileName.endsWith(".tmp")),
|
|
1463
|
-
),
|
|
1464
|
-
clearSessionArchives(),
|
|
1465
|
-
]);
|
|
1466
|
-
|
|
1467
|
-
await rebuildIndex();
|
|
1468
|
-
|
|
1469
|
-
return {
|
|
1470
|
-
cleared: result.cleared,
|
|
1471
|
-
logFilesDeleted,
|
|
1472
|
-
chunkFilesDeleted,
|
|
1473
|
-
sessionArchiveFilesDeleted,
|
|
1474
|
-
};
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
function readLogIdFromLine(line: string): number | null {
|
|
1479
|
-
if (line.trim() === "") return null;
|
|
1480
|
-
try {
|
|
1481
|
-
const parsed: unknown = JSON.parse(line);
|
|
1482
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
1483
|
-
const idDesc = Object.getOwnPropertyDescriptor(parsed, "id");
|
|
1484
|
-
if (idDesc === undefined || typeof idDesc.value !== "number") return null;
|
|
1485
|
-
return idDesc.value;
|
|
1486
|
-
} catch {
|
|
1487
|
-
return null;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
type RewriteLogFileResult = {
|
|
1492
|
-
rewritten: boolean;
|
|
1493
|
-
removedIds: Set<number>;
|
|
1494
|
-
};
|
|
1495
|
-
|
|
1496
|
-
function tempRewritePath(filePath: string): string {
|
|
1497
|
-
return `${filePath}.rewrite-${String(process.pid)}-${String(Date.now())}.tmp`;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
async function removeTempFile(filePath: string): Promise<void> {
|
|
1501
|
-
try {
|
|
1502
|
-
await unlink(filePath);
|
|
1503
|
-
} catch {
|
|
1504
|
-
// Best-effort cleanup for temp rewrite files.
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
function waitForWriterDrain(writer: WriteStream): Promise<boolean> {
|
|
1509
|
-
return new Promise((resolve) => {
|
|
1510
|
-
const cleanup = (): void => {
|
|
1511
|
-
writer.off("drain", onDrain);
|
|
1512
|
-
writer.off("error", onError);
|
|
1513
|
-
};
|
|
1514
|
-
const onDrain = (): void => {
|
|
1515
|
-
cleanup();
|
|
1516
|
-
resolve(true);
|
|
1517
|
-
};
|
|
1518
|
-
const onError = (): void => {
|
|
1519
|
-
cleanup();
|
|
1520
|
-
resolve(false);
|
|
1521
|
-
};
|
|
1522
|
-
writer.once("drain", onDrain);
|
|
1523
|
-
writer.once("error", onError);
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
async function writeKeptLogLine(writer: WriteStream, line: string): Promise<boolean> {
|
|
1528
|
-
if (writer.write(`${line}\n`, "utf-8")) return true;
|
|
1529
|
-
return await waitForWriterDrain(writer);
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
function finishWriter(writer: WriteStream): Promise<boolean> {
|
|
1533
|
-
return new Promise((resolve) => {
|
|
1534
|
-
const cleanup = (): void => {
|
|
1535
|
-
writer.off("finish", onFinish);
|
|
1536
|
-
writer.off("error", onError);
|
|
1537
|
-
};
|
|
1538
|
-
const onFinish = (): void => {
|
|
1539
|
-
cleanup();
|
|
1540
|
-
resolve(true);
|
|
1541
|
-
};
|
|
1542
|
-
const onError = (): void => {
|
|
1543
|
-
cleanup();
|
|
1544
|
-
resolve(false);
|
|
1545
|
-
};
|
|
1546
|
-
writer.once("finish", onFinish);
|
|
1547
|
-
writer.once("error", onError);
|
|
1548
|
-
writer.end();
|
|
1549
|
-
});
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
async function rewriteLogFileWithoutIds(
|
|
1553
|
-
filePath: string,
|
|
1554
|
-
ids: ReadonlySet<number>,
|
|
1555
|
-
): Promise<RewriteLogFileResult> {
|
|
1556
|
-
const tempPath = tempRewritePath(filePath);
|
|
1557
|
-
if (!ensurePrivateDirectorySync(dirname(filePath))) {
|
|
1558
|
-
return { rewritten: false, removedIds: new Set() };
|
|
1559
|
-
}
|
|
1560
|
-
const writer = createWriteStream(tempPath, {
|
|
1561
|
-
encoding: "utf-8",
|
|
1562
|
-
mode: PRIVATE_FILE_MODE,
|
|
1563
|
-
});
|
|
1564
|
-
const removedIds = new Set<number>();
|
|
1565
|
-
let keptLineCount = 0;
|
|
1566
|
-
let writeOk = true;
|
|
1567
|
-
|
|
1568
|
-
try {
|
|
1569
|
-
const fileStream = createInterface({
|
|
1570
|
-
input: createReadStream(filePath),
|
|
1571
|
-
crlfDelay: Infinity,
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
for await (const line of fileStream) {
|
|
1575
|
-
if (line === "") continue;
|
|
1576
|
-
const id = readLogIdFromLine(line);
|
|
1577
|
-
if (id !== null && ids.has(id)) {
|
|
1578
|
-
removedIds.add(id);
|
|
1579
|
-
} else {
|
|
1580
|
-
keptLineCount += 1;
|
|
1581
|
-
writeOk = await writeKeptLogLine(writer, line);
|
|
1582
|
-
if (!writeOk) break;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
} catch (err) {
|
|
1586
|
-
logger.warn("[store] Failed to stream log file for deletion:", filePath, String(err));
|
|
1587
|
-
writeOk = false;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
if (!(await finishWriter(writer)) || !writeOk) {
|
|
1591
|
-
await removeTempFile(tempPath);
|
|
1592
|
-
return { rewritten: false, removedIds: new Set() };
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
if (!securePrivateFileSync(tempPath)) {
|
|
1596
|
-
await removeTempFile(tempPath);
|
|
1597
|
-
return { rewritten: false, removedIds: new Set() };
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
if (removedIds.size === 0) {
|
|
1601
|
-
await removeTempFile(tempPath);
|
|
1602
|
-
return { rewritten: false, removedIds };
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
try {
|
|
1606
|
-
if (keptLineCount === 0) {
|
|
1607
|
-
await removeTempFile(tempPath);
|
|
1608
|
-
await unlink(filePath);
|
|
1609
|
-
} else {
|
|
1610
|
-
await rename(tempPath, filePath);
|
|
1611
|
-
}
|
|
1612
|
-
} catch (err) {
|
|
1613
|
-
await removeTempFile(tempPath);
|
|
1614
|
-
logger.warn("[store] Failed to rewrite log file after deletion:", filePath, String(err));
|
|
1615
|
-
return { rewritten: false, removedIds: new Set() };
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
return { rewritten: true, removedIds };
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
type RewriteLogFilesResult = {
|
|
1622
|
-
logFilesRewritten: number;
|
|
1623
|
-
removedIds: Set<number>;
|
|
1624
|
-
};
|
|
1625
|
-
|
|
1626
|
-
async function rewriteLogFilesWithoutIds(ids: ReadonlySet<number>): Promise<RewriteLogFilesResult> {
|
|
1627
|
-
const logDir = resolveLogDir();
|
|
1628
|
-
if (!existsSync(logDir)) return { logFilesRewritten: 0, removedIds: new Set() };
|
|
1629
|
-
|
|
1630
|
-
let entries: string[];
|
|
1631
|
-
try {
|
|
1632
|
-
entries = await readdirCallback(logDir);
|
|
1633
|
-
} catch (err) {
|
|
1634
|
-
logger.warn("[store] Failed to list log directory for deletion:", String(err));
|
|
1635
|
-
return { logFilesRewritten: 0, removedIds: new Set() };
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
let logFilesRewritten = 0;
|
|
1639
|
-
const removedIds = new Set<number>();
|
|
1640
|
-
for (const entry of entries) {
|
|
1641
|
-
if (!entry.endsWith(".jsonl")) continue;
|
|
1642
|
-
const result = await rewriteLogFileWithoutIds(join(logDir, entry), ids);
|
|
1643
|
-
if (result.rewritten) logFilesRewritten += 1;
|
|
1644
|
-
for (const removedId of result.removedIds) {
|
|
1645
|
-
removedIds.add(removedId);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
return { logFilesRewritten, removedIds };
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
async function deleteChunkFilesByIds(ids: ReadonlySet<number>): Promise<number> {
|
|
1652
|
-
const chunkDir = getChunksDir();
|
|
1653
|
-
if (!existsSync(chunkDir)) return 0;
|
|
1654
|
-
|
|
1655
|
-
let deleted = 0;
|
|
1656
|
-
for (const id of ids) {
|
|
1657
|
-
const candidates = [`${String(id)}.json`, `.${String(id)}.tmp`];
|
|
1658
|
-
for (const candidate of candidates) {
|
|
1659
|
-
try {
|
|
1660
|
-
await unlink(join(chunkDir, candidate));
|
|
1661
|
-
deleted += 1;
|
|
1662
|
-
} catch {
|
|
1663
|
-
// Missing chunk files are expected for non-streaming logs.
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
return deleted;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
export async function clearPersistedLogsByIds(
|
|
1671
|
-
ids: readonly number[],
|
|
1672
|
-
): Promise<ClearPersistedLogsByIdsResult> {
|
|
1673
|
-
const uniqueIds = new Set(ids);
|
|
1674
|
-
if (uniqueIds.size === 0) {
|
|
1675
|
-
return {
|
|
1676
|
-
cleared: 0,
|
|
1677
|
-
logFilesRewritten: 0,
|
|
1678
|
-
chunkFilesDeleted: 0,
|
|
1679
|
-
sessionArchiveLogsDeleted: 0,
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
return await runWithFlushedLogWriteLock(async () => {
|
|
1684
|
-
await flushIndex();
|
|
1685
|
-
await flushSessionArchiveWrites();
|
|
1686
|
-
|
|
1687
|
-
const memoryRemovedIds = new Set<number>();
|
|
1688
|
-
for (const id of uniqueIds) {
|
|
1689
|
-
if (memoryCache.has(id)) memoryRemovedIds.add(id);
|
|
1690
|
-
}
|
|
1691
|
-
const result = clearLogsByIds([...uniqueIds]);
|
|
1692
|
-
const [rewriteResult, chunkFilesDeleted, archivedRemovedIds] = await Promise.all([
|
|
1693
|
-
rewriteLogFilesWithoutIds(uniqueIds),
|
|
1694
|
-
deleteChunkFilesByIds(uniqueIds),
|
|
1695
|
-
deleteArchivedLogsByIds(uniqueIds),
|
|
1696
|
-
]);
|
|
1697
|
-
|
|
1698
|
-
await rebuildIndex();
|
|
1699
|
-
clearLogCursorPageIndexes();
|
|
1700
|
-
|
|
1701
|
-
const clearedIds = new Set<number>(rewriteResult.removedIds);
|
|
1702
|
-
if (result.cleared > 0) {
|
|
1703
|
-
for (const id of memoryRemovedIds) {
|
|
1704
|
-
clearedIds.add(id);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
for (const id of archivedRemovedIds) {
|
|
1708
|
-
clearedIds.add(id);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
return {
|
|
1712
|
-
cleared: clearedIds.size,
|
|
1713
|
-
logFilesRewritten: rewriteResult.logFilesRewritten,
|
|
1714
|
-
chunkFilesDeleted,
|
|
1715
|
-
sessionArchiveLogsDeleted: archivedRemovedIds.size,
|
|
1716
|
-
};
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
/**
|
|
1721
|
-
* Remove a specific set of log IDs from the in-memory cache. Returns the
|
|
1722
|
-
* number of logs actually removed. IDs not present in the cache are ignored.
|
|
1723
|
-
* This is used by the per-conversation-group "Clear" button in the UI.
|
|
1724
|
-
*/
|
|
1725
|
-
export function clearLogsByIds(ids: readonly number[]): { cleared: number } {
|
|
1726
|
-
const unique = new Set<number>();
|
|
1727
|
-
for (const id of ids) {
|
|
1728
|
-
if (memoryCache.has(id)) unique.add(id);
|
|
1729
|
-
}
|
|
1730
|
-
for (const id of unique) {
|
|
1731
|
-
removeFromCache(id);
|
|
1732
|
-
}
|
|
1733
|
-
removeLogsFromCursorPageIndexes(unique);
|
|
1734
|
-
rebuildSessionRegistry(memoryCache.values());
|
|
1735
|
-
return { cleared: unique.size };
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
export function _getLogCursorPageIndexStatsForTests(): {
|
|
1739
|
-
indexCount: number;
|
|
1740
|
-
buildCount: number;
|
|
1741
|
-
} {
|
|
1742
|
-
return {
|
|
1743
|
-
indexCount: logCursorPageIndexes.size,
|
|
1744
|
-
buildCount: logCursorPageIndexBuildCount,
|
|
1745
|
-
};
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
// SSE event system for real-time log updates
|
|
1749
|
-
type LogUpdateHandler = (log: CapturedLog) => void;
|
|
1750
|
-
const sseHandlers: Set<LogUpdateHandler> = new Set();
|
|
1751
|
-
|
|
1752
|
-
export function onLogUpdate(handler: LogUpdateHandler): () => void {
|
|
1753
|
-
sseHandlers.add(handler);
|
|
1754
|
-
return () => {
|
|
1755
|
-
sseHandlers.delete(handler);
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
export function emitLogUpdate(log: CapturedLog): void {
|
|
1760
|
-
const failedHandlers: LogUpdateHandler[] = [];
|
|
1761
|
-
for (const handler of sseHandlers) {
|
|
1762
|
-
try {
|
|
1763
|
-
handler(log);
|
|
1764
|
-
} catch {
|
|
1765
|
-
// Collect failed handlers to remove after iteration
|
|
1766
|
-
failedHandlers.push(handler);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
// Remove failed handlers after iteration to avoid Set mutation during for...of
|
|
1770
|
-
for (const handler of failedHandlers) {
|
|
1771
|
-
sseHandlers.delete(handler);
|
|
1772
|
-
}
|
|
1773
|
-
}
|