From f443b388ee1d414a2abd551b5334152346b25705 Mon Sep 17 00:00:00 2001 From: youzini Date: Sat, 9 May 2026 09:06:19 +0000 Subject: [PATCH] fix(authority): distinguish embedding and Trivium failures Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- tests/authority-vector-primary.mjs | 89 +++++++++++++++++++++- vector/authority-vector-primary-adapter.js | 22 ++++++ vector/vector-index.js | 59 +++++++++++++- 3 files changed, 163 insertions(+), 7 deletions(-) diff --git a/tests/authority-vector-primary.mjs b/tests/authority-vector-primary.mjs index 9cfd235..ae56de9 100644 --- a/tests/authority-vector-primary.mjs +++ b/tests/authority-vector-primary.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { addEdge, addNode, createEdge, createEmptyGraph, createNode } from "../graph/graph.js"; +import { AuthorityHttpError } from "../runtime/authority-http-client.js"; import { installResolveHooks, toDataModuleUrl, @@ -33,7 +34,10 @@ const { normalizeAuthorityVectorConfig, queryAuthorityTriviumNeighbors, } = await import("../vector/authority-vector-primary-adapter.js"); -const { findSimilarNodesByText: findSimilarNodesByTextFromIndex, syncGraphVectorIndex: syncGraphVectorIndexFromIndex } = await import("../vector/vector-index.js"); +const { + findSimilarNodesByText: findSimilarNodesByTextFromIndex, + syncGraphVectorIndex: syncGraphVectorIndexFromIndex, +} = await import("../vector/vector-index.js"); function createAuthorityVectorGraph() { const graph = createEmptyGraph(); @@ -66,7 +70,7 @@ function createAuthorityVectorGraph() { return { graph, first, second }; } -function createMockTriviumClient({ failBulkUpsert = false } = {}) { +function createMockTriviumClient({ failBulkUpsert = false, failSearch = false } = {}) { const calls = []; return { calls, @@ -88,7 +92,11 @@ function createMockTriviumClient({ failBulkUpsert = false } = {}) { async bulkUpsert(payload) { calls.push(["bulkUpsert", payload]); if (failBulkUpsert) { - throw new Error("trivium-down"); + throw new AuthorityHttpError("trivium-down", { + status: 503, + category: "server", + path: "/trivium/bulk-upsert", + }); } return { ok: true, upserted: payload.items?.length || 0 }; }, @@ -102,6 +110,13 @@ function createMockTriviumClient({ failBulkUpsert = false } = {}) { }, async search(payload) { calls.push(["search", payload]); + if (failSearch) { + throw new AuthorityHttpError("trivium search denied", { + status: 403, + category: "permission", + path: "/trivium/search", + }); + } return { results: [ { nodeId: "node-b", score: 0.91 }, @@ -234,9 +249,77 @@ assert.equal(isAuthorityVectorConfig(config), true); assert.equal(graph.vectorIndexState.mode, "authority"); assert.equal(graph.vectorIndexState.dirty, true); assert.equal(graph.vectorIndexState.dirtyReason, "authority-trivium-sync-failed"); + assert.equal(result.errorCategory, "server"); + assert.equal(result.errorDomain, "authority"); + assert.equal(result.timings.errorCategory, "server"); + assert.equal(result.timings.authorityErrorCategory, "server"); + assert.equal(graph.vectorIndexState.lastErrorCategory, "server"); + assert.equal(graph.vectorIndexState.lastErrorDomain, "authority"); + assert.equal(result.timings.authorityDiagnostics.upsert.errorCategory, "server"); + assert.equal(result.timings.authorityDiagnostics.upsert.chunks[0].errorCategory, "server"); assert.match(graph.vectorIndexState.lastWarning, /Authority Trivium 同步失败/); } +{ + const previousOverrides = globalThis.__stBmeTestOverrides; + globalThis.__stBmeTestOverrides = { + embedding: { + async embedBatch(texts = []) { + return texts.map(() => null); + }, + async embedText() { + return null; + }, + }, + }; + try { + const { graph } = createAuthorityVectorGraph(); + graph.nodes.forEach((node) => { + node.embedding = null; + }); + const triviumClient = createMockTriviumClient(); + const result = await syncGraphVectorIndexFromIndex(graph, config, { + chatId: "chat-authority-vector", + purge: true, + triviumClient, + }); + assert.match(result.error, /Embedding provider failed/); + assert.doesNotMatch(result.error, /Authority Trivium embedding failed/); + assert.equal(result.errorCategory, "embedding-provider"); + assert.equal(result.errorDomain, "embedding"); + assert.equal(graph.vectorIndexState.dirtyReason, "embedding-provider-sync-failed"); + assert.equal(graph.vectorIndexState.lastErrorCategory, "embedding-provider"); + assert.equal(graph.vectorIndexState.lastErrorDomain, "embedding"); + assert.match(graph.vectorIndexState.lastWarning, /Embedding provider 同步失败/); + assert.equal(triviumClient.calls.some(([name]) => name === "bulkUpsert"), false); + } finally { + globalThis.__stBmeTestOverrides = previousOverrides; + } +} + +{ + const { graph, first, second } = createAuthorityVectorGraph(); + const triviumClient = createMockTriviumClient({ failSearch: true }); + const queryConfig = { ...config, triviumClient }; + await syncGraphVectorIndexFromIndex(graph, queryConfig, { + chatId: "chat-authority-vector", + purge: true, + triviumClient, + }); + const results = await findSimilarNodesByTextFromIndex( + graph, + "archive door", + queryConfig, + 5, + [first, second], + ); + assert.deepEqual(results, []); + assert.equal(graph.vectorIndexState.lastSearchTimings.errorCategory, "permission"); + assert.equal(graph.vectorIndexState.lastSearchTimings.authorityErrorCategory, "permission"); + assert.equal(graph.vectorIndexState.lastErrorCategory, "permission"); + assert.equal(graph.vectorIndexState.lastErrorDomain, "authority"); +} + { const triviumClient = createMockTriviumClient(); const queryConfig = { ...config, triviumClient }; diff --git a/vector/authority-vector-primary-adapter.js b/vector/authority-vector-primary-adapter.js index 5ebc518..6beb906 100644 --- a/vector/authority-vector-primary-adapter.js +++ b/vector/authority-vector-primary-adapter.js @@ -2,6 +2,7 @@ import { normalizeAuthorityBaseUrl } from "../runtime/authority-capabilities.js" import { AUTHORITY_PROTOCOL_SERVER_PLUGIN_V06, AuthorityHttpClient, + AuthorityHttpError, } from "../runtime/authority-http-client.js"; import { embedText } from "./embedding.js"; @@ -89,6 +90,25 @@ function hasPlainKeys(value = null) { return isPlainObject(value) && Object.keys(value).length > 0; } +function getAuthorityErrorCategory(error = null) { + return String(error?.category || error?.errorCategory || "").trim(); +} + +function getAuthorityErrorDomain(error = null) { + if (!error) return ""; + return error instanceof AuthorityHttpError || getAuthorityErrorCategory(error) ? "authority" : ""; +} + +function buildAuthorityErrorDiagnostics(error = null) { + const category = getAuthorityErrorCategory(error); + const domain = getAuthorityErrorDomain(error); + return { + ...(category ? { errorCategory: category, authorityErrorCategory: category } : {}), + ...(domain ? { errorDomain: domain, authorityErrorDomain: domain } : {}), + ...(Number(error?.status || 0) > 0 ? { status: Number(error.status) } : {}), + }; +} + function normalizeOpenAICompatibleBaseUrl(value) { return String(value || "") .trim() @@ -816,6 +836,7 @@ export async function upsertAuthorityTriviumEntries(graph, config = {}, entries durationMs: roundMs(nowMs() - chunkStartedAt), ok: false, error: error?.message || String(error), + ...buildAuthorityErrorDiagnostics(error), }); error.authorityDiagnostics = { operation: "bulkUpsert", @@ -824,6 +845,7 @@ export async function upsertAuthorityTriviumEntries(graph, config = {}, entries chunks, totalBytes, totalMs: roundMs(nowMs() - startedAt), + ...buildAuthorityErrorDiagnostics(error), }; throw error; } diff --git a/vector/vector-index.js b/vector/vector-index.js index bbccd90..0697d70 100644 --- a/vector/vector-index.js +++ b/vector/vector-index.js @@ -626,6 +626,7 @@ function markAuthorityVectorStateDirty( config = {}, reason = "authority-trivium-failed", warning = "Authority Trivium 索引失败,已标记待重建", + diagnostics = {}, ) { if (!graph?.vectorIndexState || !isAuthorityVectorConfig(config)) { return; @@ -655,6 +656,39 @@ function markAuthorityVectorStateDirty( pending: total > 0 ? Math.max(1, Number(state.lastStats?.pending || 0)) : 0, }; state.lastWarning = String(warning || "Authority Trivium 索引失败,已标记待重建"); + const errorCategory = String(diagnostics.errorCategory || diagnostics.authorityErrorCategory || "").trim(); + const errorDomain = String(diagnostics.errorDomain || diagnostics.authorityErrorDomain || "").trim(); + if (errorCategory) state.lastErrorCategory = errorCategory; + if (errorDomain) state.lastErrorDomain = errorDomain; +} + +function getErrorCategory(error = null) { + return String(error?.category || error?.errorCategory || "").trim(); +} + +function getErrorDomain(error = null, fallback = "") { + if (!error) return ""; + if (error?.errorDomain) return String(error.errorDomain).trim(); + if (getErrorCategory(error)) return fallback || "authority"; + return fallback; +} + +function getAuthorityDiagnosticsErrorPatch(error = null) { + const errorCategory = getErrorCategory(error); + const errorDomain = getErrorDomain(error, errorCategory ? "authority" : ""); + return { + ...(errorCategory ? { errorCategory, authorityErrorCategory: errorCategory } : {}), + ...(errorDomain ? { errorDomain, authorityErrorDomain: errorDomain } : {}), + ...(Number(error?.status || 0) > 0 ? { status: Number(error.status) } : {}), + }; +} + +function createEmbeddingProviderError(failures = 0) { + const count = Math.max(0, Math.floor(Number(failures) || 0)); + const error = new Error(`Embedding provider failed for ${count} item(s)`); + error.errorCategory = "embedding-provider"; + error.errorDomain = "embedding"; + return error; } async function ensureEntryEmbeddings(graph, entries = [], config = {}, signal = undefined) { @@ -802,7 +836,7 @@ export async function syncGraphVectorIndex( embeddingsRequested += embeddingResult.requested; embedBatchMs += embeddingResult.elapsedMs; if (embeddingResult.failures > 0) { - throw new Error(`Authority Trivium embedding failed for ${embeddingResult.failures} item(s)`); + throw createEmbeddingProviderError(embeddingResult.failures); } const purgeStartedAt = nowMs(); const purgeResult = await purgeAuthorityTriviumNamespace(config, authorityOptions); @@ -866,7 +900,7 @@ export async function syncGraphVectorIndex( embeddingsRequested += embeddingResult.requested; embedBatchMs += embeddingResult.elapsedMs; if (embeddingResult.failures > 0) { - throw new Error(`Authority Trivium embedding failed for ${embeddingResult.failures} item(s)`); + throw createEmbeddingProviderError(embeddingResult.failures); } deletedNodeCount = nodeIdsToDelete.length; const deleteStartedAt = nowMs(); @@ -909,17 +943,29 @@ export async function syncGraphVectorIndex( } catch (error) { if (isAbortError(error)) throw error; const message = error?.message || String(error) || "Authority Trivium 同步失败"; + const errorCategory = getErrorCategory(error); + const errorDomain = getErrorDomain(error, errorCategory ? "authority" : ""); + const dirtyReason = errorDomain === "embedding" + ? "embedding-provider-sync-failed" + : "authority-trivium-sync-failed"; + const warningPrefix = errorDomain === "embedding" + ? "Embedding provider 同步失败" + : "Authority Trivium 同步失败"; markAuthorityVectorStateDirty( graph, config, - "authority-trivium-sync-failed", - `Authority Trivium 同步失败(${message}),已标记待重建`, + dirtyReason, + `${warningPrefix}(${message}),已标记待重建`, + { errorCategory, errorDomain }, ); state.lastSyncAt = Date.now(); state.lastTimings = { mode: syncMode, success: false, error: message, + ...(errorCategory ? { errorCategory } : {}), + ...(errorDomain ? { errorDomain } : {}), + ...(errorCategory && errorDomain === "authority" ? { authorityErrorCategory: errorCategory, authorityErrorDomain: errorDomain } : {}), desiredEntries: Number(desiredBuildDiagnostics.entryCount || desiredEntries.length), desiredBuildMs: roundMs(desiredBuildMs), authorityPurgeMs: roundMs(authorityPurgeMs), @@ -940,6 +986,8 @@ export async function syncGraphVectorIndex( stats: state.lastStats, timings: state.lastTimings, error: message, + ...(errorCategory ? { errorCategory } : {}), + ...(errorDomain ? { errorDomain } : {}), }; if (config.failOpen === false) { throw error; @@ -1291,17 +1339,20 @@ export async function findSimilarNodesByText( throw error; } const message = error?.message || String(error) || "Authority Trivium 查询失败"; + const errorPatch = getAuthorityDiagnosticsErrorPatch(error); markAuthorityVectorStateDirty( graph, config, "authority-trivium-query-failed", `Authority Trivium 查询失败(${message}),已标记待重建`, + errorPatch, ); recordSearchTimings({ success: false, reason: "authority-trivium-query-failed", requestMs: roundMs(nowMs() - requestStartedAt), error: message, + ...errorPatch, resultCount: 0, }); if (config.failOpen === false) {