@tonyclaw/agent-inspector 3.0.45 → 3.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/{nitro.json → backend/nitro.json} +3 -3
- package/.output/backend-public/assets/index-D4LX-sw-.js +11 -0
- package/.output/cli.js +2326 -516
- package/.output/server/_libs/extend.mjs +4 -8
- package/.output/server/_libs/jszip.mjs +9 -35
- package/.output/server/_libs/react-dom.mjs +2 -3
- package/.output/server/_libs/react.mjs +1 -27
- package/.output/server/_libs/tanstack__react-router.mjs +1 -329
- package/.output/server/_libs/tanstack__router-core.mjs +42 -100
- package/.output/server/_libs/use-sync-external-store.mjs +1 -64
- package/.output/server/_libs/ws.mjs +1 -1
- package/.output/server/_ssr/index.mjs +4 -4
- package/.output/server/_ssr/{publicBasePathContext-5264L3rX.mjs → publicBasePathContext-IH2ilOfu.mjs} +52 -24
- package/.output/server/_ssr/{router-BJ_ltLim.mjs → router-D18yUq36.mjs} +19770 -19447
- package/.output/server/_ssr/{start-C6MjPjxp.mjs → start-COQu_AgD.mjs} +1 -1
- package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +4 -0
- package/.output/server/index.mjs +5 -145
- package/.output/{public/assets/CompareDrawer-B3_TLTJh.js → ui/assets/CompareDrawer-DAUuIJ6G.js} +1 -1
- package/.output/{public/assets/InspectorPet-Cpn6hv4P.js → ui/assets/InspectorPet-BRBVjOWI.js} +1 -1
- package/.output/ui/assets/ProxyViewerContainer-D7Sq0ctc.js +59 -0
- package/.output/{public/assets/ReplayDialog-B3EKVdJk.js → ui/assets/ReplayDialog-nDLsjOhs.js} +1 -1
- package/.output/{public/assets/RequestAnatomy-COOQjyUC.js → ui/assets/RequestAnatomy-zd4BDgL0.js} +1 -1
- package/.output/ui/assets/ResponseView-AdIXGcSn.js +2 -0
- package/.output/ui/assets/StreamingChunkSequence-CesTz8He.js +1 -0
- package/.output/{public/assets/_sessionId-zbgYl7ZJ.js → ui/assets/_sessionId-BAfzrhSU.js} +1 -1
- package/.output/{public/assets/_sessionId-BJlM3nmt.js → ui/assets/_sessionId-QnBomgPD.js} +1 -1
- package/.output/ui/assets/favicon-DYEkHSNl.svg +21 -0
- package/.output/{public/assets/index-BIK4bPTv.js → ui/assets/index-BcsdxBAd.js} +1 -1
- package/.output/ui/assets/index-BvXp42al.css +1 -0
- package/.output/{public/assets/index-D2Qw6Ri2.js → ui/assets/index-D0rtCN9V.js} +1 -1
- package/.output/ui/assets/index-DHpwr08Z.js +73 -0
- package/.output/{public/assets/index-xF_NY8Yb.js → ui/assets/index-dNuk2dsU.js} +5 -5
- package/.output/ui/assets/json-viewer-BvFHglMb.js +1 -0
- package/.output/{public/assets/jszip.min-B_rfAUCe.js → ui/assets/jszip.min-B5Z7gNZB.js} +1 -1
- package/.output/ui/index.html +229 -0
- package/.output/ui/runtime-config.js +2 -0
- package/.output/workers/logFinalizer.worker.js +979 -414
- package/.output/workers/sessionWorkerEntry.js +979 -414
- package/README.md +175 -43
- package/docs/instance-control.openapi.yaml +59 -4
- package/package.json +25 -5
- package/scripts/generate-theme-scales.mjs +3 -3
- package/src/{router.tsx → backend/router.tsx} +3 -15
- package/src/backend/routes/$.ts +26 -0
- package/src/backend/routes/__root.ts +4 -0
- package/src/{routes → backend/routes}/api/-instances.ts +3 -3
- package/src/{routes → backend/routes}/api/alerts.summary.ts +3 -3
- package/src/{routes → backend/routes}/api/alerts.ts +4 -4
- package/src/{routes → backend/routes}/api/config.paths.ts +1 -1
- package/src/{routes → backend/routes}/api/config.ts +11 -2
- package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.help.ts +1 -1
- package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.install.ts +1 -1
- package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.runner-presets.ts +1 -1
- package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.upgrade.ts +1 -1
- package/src/{routes → backend/routes}/api/ecosystem.packages.ts +5 -1
- package/src/{routes → backend/routes}/api/ecosystem.recipes.$recipeId.run.ts +1 -1
- package/src/{routes → backend/routes}/api/ecosystem.tasks.$taskId.ts +1 -1
- package/src/{routes → backend/routes}/api/ecosystem.tasks.ts +1 -1
- package/src/{routes → backend/routes}/api/groups.$groupId.evidence.ts +2 -2
- package/src/{routes → backend/routes}/api/groups.$groupId.sessions.ts +2 -2
- package/src/{routes → backend/routes}/api/groups.$groupId.ts +2 -2
- package/src/{routes → backend/routes}/api/groups.ts +2 -2
- package/src/{routes → backend/routes}/api/health.ts +3 -3
- package/src/{routes → backend/routes}/api/instances.$name.connection.ts +1 -1
- package/src/{routes → backend/routes}/api/instances.$name.restart.ts +2 -2
- package/src/{routes → backend/routes}/api/instances.$name.start.ts +2 -2
- package/src/{routes → backend/routes}/api/instances.$name.stop.ts +2 -2
- package/src/{routes → backend/routes}/api/instances.$name.ts +1 -1
- package/src/{routes → backend/routes}/api/instances.ts +1 -1
- package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.promote.ts +2 -2
- package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.ts +2 -2
- package/src/{routes → backend/routes}/api/knowledge.candidates.ts +1 -1
- package/src/{routes → backend/routes}/api/knowledge.project-context.ts +2 -2
- package/src/{routes → backend/routes}/api/knowledge.search.ts +2 -2
- package/src/{routes → backend/routes}/api/knowledge.sessions.$sessionId.candidates.ts +3 -3
- package/src/{routes → backend/routes}/api/logs.$id.body.ts +6 -6
- package/src/{routes → backend/routes}/api/logs.$id.chunks.ts +5 -4
- package/src/{routes → backend/routes}/api/logs.$id.replay.ts +12 -11
- package/src/{routes → backend/routes}/api/logs.$id.ts +4 -3
- package/src/{routes → backend/routes}/api/logs.import.ts +3 -3
- package/src/{routes → backend/routes}/api/logs.stream.ts +11 -7
- package/src/{routes → backend/routes}/api/logs.ts +17 -9
- package/src/{routes → backend/routes}/api/mcp.ts +1 -1
- package/src/{routes → backend/routes}/api/models.ts +1 -1
- package/src/{routes → backend/routes}/api/pi-agent.ts +2 -2
- package/src/{routes → backend/routes}/api/providers.$providerId.model-metadata.ts +5 -5
- package/src/{routes → backend/routes}/api/providers.$providerId.test.log.ts +7 -7
- package/src/{routes → backend/routes}/api/providers.$providerId.ts +4 -4
- package/src/{routes → backend/routes}/api/providers.export.ts +1 -1
- package/src/{routes → backend/routes}/api/providers.import.ts +3 -3
- package/src/{routes → backend/routes}/api/providers.scan.ts +2 -2
- package/src/{routes → backend/routes}/api/providers.ts +9 -4
- package/src/{routes → backend/routes}/api/runs.$runId.evidence.ts +2 -2
- package/src/{routes → backend/routes}/api/runs.$runId.ts +2 -2
- package/src/{routes → backend/routes}/api/runs.ts +4 -4
- package/src/backend/routes/api/runtime.ts +46 -0
- package/src/{routes → backend/routes}/api/sessions.ts +3 -3
- package/src/{routes → backend/routes}/proxy/$.ts +1 -1
- package/src/{routes → backend/routes}/readyz.ts +1 -1
- package/src/{start.ts → backend/start.ts} +1 -1
- package/src/cli/alias.ts +2 -11
- package/src/cli/doctor.ts +32 -9
- package/src/cli/instance.ts +26 -13
- package/src/cli/instanceArgs.ts +30 -11
- package/src/cli/instanceControl.ts +0 -2
- package/src/cli/instanceModel.ts +9 -17
- package/src/cli/instanceRegistry.ts +0 -9
- package/src/cli/networkHints.ts +27 -14
- package/src/cli/onboard.ts +69 -2
- package/src/cli/onboardTarget.ts +89 -40
- package/src/cli/templates/codex-skill-onboard.ts +44 -14
- package/src/cli/templates/skill-onboard.ts +64 -28
- package/src/cli.ts +184 -61
- package/src/components/ProxyViewer.tsx +11 -9
- package/src/components/ProxyViewerContainer.tsx +12 -11
- package/src/components/pi-agent/PiAgentPanel.tsx +2 -2
- package/src/components/providers/ProviderCard.tsx +1 -1
- package/src/components/providers/ProviderTestResultsView.tsx +1 -1
- package/src/components/providers/SettingsDialog.tsx +159 -144
- package/src/components/proxy-viewer/ApplicationBar.tsx +2 -1
- package/src/components/proxy-viewer/LogEntry.tsx +1 -1
- package/src/components/proxy-viewer/log-formats/index.ts +1 -1
- package/src/contracts/index.ts +5 -0
- package/src/contracts/log.ts +33 -3
- package/src/knowledge/types.ts +3 -2
- package/src/lib/alertContract.ts +2 -2
- package/src/lib/apiClient.ts +30 -7
- package/src/lib/apiFormat.ts +29 -0
- package/src/lib/basePath.ts +29 -11
- package/src/lib/browserRuntimeContract.ts +124 -0
- package/src/lib/browserRuntimeResolver.ts +244 -0
- package/src/{proxy → lib}/claudeCodeStrip.ts +6 -7
- package/src/lib/codingAgentEndpoints.ts +38 -0
- package/src/lib/instanceContract.ts +27 -11
- package/src/lib/piAgentContract.ts +3 -2
- package/src/lib/publicBasePathContext.ts +40 -10
- package/src/lib/resourceLimits.ts +2 -0
- package/src/lib/runContract.ts +3 -3
- package/src/lib/sessionInfoContract.ts +3 -2
- package/src/mcp/instanceHandlers.ts +1 -0
- package/src/mcp/server.ts +19 -24
- package/src/proxy/config.ts +25 -30
- package/src/proxy/dataDir.ts +43 -7
- package/src/proxy/ecosystemTasks.ts +161 -70
- package/src/proxy/formats/registry.ts +6 -21
- package/src/proxy/handler.ts +129 -29
- package/src/proxy/identityProxy.ts +419 -39
- package/src/proxy/jsonlRecovery.ts +554 -0
- package/src/proxy/logFinalizer.ts +25 -10
- package/src/proxy/logImporter.ts +2 -1
- package/src/proxy/logIndex.ts +191 -95
- package/src/proxy/logger.ts +77 -14
- package/src/proxy/platformCommands.ts +41 -0
- package/src/proxy/privateDataPath.ts +310 -88
- package/src/proxy/rawStreamCapture.ts +194 -13
- package/src/proxy/runtimeAdmission.ts +5 -2
- package/src/proxy/runtimeHealth.ts +14 -1
- package/src/proxy/runtimeShutdown.ts +11 -1
- package/src/proxy/schemas.ts +7 -1
- package/src/proxy/sessionArchive.ts +78 -14
- package/src/proxy/sqliteLogIndex.ts +65 -2
- package/src/proxy/store.ts +141 -34
- package/src/proxy/uiAssetServer.ts +106 -0
- package/src/proxy/upstream.ts +29 -2
- package/src/ui/bootstrap.ts +167 -0
- package/src/ui/index.html +38 -0
- package/src/ui/logsStreamClient.ts +20 -0
- package/src/ui/main.tsx +13 -0
- package/src/ui/public/runtime-config.js +2 -0
- package/src/ui/router.tsx +13 -0
- package/src/ui/routes/__root.tsx +69 -0
- package/src/{routes → ui/routes}/index.tsx +2 -3
- package/src/{routes → ui/routes}/session/$sessionId.tsx +5 -11
- package/src/ui/runtimeDiscovery.ts +50 -0
- package/styles/globals.css +1 -0
- package/.output/public/assets/ProxyViewerContainer-BEAhuGOJ.js +0 -126
- package/.output/public/assets/ResponseView-DBCjkwye.js +0 -2
- package/.output/public/assets/StreamingChunkSequence-CuXhDgMo.js +0 -1
- package/.output/public/assets/index-BZo_8koU.js +0 -70
- package/.output/public/assets/index-ZWschkCh.css +0 -1
- package/.output/public/assets/json-viewer-B-Ao6pwW.js +0 -1
- package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +0 -1
- package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +0 -344
- package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +0 -2
- package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +0 -2
- package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +0 -2
- package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +0 -11
- package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +0 -69
- package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +0 -1
- package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +0 -28
- package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +0 -6
- package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +0 -34
- package/.output/server/_libs/aria-hidden.mjs +0 -122
- package/.output/server/_libs/bail.mjs +0 -8
- package/.output/server/_libs/character-entities.mjs +0 -2130
- package/.output/server/_libs/class-variance-authority.mjs +0 -44
- package/.output/server/_libs/clsx.mjs +0 -16
- package/.output/server/_libs/comma-separated-tokens.mjs +0 -10
- package/.output/server/_libs/decode-named-character-reference+[...].mjs +0 -8
- package/.output/server/_libs/dequal.mjs +0 -27
- package/.output/server/_libs/detect-node-es.mjs +0 -1
- package/.output/server/_libs/devlop.mjs +0 -8
- package/.output/server/_libs/diff.mjs +0 -320
- package/.output/server/_libs/estree-util-is-identifier-name.mjs +0 -11
- package/.output/server/_libs/floating-ui__core.mjs +0 -725
- package/.output/server/_libs/floating-ui__dom.mjs +0 -622
- package/.output/server/_libs/floating-ui__react-dom.mjs +0 -292
- package/.output/server/_libs/floating-ui__utils.mjs +0 -320
- package/.output/server/_libs/get-nonce.mjs +0 -9
- package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
- package/.output/server/_libs/hast-util-whitespace.mjs +0 -10
- package/.output/server/_libs/html-url-attributes.mjs +0 -26
- package/.output/server/_libs/inline-style-parser.mjs +0 -142
- package/.output/server/_libs/is-plain-obj.mjs +0 -10
- package/.output/server/_libs/lucide-react.mjs +0 -1027
- package/.output/server/_libs/mdast-util-from-markdown.mjs +0 -717
- package/.output/server/_libs/mdast-util-to-hast.mjs +0 -710
- package/.output/server/_libs/mdast-util-to-string.mjs +0 -38
- package/.output/server/_libs/micromark-core-commonmark.mjs +0 -2259
- package/.output/server/_libs/micromark-factory-destination.mjs +0 -94
- package/.output/server/_libs/micromark-factory-label.mjs +0 -63
- package/.output/server/_libs/micromark-factory-space.mjs +0 -24
- package/.output/server/_libs/micromark-factory-title.mjs +0 -65
- package/.output/server/_libs/micromark-factory-whitespace.mjs +0 -22
- package/.output/server/_libs/micromark-util-character.mjs +0 -44
- package/.output/server/_libs/micromark-util-chunked.mjs +0 -36
- package/.output/server/_libs/micromark-util-classify-character+[...].mjs +0 -12
- package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
- package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
- package/.output/server/_libs/micromark-util-decode-string.mjs +0 -21
- package/.output/server/_libs/micromark-util-encode.mjs +0 -1
- package/.output/server/_libs/micromark-util-html-tag-name.mjs +0 -69
- package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
- package/.output/server/_libs/micromark-util-resolve-all.mjs +0 -15
- package/.output/server/_libs/micromark-util-sanitize-uri.mjs +0 -41
- package/.output/server/_libs/micromark-util-subtokenize.mjs +0 -346
- package/.output/server/_libs/micromark.mjs +0 -906
- package/.output/server/_libs/property-information.mjs +0 -1209
- package/.output/server/_libs/radix-ui.mjs +0 -1
- package/.output/server/_libs/radix-ui__number.mjs +0 -6
- package/.output/server/_libs/radix-ui__primitive.mjs +0 -11
- package/.output/server/_libs/radix-ui__react-accordion.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
- package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-avatar.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-checkbox.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-collapsible.mjs +0 -144
- package/.output/server/_libs/radix-ui__react-collection.mjs +0 -69
- package/.output/server/_libs/radix-ui__react-compose-refs.mjs +0 -38
- package/.output/server/_libs/radix-ui__react-context-menu.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-context.mjs +0 -64
- package/.output/server/_libs/radix-ui__react-dialog.mjs +0 -280
- package/.output/server/_libs/radix-ui__react-direction.mjs +0 -9
- package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-focus-guards.mjs +0 -39
- package/.output/server/_libs/radix-ui__react-focus-scope.mjs +0 -210
- package/.output/server/_libs/radix-ui__react-form.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-hover-card.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-id.mjs +0 -14
- package/.output/server/_libs/radix-ui__react-label.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-menu.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-menubar.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-popover.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-popper.mjs +0 -340
- package/.output/server/_libs/radix-ui__react-portal.mjs +0 -16
- package/.output/server/_libs/radix-ui__react-presence.mjs +0 -173
- package/.output/server/_libs/radix-ui__react-primitive.mjs +0 -42
- package/.output/server/_libs/radix-ui__react-progress.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-radio-group.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-roving-focus.mjs +0 -235
- package/.output/server/_libs/radix-ui__react-scroll-area.mjs +0 -731
- package/.output/server/_libs/radix-ui__react-select.mjs +0 -1207
- package/.output/server/_libs/radix-ui__react-separator.mjs +0 -28
- package/.output/server/_libs/radix-ui__react-slider.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-slot.mjs +0 -132
- package/.output/server/_libs/radix-ui__react-switch.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-tabs.mjs +0 -194
- package/.output/server/_libs/radix-ui__react-toast.mjs +0 -2
- package/.output/server/_libs/radix-ui__react-toggle-group.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-toggle.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-toolbar.mjs +0 -1
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -498
- package/.output/server/_libs/radix-ui__react-use-previous.mjs +0 -14
- package/.output/server/_libs/radix-ui__react-use-size.mjs +0 -39
- package/.output/server/_libs/react-markdown.mjs +0 -235
- package/.output/server/_libs/react-remove-scroll-bar.mjs +0 -82
- package/.output/server/_libs/react-remove-scroll.mjs +0 -328
- package/.output/server/_libs/react-style-singleton.mjs +0 -69
- package/.output/server/_libs/remark-parse.mjs +0 -19
- package/.output/server/_libs/remark-rehype.mjs +0 -21
- package/.output/server/_libs/space-separated-tokens.mjs +0 -6
- package/.output/server/_libs/style-to-js.mjs +0 -72
- package/.output/server/_libs/style-to-object.mjs +0 -38
- package/.output/server/_libs/swr.mjs +0 -947
- package/.output/server/_libs/tailwind-merge.mjs +0 -3255
- package/.output/server/_libs/tanstack__react-virtual.mjs +0 -130
- package/.output/server/_libs/tanstack__virtual-core.mjs +0 -1319
- package/.output/server/_libs/three.mjs +0 -26016
- package/.output/server/_libs/trim-lines.mjs +0 -41
- package/.output/server/_libs/trough.mjs +0 -85
- package/.output/server/_libs/tslib.mjs +0 -1
- package/.output/server/_libs/ungap__structured-clone.mjs +0 -212
- package/.output/server/_libs/unified.mjs +0 -661
- package/.output/server/_libs/unist-util-is.mjs +0 -100
- package/.output/server/_libs/unist-util-position.mjs +0 -27
- package/.output/server/_libs/unist-util-stringify-position.mjs +0 -27
- package/.output/server/_libs/unist-util-visit-parents.mjs +0 -82
- package/.output/server/_libs/unist-util-visit.mjs +0 -24
- package/.output/server/_libs/use-callback-ref.mjs +0 -66
- package/.output/server/_libs/use-sidecar.mjs +0 -106
- package/.output/server/_libs/vfile-message.mjs +0 -138
- package/.output/server/_libs/vfile.mjs +0 -467
- package/.output/server/_sessionId-CJ99gZFV.mjs +0 -84
- package/.output/server/_sessionId-DEjScBpK.mjs +0 -146
- package/.output/server/_ssr/CompareDrawer-DtOIMeQB.mjs +0 -1060
- package/.output/server/_ssr/InspectorPet-CXc4oCBQ.mjs +0 -938
- package/.output/server/_ssr/ProxyViewerContainer-DVPlwfkt.mjs +0 -17632
- package/.output/server/_ssr/ReplayDialog-BQEA95qC.mjs +0 -725
- package/.output/server/_ssr/RequestAnatomy-BMyd8B7Q.mjs +0 -837
- package/.output/server/_ssr/ResponseView-qrTaEIM8.mjs +0 -757
- package/.output/server/_ssr/StreamingChunkSequence-BFTL8mIy.mjs +0 -274
- package/.output/server/_ssr/index-Bcj1Hcy4.mjs +0 -135
- package/.output/server/_ssr/index-DW-M7XUG.mjs +0 -84
- package/.output/server/_ssr/json-viewer-DiI0Yjsn.mjs +0 -483
- package/.output/server/_tanstack-start-manifest_v-C6pXXX9R.mjs +0 -4
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/.output/server/package.json +0 -9
- package/src/routes/__root.tsx +0 -170
- /package/.output/{public → ui}/assets/agent-inspector-58K1_MsC.ico +0 -0
- /package/.output/{public → ui}/assets/alibaba-TTwafVwX.svg +0 -0
- /package/.output/{public → ui}/assets/minimax-BPMzvuL-.jpeg +0 -0
- /package/.output/{public → ui}/assets/qwen-mMn3f5ul.webp +0 -0
- /package/.output/{public → ui}/assets/zhipuai-BPNAnxo-.svg +0 -0
- /package/src/{routes → backend/routes}/livez.ts +0 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { constants, existsSync, lstatSync } from "node:fs";
|
|
4
|
+
import { link, lstat, open, readFile, readdir, stat, unlink } from "node:fs/promises";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { CapturedLogSchema } from "./schemas";
|
|
7
|
+
import {
|
|
8
|
+
ensurePrivateDirectorySync,
|
|
9
|
+
PRIVATE_FILE_MODE,
|
|
10
|
+
securePrivateFileSync,
|
|
11
|
+
} from "./privateDataPath";
|
|
12
|
+
|
|
13
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
14
|
+
const QUARANTINE_DIRECTORY = "quarantine";
|
|
15
|
+
|
|
16
|
+
export type CapturedLogRecoveryLevel = "ok" | "degraded" | "unavailable";
|
|
17
|
+
|
|
18
|
+
export type CapturedLogRecoveryHealth = {
|
|
19
|
+
level: CapturedLogRecoveryLevel;
|
|
20
|
+
diagnostic: string | null;
|
|
21
|
+
repairedFiles: number;
|
|
22
|
+
delimiterRepairs: number;
|
|
23
|
+
quarantinedTails: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type FileSignature = {
|
|
27
|
+
size: number;
|
|
28
|
+
modifiedAt: number;
|
|
29
|
+
changedAt: number;
|
|
30
|
+
device: number;
|
|
31
|
+
inode: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type RecoveryContext = {
|
|
35
|
+
root: string;
|
|
36
|
+
health: CapturedLogRecoveryHealth;
|
|
37
|
+
startup: Promise<CapturedLogRecoveryHealth> | null;
|
|
38
|
+
startupComplete: boolean;
|
|
39
|
+
safeFiles: Map<string, FileSignature>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type RecordValidation =
|
|
43
|
+
| { valid: true }
|
|
44
|
+
| { valid: false; reason: "invalid-json" | "invalid-log-schema" };
|
|
45
|
+
|
|
46
|
+
type InvalidRecordReason = "invalid-json" | "invalid-log-schema";
|
|
47
|
+
|
|
48
|
+
type FileRecoveryResult =
|
|
49
|
+
| { status: "clean"; signature: FileSignature }
|
|
50
|
+
| { status: "delimiter-appended"; signature: FileSignature }
|
|
51
|
+
| { status: "tail-quarantined"; signature: FileSignature }
|
|
52
|
+
| {
|
|
53
|
+
status: "interior-corruption";
|
|
54
|
+
line: number;
|
|
55
|
+
byteOffset: number;
|
|
56
|
+
recordHash: string;
|
|
57
|
+
reason: InvalidRecordReason;
|
|
58
|
+
}
|
|
59
|
+
| { status: "failed"; reason: string };
|
|
60
|
+
|
|
61
|
+
type ScanResult =
|
|
62
|
+
| {
|
|
63
|
+
status: "complete";
|
|
64
|
+
pending: Buffer;
|
|
65
|
+
pendingOffset: number;
|
|
66
|
+
nextLine: number;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
status: "interior-corruption";
|
|
70
|
+
line: number;
|
|
71
|
+
byteOffset: number;
|
|
72
|
+
recordHash: string;
|
|
73
|
+
reason: InvalidRecordReason;
|
|
74
|
+
}
|
|
75
|
+
| { status: "failed"; reason: string };
|
|
76
|
+
|
|
77
|
+
type QuarantineRecord = {
|
|
78
|
+
schemaVersion: 1;
|
|
79
|
+
reason: "invalid-final-jsonl-record";
|
|
80
|
+
sourceFile: string;
|
|
81
|
+
sourcePathHash: string;
|
|
82
|
+
recoveredAt: string;
|
|
83
|
+
tailOffset: number;
|
|
84
|
+
tailBytes: number;
|
|
85
|
+
tailSha256: string;
|
|
86
|
+
tailBase64: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type ExistingQuarantineState = "missing" | "matching" | "conflicting";
|
|
90
|
+
|
|
91
|
+
let activeContext: RecoveryContext | null = null;
|
|
92
|
+
|
|
93
|
+
function emptyHealth(): CapturedLogRecoveryHealth {
|
|
94
|
+
return {
|
|
95
|
+
level: "ok",
|
|
96
|
+
diagnostic: null,
|
|
97
|
+
repairedFiles: 0,
|
|
98
|
+
delimiterRepairs: 0,
|
|
99
|
+
quarantinedTails: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function contextFor(logDir: string): RecoveryContext {
|
|
104
|
+
const root = resolve(logDir);
|
|
105
|
+
if (activeContext === null || activeContext.root !== root) {
|
|
106
|
+
activeContext = {
|
|
107
|
+
root,
|
|
108
|
+
health: emptyHealth(),
|
|
109
|
+
startup: null,
|
|
110
|
+
startupComplete: false,
|
|
111
|
+
safeFiles: new Map(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return activeContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function copyHealth(health: CapturedLogRecoveryHealth): CapturedLogRecoveryHealth {
|
|
118
|
+
return { ...health };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hashBytes(value: Uint8Array): string {
|
|
122
|
+
return createHash("sha256").update(value).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function signatureFromStats(stats: {
|
|
126
|
+
size: number;
|
|
127
|
+
mtimeMs: number;
|
|
128
|
+
ctimeMs: number;
|
|
129
|
+
dev: number;
|
|
130
|
+
ino: number;
|
|
131
|
+
}): FileSignature {
|
|
132
|
+
return {
|
|
133
|
+
size: stats.size,
|
|
134
|
+
modifiedAt: stats.mtimeMs,
|
|
135
|
+
changedAt: stats.ctimeMs,
|
|
136
|
+
device: stats.dev,
|
|
137
|
+
inode: stats.ino,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function signaturesMatch(left: FileSignature, right: FileSignature): boolean {
|
|
142
|
+
return (
|
|
143
|
+
left.size === right.size &&
|
|
144
|
+
left.modifiedAt === right.modifiedAt &&
|
|
145
|
+
left.changedAt === right.changedAt &&
|
|
146
|
+
left.device === right.device &&
|
|
147
|
+
left.inode === right.inode
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateRecord(bytes: Buffer): RecordValidation {
|
|
152
|
+
const withoutCarriageReturn =
|
|
153
|
+
bytes.length > 0 && bytes[bytes.length - 1] === 0x0d ? bytes.subarray(0, -1) : bytes;
|
|
154
|
+
const text = withoutCarriageReturn.toString("utf-8");
|
|
155
|
+
if (text.trim() === "") return { valid: true };
|
|
156
|
+
|
|
157
|
+
let parsed: unknown;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(text);
|
|
160
|
+
} catch {
|
|
161
|
+
return { valid: false, reason: "invalid-json" };
|
|
162
|
+
}
|
|
163
|
+
return CapturedLogSchema.safeParse(parsed).success
|
|
164
|
+
? { valid: true }
|
|
165
|
+
: { valid: false, reason: "invalid-log-schema" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function scanFile(
|
|
169
|
+
handle: Awaited<ReturnType<typeof open>>,
|
|
170
|
+
size: number,
|
|
171
|
+
): Promise<ScanResult> {
|
|
172
|
+
const readBuffer = Buffer.alloc(READ_CHUNK_BYTES);
|
|
173
|
+
let position = 0;
|
|
174
|
+
let pending = Buffer.alloc(0);
|
|
175
|
+
let pendingOffset = 0;
|
|
176
|
+
let line = 1;
|
|
177
|
+
|
|
178
|
+
while (position < size) {
|
|
179
|
+
const length = Math.min(READ_CHUNK_BYTES, size - position);
|
|
180
|
+
let bytesRead: number;
|
|
181
|
+
try {
|
|
182
|
+
const result = await handle.read(readBuffer, 0, length, position);
|
|
183
|
+
bytesRead = result.bytesRead;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return { status: "failed", reason: `read failed: ${String(error)}` };
|
|
186
|
+
}
|
|
187
|
+
if (bytesRead === 0) {
|
|
188
|
+
return { status: "failed", reason: "captured-log file changed while being scanned" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const chunk = readBuffer.subarray(0, bytesRead);
|
|
192
|
+
const combined = pending.length === 0 ? Buffer.from(chunk) : Buffer.concat([pending, chunk]);
|
|
193
|
+
let consumed = 0;
|
|
194
|
+
let newline = combined.indexOf(0x0a, consumed);
|
|
195
|
+
while (newline !== -1) {
|
|
196
|
+
const record = combined.subarray(consumed, newline);
|
|
197
|
+
const validation = validateRecord(record);
|
|
198
|
+
if (!validation.valid) {
|
|
199
|
+
return {
|
|
200
|
+
status: "interior-corruption",
|
|
201
|
+
line,
|
|
202
|
+
byteOffset: pendingOffset + consumed,
|
|
203
|
+
recordHash: hashBytes(record),
|
|
204
|
+
reason: validation.reason,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
consumed = newline + 1;
|
|
208
|
+
line += 1;
|
|
209
|
+
newline = combined.indexOf(0x0a, consumed);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
pending = Buffer.from(combined.subarray(consumed));
|
|
213
|
+
pendingOffset += consumed;
|
|
214
|
+
position += bytesRead;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { status: "complete", pending, pendingOffset, nextLine: line };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function syncDirectory(directory: string): Promise<void> {
|
|
221
|
+
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
222
|
+
try {
|
|
223
|
+
handle = await open(directory, "r");
|
|
224
|
+
await handle.sync();
|
|
225
|
+
} catch {
|
|
226
|
+
// Directory fsync is unavailable on some supported Windows filesystems.
|
|
227
|
+
} finally {
|
|
228
|
+
await handle?.close().catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function removeTemporaryFile(filePath: string): Promise<void> {
|
|
233
|
+
await unlink(filePath).catch(() => undefined);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readOwnProperty(value: unknown, name: string): unknown {
|
|
237
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
238
|
+
return Object.getOwnPropertyDescriptor(value, name)?.value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function inspectExistingQuarantine(
|
|
242
|
+
filePath: string,
|
|
243
|
+
expected: QuarantineRecord,
|
|
244
|
+
): Promise<ExistingQuarantineState> {
|
|
245
|
+
if (!existsSync(filePath)) return "missing";
|
|
246
|
+
try {
|
|
247
|
+
const parsed: unknown = JSON.parse(await readFile(filePath, "utf-8"));
|
|
248
|
+
return readOwnProperty(parsed, "schemaVersion") === expected.schemaVersion &&
|
|
249
|
+
readOwnProperty(parsed, "reason") === expected.reason &&
|
|
250
|
+
readOwnProperty(parsed, "sourceFile") === expected.sourceFile &&
|
|
251
|
+
readOwnProperty(parsed, "sourcePathHash") === expected.sourcePathHash &&
|
|
252
|
+
readOwnProperty(parsed, "tailOffset") === expected.tailOffset &&
|
|
253
|
+
readOwnProperty(parsed, "tailBytes") === expected.tailBytes &&
|
|
254
|
+
readOwnProperty(parsed, "tailSha256") === expected.tailSha256 &&
|
|
255
|
+
readOwnProperty(parsed, "tailBase64") === expected.tailBase64
|
|
256
|
+
? "matching"
|
|
257
|
+
: "conflicting";
|
|
258
|
+
} catch {
|
|
259
|
+
return "conflicting";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function quarantineTail(
|
|
264
|
+
context: RecoveryContext,
|
|
265
|
+
sourceFile: string,
|
|
266
|
+
tail: Buffer,
|
|
267
|
+
tailOffset: number,
|
|
268
|
+
): Promise<string | null> {
|
|
269
|
+
const quarantineDirectory = join(context.root, QUARANTINE_DIRECTORY);
|
|
270
|
+
if (!ensurePrivateDirectorySync(quarantineDirectory)) return null;
|
|
271
|
+
|
|
272
|
+
const tailSha256 = hashBytes(tail);
|
|
273
|
+
const sourceFileName = basename(sourceFile);
|
|
274
|
+
const destination = join(quarantineDirectory, `${sourceFileName}.tail-${tailSha256}.json`);
|
|
275
|
+
const temporary = join(
|
|
276
|
+
quarantineDirectory,
|
|
277
|
+
`.${sourceFileName}.${String(process.pid)}.${randomUUID()}.tmp`,
|
|
278
|
+
);
|
|
279
|
+
const record: QuarantineRecord = {
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
reason: "invalid-final-jsonl-record",
|
|
282
|
+
sourceFile: sourceFileName,
|
|
283
|
+
sourcePathHash: hashBytes(Buffer.from(resolve(sourceFile), "utf-8")),
|
|
284
|
+
recoveredAt: new Date().toISOString(),
|
|
285
|
+
tailOffset,
|
|
286
|
+
tailBytes: tail.length,
|
|
287
|
+
tailSha256,
|
|
288
|
+
tailBase64: tail.toString("base64"),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const existing = await inspectExistingQuarantine(destination, record);
|
|
292
|
+
if (existing === "matching") {
|
|
293
|
+
return securePrivateFileSync(destination) ? destination : null;
|
|
294
|
+
}
|
|
295
|
+
if (existing === "conflicting") return null;
|
|
296
|
+
|
|
297
|
+
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
298
|
+
try {
|
|
299
|
+
handle = await open(temporary, "wx", PRIVATE_FILE_MODE);
|
|
300
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`, "utf-8");
|
|
301
|
+
await handle.sync();
|
|
302
|
+
await handle.close();
|
|
303
|
+
handle = null;
|
|
304
|
+
if (!securePrivateFileSync(temporary)) {
|
|
305
|
+
await removeTemporaryFile(temporary);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
// A hard-link install has create-if-absent semantics on every supported platform. Unlike
|
|
309
|
+
// rename(), it cannot overwrite a conflicting quarantine artifact that appears after the
|
|
310
|
+
// preflight check on POSIX.
|
|
311
|
+
await link(temporary, destination);
|
|
312
|
+
await unlink(temporary);
|
|
313
|
+
if (!securePrivateFileSync(destination)) return null;
|
|
314
|
+
await syncDirectory(quarantineDirectory);
|
|
315
|
+
return destination;
|
|
316
|
+
} catch {
|
|
317
|
+
await handle?.close().catch(() => undefined);
|
|
318
|
+
await removeTemporaryFile(temporary);
|
|
319
|
+
const racedExisting = await inspectExistingQuarantine(destination, record);
|
|
320
|
+
return racedExisting === "matching" && securePrivateFileSync(destination) ? destination : null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function recoverFile(
|
|
325
|
+
context: RecoveryContext,
|
|
326
|
+
filePath: string,
|
|
327
|
+
): Promise<FileRecoveryResult> {
|
|
328
|
+
if (!existsSync(filePath)) {
|
|
329
|
+
context.safeFiles.delete(filePath);
|
|
330
|
+
return {
|
|
331
|
+
status: "clean",
|
|
332
|
+
signature: { size: 0, modifiedAt: 0, changedAt: 0, device: 0, inode: 0 },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let fileStats: Awaited<ReturnType<typeof lstat>>;
|
|
337
|
+
try {
|
|
338
|
+
fileStats = await lstat(filePath);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return { status: "failed", reason: `stat failed: ${String(error)}` };
|
|
341
|
+
}
|
|
342
|
+
if (fileStats.isSymbolicLink() || !fileStats.isFile()) {
|
|
343
|
+
return { status: "failed", reason: "captured-log path is not a regular private file" };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const initialSignature = signatureFromStats(fileStats);
|
|
347
|
+
const safeSignature = context.safeFiles.get(filePath);
|
|
348
|
+
if (safeSignature !== undefined && signaturesMatch(safeSignature, initialSignature)) {
|
|
349
|
+
return { status: "clean", signature: initialSignature };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
353
|
+
try {
|
|
354
|
+
handle = await open(filePath, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
355
|
+
const openedStats = await handle.stat();
|
|
356
|
+
if (!openedStats.isFile()) {
|
|
357
|
+
return { status: "failed", reason: "captured-log handle is not a regular file" };
|
|
358
|
+
}
|
|
359
|
+
const openedSignature = signatureFromStats(openedStats);
|
|
360
|
+
if (!signaturesMatch(initialSignature, openedSignature)) {
|
|
361
|
+
return {
|
|
362
|
+
status: "failed",
|
|
363
|
+
reason: "captured-log path changed between metadata validation and open",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const scan = await scanFile(handle, openedStats.size);
|
|
367
|
+
const scannedSignature = signatureFromStats(await handle.stat());
|
|
368
|
+
if (!signaturesMatch(openedSignature, scannedSignature)) {
|
|
369
|
+
return {
|
|
370
|
+
status: "failed",
|
|
371
|
+
reason: "captured-log file changed while recovery was scanning it",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
switch (scan.status) {
|
|
375
|
+
case "failed":
|
|
376
|
+
return scan;
|
|
377
|
+
case "interior-corruption":
|
|
378
|
+
return scan;
|
|
379
|
+
case "complete": {
|
|
380
|
+
if (scan.pending.length === 0) {
|
|
381
|
+
return { status: "clean", signature: signatureFromStats(await handle.stat()) };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const validation = validateRecord(scan.pending);
|
|
385
|
+
if (validation.valid) {
|
|
386
|
+
const write = await handle.write(Buffer.from("\n", "utf-8"), 0, 1, openedStats.size);
|
|
387
|
+
if (write.bytesWritten !== 1) {
|
|
388
|
+
return { status: "failed", reason: "captured-log delimiter append was incomplete" };
|
|
389
|
+
}
|
|
390
|
+
await handle.sync();
|
|
391
|
+
return {
|
|
392
|
+
status: "delimiter-appended",
|
|
393
|
+
signature: signatureFromStats(await handle.stat()),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const quarantinePath = await quarantineTail(
|
|
398
|
+
context,
|
|
399
|
+
filePath,
|
|
400
|
+
scan.pending,
|
|
401
|
+
scan.pendingOffset,
|
|
402
|
+
);
|
|
403
|
+
if (quarantinePath === null) {
|
|
404
|
+
return { status: "failed", reason: "invalid tail could not be privately quarantined" };
|
|
405
|
+
}
|
|
406
|
+
const beforeTruncate = signatureFromStats(await handle.stat());
|
|
407
|
+
if (!signaturesMatch(openedSignature, beforeTruncate)) {
|
|
408
|
+
return {
|
|
409
|
+
status: "failed",
|
|
410
|
+
reason: "captured-log file changed before invalid-tail truncation",
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
await handle.truncate(scan.pendingOffset);
|
|
414
|
+
await handle.sync();
|
|
415
|
+
return {
|
|
416
|
+
status: "tail-quarantined",
|
|
417
|
+
signature: signatureFromStats(await handle.stat()),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { status: "failed", reason: "unsupported captured-log scan state" };
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return { status: "failed", reason: String(error) };
|
|
424
|
+
} finally {
|
|
425
|
+
await handle?.close().catch(() => undefined);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function applyFileResult(
|
|
430
|
+
context: RecoveryContext,
|
|
431
|
+
filePath: string,
|
|
432
|
+
result: FileRecoveryResult,
|
|
433
|
+
): void {
|
|
434
|
+
const file = basename(filePath);
|
|
435
|
+
switch (result.status) {
|
|
436
|
+
case "clean":
|
|
437
|
+
context.safeFiles.set(filePath, result.signature);
|
|
438
|
+
return;
|
|
439
|
+
case "delimiter-appended":
|
|
440
|
+
context.safeFiles.set(filePath, result.signature);
|
|
441
|
+
context.health.delimiterRepairs += 1;
|
|
442
|
+
context.health.repairedFiles += 1;
|
|
443
|
+
return;
|
|
444
|
+
case "tail-quarantined":
|
|
445
|
+
context.safeFiles.set(filePath, result.signature);
|
|
446
|
+
context.health.quarantinedTails += 1;
|
|
447
|
+
context.health.repairedFiles += 1;
|
|
448
|
+
if (context.health.level !== "unavailable") {
|
|
449
|
+
context.health.level = "degraded";
|
|
450
|
+
context.health.diagnostic = `Recovered and quarantined an invalid JSONL tail in ${file}`;
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
case "interior-corruption":
|
|
454
|
+
context.safeFiles.delete(filePath);
|
|
455
|
+
context.health.level = "unavailable";
|
|
456
|
+
context.health.diagnostic =
|
|
457
|
+
`JSONL corruption in ${file} at line ${String(result.line)} ` +
|
|
458
|
+
`(offset ${String(result.byteOffset)}, ${result.reason}, sha256 ${result.recordHash})`;
|
|
459
|
+
return;
|
|
460
|
+
case "failed":
|
|
461
|
+
context.safeFiles.delete(filePath);
|
|
462
|
+
context.health.level = "unavailable";
|
|
463
|
+
context.health.diagnostic = `JSONL recovery failed for ${file}: ${result.reason}`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function runStartupRecovery(context: RecoveryContext): Promise<CapturedLogRecoveryHealth> {
|
|
468
|
+
if (!ensurePrivateDirectorySync(context.root)) {
|
|
469
|
+
context.health.level = "unavailable";
|
|
470
|
+
context.health.diagnostic = "Captured-log directory could not be made private";
|
|
471
|
+
return copyHealth(context.health);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let entries: string[];
|
|
475
|
+
try {
|
|
476
|
+
entries = await readdir(context.root);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
context.health.level = "unavailable";
|
|
479
|
+
context.health.diagnostic = `Captured-log directory could not be listed: ${String(error)}`;
|
|
480
|
+
return copyHealth(context.health);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const files = entries.filter((entry) => entry.endsWith(".jsonl")).sort();
|
|
484
|
+
for (const file of files) {
|
|
485
|
+
const filePath = join(context.root, file);
|
|
486
|
+
const result = await recoverFile(context, filePath);
|
|
487
|
+
applyFileResult(context, filePath, result);
|
|
488
|
+
if (context.health.level === "unavailable") break;
|
|
489
|
+
}
|
|
490
|
+
return copyHealth(context.health);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Recover all captured-log ledgers once before startup reads, indexing, readiness, or appends. */
|
|
494
|
+
export async function recoverCapturedLogStorage(
|
|
495
|
+
logDir: string,
|
|
496
|
+
): Promise<CapturedLogRecoveryHealth> {
|
|
497
|
+
const context = contextFor(logDir);
|
|
498
|
+
if (context.startup === null) {
|
|
499
|
+
context.startup = runStartupRecovery(context).then((health) => {
|
|
500
|
+
context.startupComplete = true;
|
|
501
|
+
return health;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return await context.startup;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Re-check a ledger immediately before appending; unchanged files use a stat-only fast path. */
|
|
508
|
+
export async function recoverCapturedLogFileBeforeAppend(
|
|
509
|
+
logDir: string,
|
|
510
|
+
filePath: string,
|
|
511
|
+
): Promise<CapturedLogRecoveryHealth> {
|
|
512
|
+
const context = contextFor(logDir);
|
|
513
|
+
await recoverCapturedLogStorage(logDir);
|
|
514
|
+
if (context.health.level === "unavailable") return copyHealth(context.health);
|
|
515
|
+
|
|
516
|
+
const result = await recoverFile(context, filePath);
|
|
517
|
+
applyFileResult(context, filePath, result);
|
|
518
|
+
return copyHealth(context.health);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Refresh the trusted signature after this process successfully appends a complete JSONL record. */
|
|
522
|
+
export async function markCapturedLogFileAppended(logDir: string, filePath: string): Promise<void> {
|
|
523
|
+
const context = contextFor(logDir);
|
|
524
|
+
try {
|
|
525
|
+
const fileStats = await stat(filePath);
|
|
526
|
+
context.safeFiles.set(filePath, signatureFromStats(fileStats));
|
|
527
|
+
} catch {
|
|
528
|
+
context.safeFiles.delete(filePath);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function getCapturedLogRecoveryHealth(logDir: string): CapturedLogRecoveryHealth {
|
|
533
|
+
return copyHealth(contextFor(logDir).health);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Fail-closed guard for the process-exit writer, where asynchronous recovery is unavailable. */
|
|
537
|
+
export function canAppendCapturedLogFileOnExit(logDir: string, filePath: string): boolean {
|
|
538
|
+
const context = contextFor(logDir);
|
|
539
|
+
if (!context.startupComplete || context.health.level === "unavailable") return false;
|
|
540
|
+
const trusted = context.safeFiles.get(filePath);
|
|
541
|
+
if (trusted === undefined) return false;
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const observed = lstatSync(filePath);
|
|
545
|
+
if (observed === undefined || observed.isSymbolicLink() || !observed.isFile()) return false;
|
|
546
|
+
return signaturesMatch(trusted, signatureFromStats(observed));
|
|
547
|
+
} catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function _resetCapturedLogRecoveryForTests(): void {
|
|
553
|
+
activeContext = null;
|
|
554
|
+
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
getSessionProcessPendingCount,
|
|
14
14
|
isSessionProcessAvailable,
|
|
15
15
|
} from "./sessionProcess";
|
|
16
|
-
import {
|
|
16
|
+
import { persistFinalizedLogUpdate } from "./store";
|
|
17
17
|
import { computeTokensPerSecond } from "./streamTiming";
|
|
18
18
|
import { analyzeToolSchemaWarnings } from "./toolSchemaWarnings";
|
|
19
19
|
import { isPlainRecord, safeGetOwnProperty } from "../lib/objectUtils";
|
|
@@ -37,6 +37,7 @@ export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
|
|
|
37
37
|
collectStreamingChunks: boolean;
|
|
38
38
|
firstChunkMs: number | null;
|
|
39
39
|
totalStreamMs: number;
|
|
40
|
+
terminalError?: string | null;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
@@ -99,6 +100,10 @@ export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record
|
|
|
99
100
|
clientProjectFolder: log.clientProjectFolder,
|
|
100
101
|
streamingChunks: log.streamingChunks,
|
|
101
102
|
streamingChunksPath: log.streamingChunksPath,
|
|
103
|
+
captureIncomplete: log.captureIncomplete ?? false,
|
|
104
|
+
droppedChunks: log.droppedChunks ?? 0,
|
|
105
|
+
droppedBytes: log.droppedBytes ?? 0,
|
|
106
|
+
captureIncompleteReason: log.captureIncompleteReason ?? null,
|
|
102
107
|
warnings: log.warnings,
|
|
103
108
|
error: log.error,
|
|
104
109
|
};
|
|
@@ -198,7 +203,7 @@ async function runToolSchemaWarningAnalysis(job: ToolSchemaWarningAnalysisJob):
|
|
|
198
203
|
if (updatedLog === null) return;
|
|
199
204
|
|
|
200
205
|
appendLogEntry({ ...buildFileLogEntry(updatedLog, job.upstreamUrl), error: job.error });
|
|
201
|
-
|
|
206
|
+
await persistFinalizedLogUpdate(updatedLog);
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
function scheduleToolSchemaWarningAnalysis(job: ToolSchemaWarningAnalysisJob): void {
|
|
@@ -455,8 +460,13 @@ export function buildFinalizeLogResult(job: FinalizeLogJob): FinalizeLogResult {
|
|
|
455
460
|
switch (job.type) {
|
|
456
461
|
case "non-streaming":
|
|
457
462
|
return finalizeNonStreaming(job, log);
|
|
458
|
-
case "streaming":
|
|
459
|
-
|
|
463
|
+
case "streaming": {
|
|
464
|
+
const result = finalizeStreaming(job, log);
|
|
465
|
+
if (job.terminalError !== undefined && job.terminalError !== null) {
|
|
466
|
+
result.log.error = job.terminalError;
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
460
470
|
case "stream-abort":
|
|
461
471
|
return finalizeStreamAbort(job, log);
|
|
462
472
|
default: {
|
|
@@ -472,13 +482,18 @@ export function buildFinalizeLogResult(job: FinalizeLogJob): FinalizeLogResult {
|
|
|
472
482
|
}
|
|
473
483
|
}
|
|
474
484
|
|
|
475
|
-
export function commitFinalizeLogResult(result: FinalizeLogResult): void {
|
|
476
|
-
|
|
477
|
-
|
|
485
|
+
export async function commitFinalizeLogResult(result: FinalizeLogResult): Promise<void> {
|
|
486
|
+
const persistedError =
|
|
487
|
+
result.log.captureIncompleteReason === "upstream-stream-error"
|
|
488
|
+
? (result.log.error ?? result.error ?? null)
|
|
489
|
+
: (result.error ?? result.log.error ?? null);
|
|
490
|
+
result.log.error = persistedError;
|
|
491
|
+
appendLogEntry({ ...buildFileLogEntry(result.log, result.upstreamUrl), error: persistedError });
|
|
492
|
+
await persistFinalizedLogUpdate(result.log);
|
|
478
493
|
scheduleToolSchemaWarningAnalysis({
|
|
479
494
|
log: cloneLog(result.log),
|
|
480
495
|
upstreamUrl: result.upstreamUrl,
|
|
481
|
-
error:
|
|
496
|
+
error: persistedError,
|
|
482
497
|
});
|
|
483
498
|
if (result.cleanupRawStreamPath !== undefined && result.cleanupRawStreamPath !== null) {
|
|
484
499
|
try {
|
|
@@ -665,8 +680,8 @@ export function executeBuildInWorker(job: FinalizeLogJob): Promise<FinalizeLogRe
|
|
|
665
680
|
}
|
|
666
681
|
|
|
667
682
|
export function executeFinalizeLogJob(job: FinalizeLogJob): Promise<void> {
|
|
668
|
-
return resolveBuildPromise(job).then((result) => {
|
|
669
|
-
commitFinalizeLogResult(result);
|
|
683
|
+
return resolveBuildPromise(job).then(async (result) => {
|
|
684
|
+
await commitFinalizeLogResult(result);
|
|
670
685
|
if (result.error !== null && result.error !== "Client aborted") {
|
|
671
686
|
return Promise.reject(new Error(result.error));
|
|
672
687
|
}
|
package/src/proxy/logImporter.ts
CHANGED
|
@@ -4,6 +4,7 @@ import JSZip from "jszip";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import {
|
|
6
6
|
CapturedLogSchema,
|
|
7
|
+
LogIdSchema,
|
|
7
8
|
StreamingChunkSchema,
|
|
8
9
|
type ApiFormat,
|
|
9
10
|
type CapturedLog,
|
|
@@ -58,7 +59,7 @@ const ManifestSchema = z
|
|
|
58
59
|
exportedAt: z.string().optional(),
|
|
59
60
|
mode: z.string().optional(),
|
|
60
61
|
redacted: z.boolean().optional(),
|
|
61
|
-
logIds: z.array(
|
|
62
|
+
logIds: z.array(LogIdSchema).optional(),
|
|
62
63
|
})
|
|
63
64
|
.passthrough();
|
|
64
65
|
|