diff --git a/index.js b/index.js index 7f78560..24295cd 100644 --- a/index.js +++ b/index.js @@ -388,6 +388,7 @@ import { import { buildAuthorityDiagnosticsBundle, buildAuthorityDiagnosticsBundlePath, + buildAuthorityPerformanceBaseline, writeAuthorityDiagnosticsBundle as writeAuthorityDiagnosticsBundleFile, } from "./maintenance/authority-diagnostics-bundle.js"; @@ -1885,6 +1886,16 @@ function getGraphPersistenceLiveState() { authorityCheckpointRestoreError: String( graphPersistenceState.authorityCheckpointRestoreError || "", ), + authorityPerformanceBaseline: cloneRuntimeDebugValue( + graphPersistenceState.authorityPerformanceBaseline, + null, + ), + authorityPerformanceBaselineUpdatedAt: String( + graphPersistenceState.authorityPerformanceBaselineUpdatedAt || "", + ), + authorityPerformanceBaselineReason: String( + graphPersistenceState.authorityPerformanceBaselineReason || "", + ), authorityBrowserCacheMode: String( authorityRuntime.browserState.mode || "minimal", ), @@ -2284,6 +2295,147 @@ function getAuthorityBlobAdapter(options = {}) { }); } +function buildAuthorityPerformanceBaselineSnapshot(options = {}) { + const liveGraphPersistence = getGraphPersistenceLiveState(); + return buildAuthorityPerformanceBaseline({ + chatId: + normalizeChatIdCandidate(options.chatId) || + normalizeChatIdCandidate(getCurrentChatId()) || + normalizeChatIdCandidate(graphPersistenceState.chatId), + graphPersistence: liveGraphPersistence, + graph: currentGraph, + consistencyAudit: liveGraphPersistence.authorityConsistencyAudit, + }); +} + +function captureAuthorityPerformanceBaseline(options = {}) { + const baseline = buildAuthorityPerformanceBaselineSnapshot(options); + const capturedAt = String(baseline?.capturedAt || new Date().toISOString()); + const reason = String(options.reason || "manual-authority-performance-baseline"); + updateGraphPersistenceState({ + authorityPerformanceBaseline: cloneRuntimeDebugValue(baseline, null), + authorityPerformanceBaselineUpdatedAt: capturedAt, + authorityPerformanceBaselineReason: reason, + }); + refreshPanelLiveState(); + return { + ok: true, + baseline, + }; +} + +async function exportAuthorityDiagnosticsBundle(options = {}) { + const settings = getSettings(); + if (!shouldUseAuthorityDiagnosticsBundle()) { + return { + ok: false, + reason: "authority-diagnostics-unavailable", + }; + } + const chatId = normalizeChatIdCandidate( + options.chatId || getCurrentChatId() || graphPersistenceState.chatId, + ); + if (!chatId) { + return { + ok: false, + reason: "missing-chat-id", + }; + } + const reason = String(options.reason || "diagnostics-bundle").trim() || "diagnostics-bundle"; + const liveGraphPersistence = getGraphPersistenceLiveState(); + const baseline = buildAuthorityPerformanceBaseline({ + chatId, + graphPersistence: liveGraphPersistence, + graph: currentGraph, + consistencyAudit: liveGraphPersistence.authorityConsistencyAudit, + }); + const bundle = buildAuthorityDiagnosticsBundle({ + chatId, + reason, + settings, + runtimeStatus, + runtimeDebug: readRuntimeDebugSnapshot(), + graphPersistence: { + ...liveGraphPersistence, + authorityPerformanceBaseline: cloneRuntimeDebugValue(baseline, null), + authorityPerformanceBaselineUpdatedAt: String(baseline?.capturedAt || ""), + authorityPerformanceBaselineReason: reason, + }, + graph: currentGraph, + lastExtractionStatus, + lastVectorStatus, + lastRecallStatus, + lastBatchStatus: cloneRuntimeDebugValue(currentGraph?.historyState?.lastBatchStatus, null), + lastInjection: lastInjectionContent, + lastExtract: lastExtractedItems, + lastRecall: lastRecalledItems, + performanceBaseline: baseline, + }); + const path = buildAuthorityDiagnosticsBundlePath(chatId, reason); + const adapter = getAuthorityBlobAdapter(); + try { + const result = await writeAuthorityDiagnosticsBundleFile(adapter, bundle, { + chatId, + reason, + path, + signal: options.signal, + }); + const updatedAt = new Date().toISOString(); + const bundleSize = (() => { + if (Number.isFinite(Number(result?.result?.size))) { + return Number(result.result.size); + } + try { + return JSON.stringify(bundle).length; + } catch { + return 0; + } + })(); + recordAuthorityBlobSnapshot({ + action: "diagnostics-write", + ok: result?.ok !== false, + backend: "authority-blob", + path: result?.path || path, + reason, + }); + updateGraphPersistenceState({ + authorityPerformanceBaseline: cloneRuntimeDebugValue(baseline, null), + authorityPerformanceBaselineUpdatedAt: String(baseline?.capturedAt || updatedAt), + authorityPerformanceBaselineReason: reason, + authorityDiagnosticsBundlePath: String(result?.path || path), + authorityDiagnosticsBundleReason: reason, + authorityDiagnosticsBundleUpdatedAt: updatedAt, + authorityDiagnosticsBundleSize: bundleSize, + }); + if (options.refreshHost !== false) { + refreshPanelLiveState(); + } + return { + ok: result?.ok !== false, + path: String(result?.path || path), + size: bundleSize, + baseline, + bundle, + }; + } catch (error) { + const message = + error?.message || String(error) || "Authority diagnostics bundle failed"; + recordAuthorityBlobSnapshot({ + action: "diagnostics-write", + ok: false, + backend: "authority-blob", + path, + reason, + error: message, + }); + return { + ok: false, + reason: "authority-diagnostics-bundle-error", + error, + }; + } +} + async function writeAuthorityLukerCheckpointBlob( checkpoint = null, { chatId = "", reason = "luker-checkpoint", signal = undefined } = {}, @@ -21258,6 +21410,12 @@ async function onRestoreAuthorityCheckpoint() { }); } +async function onCaptureAuthorityPerformanceBaseline() { + return captureAuthorityPerformanceBaseline({ + reason: "panel-authority-performance-baseline", + }); +} + async function onReembedDirect() { return await onReembedDirectController({ getEmbeddingConfig, @@ -21839,6 +21997,7 @@ async function onCompactLukerSidecar() { refreshAuthorityJobs: onRefreshAuthorityJobs, runAuthorityConsistencyAudit: onRunAuthorityConsistencyAudit, restoreAuthorityCheckpoint: onRestoreAuthorityCheckpoint, + captureAuthorityPerformanceBaseline: onCaptureAuthorityPerformanceBaseline, reembedDirect: onReembedDirect, reroll: onReroll, clearGraph: onClearGraph, diff --git a/maintenance/authority-diagnostics-bundle.js b/maintenance/authority-diagnostics-bundle.js index f03d226..f29c1e1 100644 --- a/maintenance/authority-diagnostics-bundle.js +++ b/maintenance/authority-diagnostics-bundle.js @@ -169,6 +169,126 @@ function buildStatusSnapshot(status = null) { ); } +export function buildAuthorityPerformanceBaseline({ + chatId = "", + graphPersistence = null, + graph = null, + consistencyAudit = null, +} = {}) { + const persistence = + graphPersistence && typeof graphPersistence === "object" && !Array.isArray(graphPersistence) + ? graphPersistence + : {}; + const runtimeGraph = graph && typeof graph === "object" && !Array.isArray(graph) ? graph : {}; + const audit = + consistencyAudit && typeof consistencyAudit === "object" && !Array.isArray(consistencyAudit) + ? consistencyAudit + : persistence.authorityConsistencyAudit && + typeof persistence.authorityConsistencyAudit === "object" && + !Array.isArray(persistence.authorityConsistencyAudit) + ? persistence.authorityConsistencyAudit + : null; + const loadDiagnostics = + persistence.loadDiagnostics && typeof persistence.loadDiagnostics === "object" + ? persistence.loadDiagnostics + : {}; + const persistDelta = + persistence.persistDelta && typeof persistence.persistDelta === "object" + ? persistence.persistDelta + : {}; + const recentJobs = Array.isArray(persistence.authorityRecentJobs) + ? persistence.authorityRecentJobs + : []; + const failedJobs = recentJobs.filter((job) => { + const queueState = String(job?.queueState || "").trim(); + return queueState === "failed" || queueState === "error"; + }); + const runningJobs = recentJobs.filter( + (job) => String(job?.queueState || "").trim() === "running", + ); + const graphRevision = Number( + runtimeGraph?.meta?.revision || persistence.revision || 0, + ); + return { + kind: "authority-performance-baseline", + capturedAt: new Date().toISOString(), + chatId: normalizeRecordId(chatId || persistence.chatId || runtimeGraph?.chatId), + graphRevision: Number.isFinite(graphRevision) ? graphRevision : 0, + graphNodeCount: Array.isArray(runtimeGraph?.nodes) ? runtimeGraph.nodes.length : 0, + graphEdgeCount: Array.isArray(runtimeGraph?.edges) ? runtimeGraph.edges.length : 0, + extractionCount: Number(runtimeGraph?.historyState?.extractionCount || 0), + load: safeClone( + { + state: String(persistence.loadState || ""), + source: String(loadDiagnostics.source || ""), + totalMs: Number(loadDiagnostics.totalMs || 0), + preApplyMs: Number(loadDiagnostics.preApplyMs || 0), + exportSnapshotMs: Number(loadDiagnostics.exportSnapshotMs || 0), + hydrateMs: Number(loadDiagnostics.hydrateMs || 0), + hydrateNativeRecordsMs: Number(loadDiagnostics.hydrateNativeRecordsMs || 0), + applyRuntimeMs: Number(loadDiagnostics.applyRuntimeMs || 0), + updatedAt: String(loadDiagnostics.updatedAt || ""), + }, + null, + ), + persist: safeClone( + { + totalMs: Number(persistDelta.totalMs || persistDelta.buildMs || 0), + buildMs: Number(persistDelta.buildMs || 0), + baseSnapshotReadMs: Number(persistDelta.baseSnapshotReadMs || 0), + snapshotBuildMs: Number(persistDelta.snapshotBuildMs || 0), + prepareMs: Number(persistDelta.prepareMs || 0), + nativeAttemptMs: Number(persistDelta.nativeAttemptMs || 0), + lookupMs: Number(persistDelta.lookupMs || 0), + jsDiffMs: Number(persistDelta.jsDiffMs || 0), + hydrateMs: Number(persistDelta.hydrateMs || 0), + commitQueueWaitMs: Number(persistDelta.commitQueueWaitMs || 0), + commitMs: Number(persistDelta.commitMs || 0), + commitPayloadBytes: Number(persistDelta.commitPayloadBytes || 0), + commitWalBytes: Number(persistDelta.commitWalBytes || 0), + updatedAt: String(persistDelta.updatedAt || ""), + }, + null, + ), + soak: { + recentJobCount: recentJobs.length, + runningJobCount: runningJobs.length, + failedJobCount: failedJobs.length, + lastJobId: String(persistence.authorityLastJobId || ""), + lastJobStatus: String(persistence.authorityLastJobStatus || ""), + lastJobUpdatedAt: String(persistence.authorityLastJobUpdatedAt || ""), + }, + audit: safeClone( + { + state: String(persistence.authorityConsistencyState || audit?.summary?.level || "idle"), + issueCount: Array.isArray(audit?.issues) ? audit.issues.length : 0, + sqlRevision: Number(audit?.sql?.revision || 0), + triviumRevision: Number(audit?.trivium?.revision || 0), + blobRevision: Number( + audit?.blob?.revision || persistence.authorityBlobCheckpointRevision || 0, + ), + }, + null, + ), + artifacts: safeClone( + { + diagnosticsBundlePath: String(persistence.authorityDiagnosticsBundlePath || ""), + diagnosticsBundleReason: String(persistence.authorityDiagnosticsBundleReason || ""), + diagnosticsBundleUpdatedAt: String( + persistence.authorityDiagnosticsBundleUpdatedAt || "", + ), + diagnosticsBundleSize: Number(persistence.authorityDiagnosticsBundleSize || 0), + blobCheckpointPath: String(persistence.authorityBlobCheckpointPath || ""), + blobCheckpointRevision: Number(persistence.authorityBlobCheckpointRevision || 0), + blobCheckpointUpdatedAt: String( + persistence.authorityBlobCheckpointUpdatedAt || "", + ), + }, + null, + ), + }; +} + export function sanitizeDiagnosticsSettings(settings = {}) { return sanitizeValue(settings, "settings", 0); } @@ -197,6 +317,7 @@ export function buildAuthorityDiagnosticsBundle({ lastInjection = "", lastExtract = [], lastRecall = [], + performanceBaseline = null, } = {}) { const createdAt = new Date().toISOString(); return { @@ -210,6 +331,7 @@ export function buildAuthorityDiagnosticsBundle({ runtimeDebug: safeClone(runtimeDebug, null), graphPersistence: safeClone(graphPersistence, null), graphSummary: buildGraphSummary(graph), + performanceBaseline: safeClone(performanceBaseline, null), lastStatuses: { extraction: buildStatusSnapshot(lastExtractionStatus), vector: buildStatusSnapshot(lastVectorStatus), diff --git a/tests/authority-diagnostics-bundle.mjs b/tests/authority-diagnostics-bundle.mjs index d6532af..ec5cd6f 100644 --- a/tests/authority-diagnostics-bundle.mjs +++ b/tests/authority-diagnostics-bundle.mjs @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { buildAuthorityDiagnosticsBundle, buildAuthorityDiagnosticsBundlePath, + buildAuthorityPerformanceBaseline, sanitizeDiagnosticsSettings, writeAuthorityDiagnosticsBundle, } from "../maintenance/authority-diagnostics-bundle.js"; @@ -42,6 +43,57 @@ function createMockAdapter() { assert.equal(sanitized.models[0].token, "[REDACTED]"); } +{ + const baseline = buildAuthorityPerformanceBaseline({ + chatId: "chat/main", + graphPersistence: { + chatId: "chat/main", + revision: 12, + loadState: "loaded", + loadDiagnostics: { + source: "authority-sql", + totalMs: 45, + hydrateMs: 18, + }, + persistDelta: { + totalMs: 22, + commitMs: 8, + commitPayloadBytes: 2048, + }, + authorityRecentJobs: [ + { id: "job-1", queueState: "success" }, + { id: "job-2", queueState: "failed" }, + ], + authorityLastJobId: "job-2", + authorityLastJobStatus: "failed", + authorityConsistencyState: "warning", + authorityBlobCheckpointRevision: 11, + authorityDiagnosticsBundlePath: "user/files/diag.json", + authorityDiagnosticsBundleSize: 512, + }, + graph: { + chatId: "chat/main", + meta: { revision: 12 }, + nodes: [{ id: "n1" }, { id: "n2" }], + edges: [{ id: "e1" }], + historyState: { extractionCount: 5 }, + }, + consistencyAudit: { + issues: [{ code: "revision-drift" }], + sql: { revision: 12 }, + trivium: { revision: 10 }, + blob: { revision: 11 }, + }, + }); + assert.equal(baseline.kind, "authority-performance-baseline"); + assert.equal(baseline.graphRevision, 12); + assert.equal(baseline.graphNodeCount, 2); + assert.equal(baseline.soak.recentJobCount, 2); + assert.equal(baseline.soak.failedJobCount, 1); + assert.equal(baseline.audit.issueCount, 1); + assert.equal(baseline.artifacts.diagnosticsBundlePath, "user/files/diag.json"); +} + { const bundle = buildAuthorityDiagnosticsBundle({ chatId: "chat/main", @@ -123,6 +175,11 @@ function createMockAdapter() { lastInjection: "A".repeat(4500), lastExtract: [{ id: "n1" }], lastRecall: [{ id: "n2" }], + performanceBaseline: { + kind: "authority-performance-baseline", + graphRevision: 9, + load: { totalMs: 12 }, + }, }); assert.equal(bundle.kind, "st-bme-authority-diagnostics"); @@ -136,6 +193,7 @@ function createMockAdapter() { assert.equal(bundle.lastInjection.textPreview.length, 4000); assert.equal(bundle.recentExtractedItems.length, 1); assert.equal(bundle.recentRecalledItems.length, 1); + assert.equal(bundle.performanceBaseline?.graphRevision, 9); } { diff --git a/ui/panel.js b/ui/panel.js index ab9fbd8..b688bb2 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -3104,6 +3104,34 @@ function _refreshTaskPersistence() { const authorityRestoreUpdatedLabel = ps.authorityCheckpointRestoreUpdatedAt ? _formatTaskProfileTime(ps.authorityCheckpointRestoreUpdatedAt) : "—"; + const authorityBaseline = + ps.authorityPerformanceBaseline && + typeof ps.authorityPerformanceBaseline === "object" && + !Array.isArray(ps.authorityPerformanceBaseline) + ? ps.authorityPerformanceBaseline + : null; + const authorityBaselineUpdatedLabel = ps.authorityPerformanceBaselineUpdatedAt + ? _formatTaskProfileTime(ps.authorityPerformanceBaselineUpdatedAt) + : authorityBaseline?.capturedAt + ? _formatTaskProfileTime(authorityBaseline.capturedAt) + : "—"; + const authorityBaselineLoadLabel = authorityBaseline?.load + ? `${_formatDurationMs(authorityBaseline.load.totalMs)} / hydrate ${_formatDurationMs(authorityBaseline.load.hydrateMs)}` + : "—"; + const authorityBaselinePersistLabel = authorityBaseline?.persist + ? `${_formatDurationMs(authorityBaseline.persist.totalMs)} / commit ${_formatDurationMs(authorityBaseline.persist.commitMs)}` + : "—"; + const authorityBaselineSoakLabel = authorityBaseline?.soak + ? `${Number(authorityBaseline.soak.recentJobCount || 0)} recent / ${Number(authorityBaseline.soak.failedJobCount || 0)} failed / ${Number(authorityBaseline.soak.runningJobCount || 0)} running` + : "—"; + const authorityBaselineGraphLabel = authorityBaseline + ? `rev ${Number(authorityBaseline.graphRevision || 0)} · ${Number(authorityBaseline.graphNodeCount || 0)} 节点 / ${Number(authorityBaseline.graphEdgeCount || 0)} 边` + : "—"; + const authorityBundlePathLabel = String(ps.authorityDiagnosticsBundlePath || "").trim() || "—"; + const authorityBundleUpdatedLabel = ps.authorityDiagnosticsBundleUpdatedAt + ? _formatTaskProfileTime(ps.authorityDiagnosticsBundleUpdatedAt) + : "—"; + const authorityBundleSizeLabel = _formatDataSizeBytes(ps.authorityDiagnosticsBundleSize); const activeRegionLabel = String( historyState?.activeRegion || historyState?.lastExtractedRegion || @@ -3210,6 +3238,15 @@ function _refreshTaskPersistence() { ["恢复状态", authorityRestoreLabel], ["恢复结果", authorityRestoreResult?.revision ? `rev ${Number(authorityRestoreResult.revision)}` : "—"], ["最近恢复", authorityRestoreUpdatedLabel], + ["Baseline 图谱", authorityBaselineGraphLabel], + ["Baseline Load", authorityBaselineLoadLabel], + ["Baseline Persist", authorityBaselinePersistLabel], + ["Baseline Soak", authorityBaselineSoakLabel], + ["最近 Baseline", authorityBaselineUpdatedLabel], + ["诊断包路径", authorityBundlePathLabel], + ["诊断包大小", authorityBundleSizeLabel], + ["诊断包时间", authorityBundleUpdatedLabel], + ["诊断包原因", ps.authorityDiagnosticsBundleReason || "—"], ]; const authorityActionButtons = [ typeof _actionHandlers.runAuthorityConsistencyAudit === "function" @@ -3218,6 +3255,12 @@ function _refreshTaskPersistence() { typeof _actionHandlers.restoreAuthorityCheckpoint === "function" ? `` : "", + typeof _actionHandlers.captureAuthorityPerformanceBaseline === "function" + ? `` + : "", + typeof _actionHandlers.exportDiagnosticsBundle === "function" + ? `` + : "", ].filter(Boolean).join(""); el.innerHTML = ` @@ -3287,11 +3330,27 @@ function _refreshTaskPersistence() { } else { toastr.warning(`Authority Checkpoint 恢复失败:${result?.error || "unknown"}`, "ST-BME"); } + } else if (action === "baseline") { + if (typeof _actionHandlers.captureAuthorityPerformanceBaseline !== "function") return; + const result = await _actionHandlers.captureAuthorityPerformanceBaseline(); + if (result?.ok) { + toastr.success("Authority Perf Baseline 已捕获", "ST-BME"); + } else { + toastr.warning(`Authority Perf Baseline 捕获失败:${result?.error || "unknown"}`, "ST-BME"); + } + } else if (action === "bundle") { + if (typeof _actionHandlers.exportDiagnosticsBundle !== "function") return; + const result = await _actionHandlers.exportDiagnosticsBundle(); + if (result?.handledToast) { + return; + } } } catch (error) { toastr.error( action === "restore" ? `Authority Checkpoint 恢复失败: ${error?.message || error}` + : action === "baseline" + ? `Authority Perf Baseline 捕获失败: ${error?.message || error}` : `Authority 审计失败: ${error?.message || error}`, "ST-BME", ); diff --git a/ui/ui-status.js b/ui/ui-status.js index 318c412..f06d324 100644 --- a/ui/ui-status.js +++ b/ui/ui-status.js @@ -180,6 +180,9 @@ export function createGraphPersistenceState() { authorityCheckpointRestoreResult: null, authorityCheckpointRestoreUpdatedAt: "", authorityCheckpointRestoreError: "", + authorityPerformanceBaseline: null, + authorityPerformanceBaselineUpdatedAt: "", + authorityPerformanceBaselineReason: "", authorityDiagnosticsBundlePath: "", authorityDiagnosticsBundleReason: "", authorityDiagnosticsBundleUpdatedAt: "",