Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot]
e22f9e4e37 chore: bump manifest version [skip ci] 2026-05-09 09:09:08 +00:00
youzini
ea57cf9a6a Merge remote-tracking branch 'origin/dev' into dev 2026-05-09 09:07:24 +00:00
youzini
f443b388ee fix(authority): distinguish embedding and Trivium failures
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-09 09:06:19 +00:00
youzini
880e2b9158 fix(authority): classify capability probe failures
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-09 09:05:32 +00:00
6 changed files with 284 additions and 10 deletions

View File

@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Youzini",
"version": "6.3.1",
"version": "6.3.2",
"homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology"
}

View File

@@ -162,6 +162,26 @@ function readPayloadMessage(payload = {}, fallback = "") {
return String(payload.error || payload.message || payload.reason || fallback || "");
}
function classifyAuthorityProbeStatus(status = 0, payload = null) {
const payloadCategory = String(payload?.category || "").trim();
if (payloadCategory) return payloadCategory;
const numericStatus = Number(status || 0);
if (numericStatus === 408) return "timeout";
if (numericStatus === 401 || numericStatus === 403) return "permission";
if (numericStatus === 413) return "payload-too-large";
if (numericStatus === 429) return "rate-limit";
if (numericStatus >= 500) return "server";
if (numericStatus >= 400) return "validation";
return "";
}
function classifyAuthorityProbeError(error = null) {
const category = String(error?.category || error?.errorCategory || "").trim();
if (category) return category;
if (String(error?.name || "") === "AbortError") return "timeout";
return error ? "network" : "";
}
function buildAuthorityPermissionEvaluateRequests(settings = {}, readiness = {}, options = {}) {
const requests = [];
const sqlTarget = String(options.sqlTarget || settings.sqlTarget || "default");
@@ -198,6 +218,8 @@ async function verifyAuthorityDataPlane(baseUrl, fetchImpl, headers, settings =
reason: initStatus === 401 || initStatus === 403 ? "session-init-denied" : "session-init-failed",
lastError: readPayloadMessage(initPayload, `HTTP ${initStatus || "unknown"}`),
status: initStatus,
errorCategory: classifyAuthorityProbeStatus(initStatus, initPayload),
errorDomain: "authority",
};
}
@@ -231,6 +253,8 @@ async function verifyAuthorityDataPlane(baseUrl, fetchImpl, headers, settings =
reason: currentStatus === 401 || currentStatus === 403 ? "session-invalid" : "session-current-failed",
lastError: readPayloadMessage(currentPayload, `HTTP ${currentStatus || "unknown"}`),
status: currentStatus,
errorCategory: classifyAuthorityProbeStatus(currentStatus, currentPayload),
errorDomain: "authority",
};
}
@@ -259,6 +283,8 @@ async function verifyAuthorityDataPlane(baseUrl, fetchImpl, headers, settings =
reason: permissionStatus === 401 || permissionStatus === 403 ? "permission-denied" : "permission-evaluate-failed",
lastError: readPayloadMessage(permissionPayload, `HTTP ${permissionStatus || "unknown"}`),
status: permissionStatus,
errorCategory: classifyAuthorityProbeStatus(permissionStatus, permissionPayload),
errorDomain: "authority",
};
}
@@ -408,6 +434,8 @@ export function createDefaultAuthorityCapabilityState(overrides = {}) {
missingFeatures: ["sql.query", "sql.mutation", "trivium.search", "jobs", "blob-or-private-files"],
reason: "not-probed",
lastError: "",
errorCategory: "",
errorDomain: "",
endpoint: "",
status: 0,
latencyMs: 0,
@@ -459,6 +487,8 @@ export function normalizeAuthorityCapabilityState(input = {}, settings = {}) {
missingFeatures,
reason: String(source.reason || (healthy ? "ok" : "not-ready")),
lastError: String(source.lastError || ""),
errorCategory: String(source.errorCategory || ""),
errorDomain: String(source.errorDomain || ""),
endpoint: String(source.endpoint || ""),
status: clampInteger(source.status, 0, 0, 999),
latencyMs: Math.max(0, Number(source.latencyMs) || 0),
@@ -547,6 +577,7 @@ export async function probeAuthorityCapabilities(options = {}) {
let lastError = "";
let lastStatus = 0;
let lastErrorCategory = "";
for (const endpoint of buildAuthorityProbeUrls(settings.baseUrl)) {
const startedAt = readNowMs();
try {
@@ -555,6 +586,7 @@ export async function probeAuthorityCapabilities(options = {}) {
const status = Number(response?.status || 0);
lastStatus = status;
if (status === 404) continue;
const errorPayload = response?.ok ? null : await readResponsePayload(response);
if (status === 401 || status === 403) {
return normalizeAuthorityCapabilityState(
{
@@ -563,7 +595,9 @@ export async function probeAuthorityCapabilities(options = {}) {
sessionReady: false,
permissionReady: false,
reason: "permission-denied",
lastError: `HTTP ${status}`,
lastError: readPayloadMessage(errorPayload, `HTTP ${status}`),
errorCategory: classifyAuthorityProbeStatus(status, errorPayload),
errorDomain: "authority",
endpoint,
status,
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
@@ -579,7 +613,9 @@ export async function probeAuthorityCapabilities(options = {}) {
installed: status > 0,
healthy: false,
reason: "http-error",
lastError: `HTTP ${status || "unknown"}`,
lastError: readPayloadMessage(errorPayload, `HTTP ${status || "unknown"}`),
errorCategory: classifyAuthorityProbeStatus(status, errorPayload),
errorDomain: "authority",
endpoint,
status,
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
@@ -605,12 +641,16 @@ export async function probeAuthorityCapabilities(options = {}) {
let reason = missingFeatures.length ? "missing-required-features" : "ok";
let dataPlaneLastError = "";
let dataPlaneStatus = status;
let dataPlaneErrorCategory = "";
let dataPlaneErrorDomain = "";
if (healthy) {
const verified = await verifyAuthorityDataPlane(settings.baseUrl, fetchImpl, headers, settings, readiness, options);
sessionReady = verified.sessionReady;
permissionReady = verified.permissionReady;
dataPlaneStatus = Number(verified.status || status || 0);
dataPlaneLastError = String(verified.lastError || "");
dataPlaneErrorCategory = String(verified.errorCategory || "");
dataPlaneErrorDomain = String(verified.errorDomain || "");
if (verified.reason && verified.reason !== "ok") {
reason = verified.reason;
}
@@ -627,6 +667,8 @@ export async function probeAuthorityCapabilities(options = {}) {
missingFeatures,
reason,
lastError: dataPlaneLastError,
errorCategory: dataPlaneErrorCategory,
errorDomain: dataPlaneErrorDomain,
endpoint,
status: dataPlaneStatus,
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
@@ -637,6 +679,8 @@ export async function probeAuthorityCapabilities(options = {}) {
);
} catch (error) {
lastError = error?.message || String(error);
lastStatus = Number(error?.status || lastStatus || 0);
lastErrorCategory = classifyAuthorityProbeError(error);
}
}
@@ -646,6 +690,8 @@ export async function probeAuthorityCapabilities(options = {}) {
healthy: false,
reason: lastStatus === 404 ? "not-installed" : "probe-failed",
lastError,
errorCategory: lastErrorCategory || classifyAuthorityProbeStatus(lastStatus),
errorDomain: lastErrorCategory || lastStatus ? "authority" : "",
status: lastStatus,
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),

View File

@@ -176,6 +176,78 @@ const relativeUnavailable = await probeAuthorityCapabilities({
assert.equal(relativeUnavailable.reason, "relative-url-unavailable");
assert.equal(relativeUnavailable.serverPrimaryReady, false);
const permissionDeniedState = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: true,
nowMs: 3100,
fetchImpl: async () => ({
ok: false,
status: 403,
async json() {
return { error: "permission denied" };
},
}),
});
assert.equal(permissionDeniedState.reason, "permission-denied");
assert.equal(permissionDeniedState.errorCategory, "permission");
assert.equal(permissionDeniedState.errorDomain, "authority");
const rateLimitedState = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: true,
nowMs: 3200,
fetchImpl: async () => ({
ok: false,
status: 429,
async json() {
return { error: "slow down" };
},
}),
});
assert.equal(rateLimitedState.reason, "http-error");
assert.equal(rateLimitedState.errorCategory, "rate-limit");
assert.equal(rateLimitedState.errorDomain, "authority");
const serverErrorState = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: true,
nowMs: 3300,
fetchImpl: async () => ({
ok: false,
status: 503,
async json() {
return { category: "backpressure", code: "job_queue_full" };
},
}),
});
assert.equal(serverErrorState.reason, "http-error");
assert.equal(serverErrorState.errorCategory, "backpressure");
assert.equal(serverErrorState.errorDomain, "authority");
const networkFailedState = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: true,
nowMs: 3400,
fetchImpl: async () => {
throw new Error("fetch failed");
},
});
assert.equal(networkFailedState.reason, "probe-failed");
assert.equal(networkFailedState.errorCategory, "network");
assert.equal(networkFailedState.errorDomain, "authority");
const timeoutState = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: true,
nowMs: 3500,
fetchImpl: async () => {
throw Object.assign(new Error("aborted"), { name: "AbortError" });
},
});
assert.equal(timeoutState.reason, "probe-failed");
assert.equal(timeoutState.errorCategory, "timeout");
assert.equal(timeoutState.errorDomain, "authority");
// Regression: Authority capability normalization records explicit supported job types from probe payloads.
// When a probe payload provides jobs.supportedTypes, normalizeAuthorityCapabilityState should surface
// them as supportedJobTypes and set supportedJobTypesKnown = true.

View File

@@ -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 };

View File

@@ -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;
}

View File

@@ -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) {