feat(authority): add server-primary capability probe

This commit is contained in:
Youzini-afk
2026-04-28 01:40:38 +08:00
parent c27b7b957d
commit ee9b0afa35
8 changed files with 1101 additions and 0 deletions

187
index.js
View File

@@ -249,6 +249,16 @@ import {
getPersistedSettingsSnapshot,
mergePersistedSettings,
} from "./runtime/settings-defaults.js";
import {
createDefaultAuthorityCapabilityState,
normalizeAuthorityCapabilityState,
probeAuthorityCapabilities,
} from "./runtime/authority-capabilities.js";
import {
createAuthorityBrowserState,
getAuthorityBrowserStateSnapshot,
normalizeAuthorityBrowserState,
} from "./sync/authority-browser-state.js";
import { retrieve } from "./retrieval/retriever.js";
import {
applyProcessedHistorySnapshotToGraph,
@@ -1234,6 +1244,9 @@ let lastExtractionStatus = createUiStatus("待命", "尚未执行提取", "idle"
let lastVectorStatus = createUiStatus("待命", "尚未执行向量任务", "idle");
let lastRecallStatus = createUiStatus("待命", "尚未执行召回", "idle");
let graphPersistenceState = createGraphPersistenceState();
let authorityCapabilityState = createDefaultAuthorityCapabilityState();
let authorityBrowserState = createAuthorityBrowserState();
let authorityProbePromise = null;
const lastStatusToastAt = {};
let pendingRecallSendIntent = createRecallInputRecord();
let lastRecallSentUserMessage = createRecallInputRecord();
@@ -1437,6 +1450,115 @@ function isLukerPrimaryPersistenceHost(context = getContext()) {
return resolvePersistenceHostProfile(context) === "luker";
}
function getAuthorityRuntimeSnapshot(settings = getSettings()) {
authorityCapabilityState = normalizeAuthorityCapabilityState(
authorityCapabilityState,
settings,
);
authorityBrowserState = normalizeAuthorityBrowserState(
authorityBrowserState,
settings,
);
return {
capability: authorityCapabilityState,
browserState: getAuthorityBrowserStateSnapshot(authorityBrowserState, settings),
};
}
function buildAuthorityPersistenceStatePatch(settings = getSettings()) {
const { capability, browserState } = getAuthorityRuntimeSnapshot(settings);
return {
authority: cloneRuntimeDebugValue(capability, null),
authorityBrowserState: cloneRuntimeDebugValue(browserState, null),
authorityInstalled: Boolean(capability.installed),
authorityHealthy: Boolean(capability.healthy),
authorityServerPrimaryReady: Boolean(capability.serverPrimaryReady),
authorityStoragePrimaryReady: Boolean(capability.storagePrimaryReady),
authorityTriviumPrimaryReady: Boolean(capability.triviumPrimaryReady),
authorityBrowserCacheMode: String(browserState.mode || "minimal"),
authorityOfflineQueueBytes: Number(browserState.offlineQueueBytes || 0),
authorityOfflineQueueItems: Number(browserState.offlineQueueItems || 0),
authorityDegradedReason: capability.serverPrimaryReady
? ""
: String(capability.reason || capability.lastError || ""),
};
}
async function refreshAuthorityRuntimeState({
force = false,
source = "authority-refresh",
} = {}) {
if (authorityProbePromise && !force) {
return await authorityProbePromise;
}
const settings = getSettings();
authorityBrowserState = normalizeAuthorityBrowserState(
authorityBrowserState,
settings,
);
updateGraphPersistenceState({
...buildAuthorityPersistenceStatePatch(settings),
authorityLastRefreshSource: String(source || "authority-refresh"),
});
const allowRelativeUrl =
typeof window === "object" &&
Boolean(window?.location) &&
typeof window.location.href === "string";
authorityProbePromise = probeAuthorityCapabilities({
settings,
fetchImpl:
typeof globalThis.fetch === "function"
? globalThis.fetch.bind(globalThis)
: null,
headerProvider:
typeof getRequestHeaders === "function" ? getRequestHeaders : null,
allowRelativeUrl,
nowMs: Date.now(),
})
.then((snapshot) => {
authorityCapabilityState = normalizeAuthorityCapabilityState(
snapshot,
settings,
);
authorityBrowserState = normalizeAuthorityBrowserState(
{
...authorityBrowserState,
lastProbeAt: authorityCapabilityState.lastProbeAt,
lastError: authorityCapabilityState.lastError,
},
settings,
);
updateGraphPersistenceState({
...buildAuthorityPersistenceStatePatch(settings),
authorityLastRefreshSource: String(source || "authority-refresh"),
});
return authorityCapabilityState;
})
.catch((error) => {
authorityCapabilityState = normalizeAuthorityCapabilityState(
{
installed: false,
healthy: false,
reason: "probe-failed",
lastError: error?.message || String(error),
lastProbeAt: Date.now(),
updatedAt: new Date().toISOString(),
},
settings,
);
updateGraphPersistenceState({
...buildAuthorityPersistenceStatePatch(settings),
authorityLastRefreshSource: String(source || "authority-refresh"),
});
return authorityCapabilityState;
})
.finally(() => {
authorityProbePromise = null;
});
return await authorityProbePromise;
}
function getGraphPersistenceLiveState() {
const liveCommitMarker =
cloneRuntimeDebugValue(graphPersistenceState.commitMarker, null) ||
@@ -1452,6 +1574,7 @@ function getGraphPersistenceLiveState() {
adapterRuntime.adapter.hostProfile ||
persistenceEnvironment.hostProfile,
);
const authorityRuntime = getAuthorityRuntimeSnapshot();
const primaryStorageTier = normalizePersistenceStorageTier(
graphPersistenceState.primaryStorageTier ||
persistenceEnvironment.primaryStorageTier,
@@ -1515,6 +1638,38 @@ function getGraphPersistenceLiveState() {
updatedAt: graphPersistenceState.updatedAt,
storagePrimary: graphPersistenceState.storagePrimary || "indexeddb",
storageMode: graphPersistenceState.storageMode || "indexeddb",
authority: cloneRuntimeDebugValue(authorityRuntime.capability, null),
authorityBrowserState: cloneRuntimeDebugValue(
authorityRuntime.browserState,
null,
),
authorityInstalled: Boolean(authorityRuntime.capability.installed),
authorityHealthy: Boolean(authorityRuntime.capability.healthy),
authorityServerPrimaryReady: Boolean(
authorityRuntime.capability.serverPrimaryReady,
),
authorityStoragePrimaryReady: Boolean(
authorityRuntime.capability.storagePrimaryReady,
),
authorityTriviumPrimaryReady: Boolean(
authorityRuntime.capability.triviumPrimaryReady,
),
authorityBrowserCacheMode: String(
authorityRuntime.browserState.mode || "minimal",
),
authorityOfflineQueueBytes: Number(
authorityRuntime.browserState.offlineQueueBytes || 0,
),
authorityOfflineQueueItems: Number(
authorityRuntime.browserState.offlineQueueItems || 0,
),
authorityDegradedReason: authorityRuntime.capability.serverPrimaryReady
? ""
: String(
authorityRuntime.capability.reason ||
authorityRuntime.capability.lastError ||
"",
),
resolvedLocalStore: String(
graphPersistenceState.resolvedLocalStore ||
buildGraphLocalStoreSelectorKey(getPreferredGraphLocalStorePresentationSync()),
@@ -13299,6 +13454,27 @@ function updateModuleSettings(patch = {}) {
]);
const recallUiKeys = new Set(["recallCardUserInputDisplayMode"]);
const noticeUiKeys = new Set(["noticeDisplayMode"]);
const authorityKeys = new Set([
"authorityEnabled",
"authorityBaseUrl",
"authorityPrimaryWhenAvailable",
"authorityStorageMode",
"authorityVectorMode",
"authoritySqlPrimary",
"authorityTriviumPrimary",
"authorityGraphQueryEnabled",
"authorityJobsEnabled",
"authorityBlobCheckpointEnabled",
"authorityBrowserCacheMode",
"authorityOfflineWritePolicy",
"authorityOfflineQueueMaxBytes",
"authorityOfflineQueueMaxItems",
"authorityOfflineQueueMaxAgeMs",
"authorityVectorSyncChunkSize",
"authorityVectorFailOpen",
"authorityDiagnosticsEnabled",
"authorityProbeIntervalMs",
]);
const settings = getSettings();
const previousCloudStorageMode = String(
settings.cloudStorageMode || "automatic",
@@ -13380,6 +13556,13 @@ function updateModuleSettings(patch = {}) {
refreshVisibleStageNotices();
}
if (Object.keys(patch).some((key) => authorityKeys.has(key))) {
void refreshAuthorityRuntimeState({
force: true,
source: "settings-updated",
});
}
const currentGraphLocalStorageMode = getRequestedGraphLocalStorageMode(
settings,
);
@@ -20007,6 +20190,10 @@ async function onCompactLukerSidecar() {
(async function init() {
await loadServerSettings();
void refreshAuthorityRuntimeState({
force: true,
source: "init",
});
const { target, lightweightHostMode, adapter } = syncBmeHostRuntimeFlags(getContext());
updateGraphPersistenceState({
hostProfile: adapter.hostProfile,

View File

@@ -0,0 +1,367 @@
const DEFAULT_AUTHORITY_BASE_URL = "/api/plugins/authority";
const DEFAULT_AUTHORITY_PROBE_INTERVAL_MS = 60000;
const SQL_FEATURES = ["sql", "sql.query", "sql.page", "sql.pageall", "querysql"];
const SQL_MUTATION_FEATURES = ["sql", "sql.mutation", "sql.execute", "sql.write", "sql.transaction"];
const TRIVIUM_FEATURES = ["trivium", "trivium.search", "trivium.query", "trivium.filterwhere", "trivium.bulkupsert"];
const JOB_FEATURES = ["jobs", "jobs.list", "jobs.wait", "events", "sse"];
const BLOB_FEATURES = ["blob", "blob.write", "privatefiles", "private.files", "files.private"];
function toBoolean(value, fallback = false) {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (["true", "1", "yes", "on"].includes(normalized)) return true;
if (["false", "0", "no", "off"].includes(normalized)) return false;
}
return fallback;
}
function clampInteger(value, fallback, min, max) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(numeric)));
}
function normalizeMode(value, fallback, allowed) {
const normalized = String(value ?? fallback).trim().toLowerCase();
return allowed.includes(normalized) ? normalized : fallback;
}
function normalizeFeatureName(value) {
return String(value ?? "").trim().toLowerCase();
}
function addFeature(features, value) {
const normalized = normalizeFeatureName(value);
if (normalized) features.add(normalized);
}
function addFeatureObject(features, value, prefix = "") {
if (!value || typeof value !== "object" || Array.isArray(value)) return;
for (const [key, enabled] of Object.entries(value)) {
if (!enabled) continue;
addFeature(features, key);
if (prefix) addFeature(features, `${prefix}.${key}`);
if (enabled && typeof enabled === "object" && !Array.isArray(enabled)) {
addFeatureObject(features, enabled, prefix ? `${prefix}.${key}` : key);
}
}
}
function hasAnyFeature(features, aliases) {
return aliases.some((alias) => features.has(normalizeFeatureName(alias)));
}
function createFeatureReadiness(features) {
return {
sql: hasAnyFeature(features, SQL_FEATURES),
sqlMutation: hasAnyFeature(features, SQL_MUTATION_FEATURES),
trivium: hasAnyFeature(features, TRIVIUM_FEATURES),
jobs: hasAnyFeature(features, JOB_FEATURES),
blob: hasAnyFeature(features, BLOB_FEATURES),
};
}
function collectMissingFeatures(readiness) {
const missing = [];
if (!readiness.sql) missing.push("sql.query");
if (!readiness.sqlMutation) missing.push("sql.mutation");
if (!readiness.trivium) missing.push("trivium.search");
if (!readiness.jobs) missing.push("jobs");
if (!readiness.blob) missing.push("blob-or-private-files");
return missing;
}
function isRelativeAuthorityUrl(baseUrl) {
return /^\//.test(String(baseUrl || ""));
}
function normalizeLatencyMs(startedAt, finishedAt) {
return Math.max(0, Math.round((Number(finishedAt) - Number(startedAt)) * 10) / 10);
}
function readNowMs() {
if (typeof performance === "object" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
}
export function normalizeAuthorityBaseUrl(baseUrl = DEFAULT_AUTHORITY_BASE_URL) {
const normalized = String(baseUrl || DEFAULT_AUTHORITY_BASE_URL).trim() || DEFAULT_AUTHORITY_BASE_URL;
return normalized.replace(/\/+$/, "");
}
export function normalizeAuthoritySettings(settings = {}) {
const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
const enabledMode = normalizeMode(source.authorityEnabled ?? source.enabledMode, "auto", ["auto", "on", "off", "true", "false"]);
return {
enabledMode: enabledMode === "true" ? "on" : enabledMode === "false" ? "off" : enabledMode,
enabled: enabledMode !== "off" && enabledMode !== "false",
baseUrl: normalizeAuthorityBaseUrl(source.authorityBaseUrl ?? source.baseUrl),
primaryWhenAvailable: toBoolean(source.authorityPrimaryWhenAvailable ?? source.primaryWhenAvailable, true),
storageMode: normalizeMode(source.authorityStorageMode ?? source.storageMode, "auto-server-primary", ["auto-server-primary", "server-primary", "local-primary", "off"]),
vectorMode: normalizeMode(source.authorityVectorMode ?? source.vectorMode, "auto-primary", ["auto-primary", "primary", "local-fallback", "off"]),
sqlPrimary: toBoolean(source.authoritySqlPrimary ?? source.sqlPrimary, true),
triviumPrimary: toBoolean(source.authorityTriviumPrimary ?? source.triviumPrimary, true),
jobsEnabled: toBoolean(source.authorityJobsEnabled ?? source.jobsEnabled, true),
blobCheckpointEnabled: toBoolean(source.authorityBlobCheckpointEnabled ?? source.blobCheckpointEnabled, true),
diagnosticsEnabled: toBoolean(source.authorityDiagnosticsEnabled ?? source.diagnosticsEnabled, true),
failOpen: toBoolean(source.authorityFailOpen ?? source.failOpen, true),
probeIntervalMs: clampInteger(source.authorityProbeIntervalMs ?? source.probeIntervalMs, DEFAULT_AUTHORITY_PROBE_INTERVAL_MS, 1000, 3600000),
};
}
export function buildAuthorityProbeUrls(baseUrl = DEFAULT_AUTHORITY_BASE_URL) {
const normalizedBaseUrl = normalizeAuthorityBaseUrl(baseUrl);
return [
`${normalizedBaseUrl}/v1/diagnostics/probe`,
`${normalizedBaseUrl}/v1/probe`,
`${normalizedBaseUrl}/probe`,
normalizedBaseUrl,
];
}
export function collectAuthorityFeatures(payload = {}) {
const features = new Set();
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
for (const value of Array.isArray(source.features) ? source.features : []) {
addFeature(features, value);
}
for (const value of Array.isArray(source.capabilities) ? source.capabilities : []) {
addFeature(features, value);
}
addFeatureObject(features, source.features);
addFeatureObject(features, source.capabilities);
addFeatureObject(features, source.services);
addFeatureObject(features, source.featureFlags);
addFeatureObject(features, source.flags);
return features;
}
export function createDefaultAuthorityCapabilityState(overrides = {}) {
return {
enabledMode: "auto",
baseUrl: DEFAULT_AUTHORITY_BASE_URL,
installed: false,
healthy: false,
sessionReady: false,
permissionReady: false,
minimumFeatureSetReady: false,
serverPrimaryReady: false,
storagePrimaryReady: false,
triviumPrimaryReady: false,
jobsReady: false,
blobReady: false,
features: [],
missingFeatures: ["sql.query", "sql.mutation", "trivium.search", "jobs", "blob-or-private-files"],
reason: "not-probed",
lastError: "",
endpoint: "",
status: 0,
latencyMs: 0,
lastProbeAt: 0,
updatedAt: "",
...overrides,
};
}
export function normalizeAuthorityCapabilityState(input = {}, settings = {}) {
const normalizedSettings = normalizeAuthoritySettings(settings);
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
const features = new Set((Array.isArray(source.features) ? source.features : []).map(normalizeFeatureName).filter(Boolean));
const readiness = createFeatureReadiness(features);
const missingFeatures = Array.isArray(source.missingFeatures) && source.missingFeatures.length
? source.missingFeatures.map(String)
: collectMissingFeatures(readiness);
const healthy = Boolean(source.healthy);
const sessionReady = source.sessionReady == null ? healthy : Boolean(source.sessionReady);
const permissionReady = source.permissionReady == null ? sessionReady : Boolean(source.permissionReady);
const storagePrimaryReady = healthy && sessionReady && permissionReady && readiness.sql && readiness.sqlMutation;
const triviumPrimaryReady = healthy && sessionReady && permissionReady && readiness.trivium;
const jobsReady = healthy && readiness.jobs;
const blobReady = healthy && readiness.blob;
const minimumFeatureSetReady = storagePrimaryReady && triviumPrimaryReady && jobsReady && blobReady;
const serverPrimaryRequested =
normalizedSettings.enabled &&
normalizedSettings.primaryWhenAvailable &&
normalizedSettings.storageMode !== "local-primary" &&
normalizedSettings.storageMode !== "off";
return createDefaultAuthorityCapabilityState({
...source,
enabledMode: normalizedSettings.enabledMode,
baseUrl: normalizedSettings.baseUrl,
installed: Boolean(source.installed),
healthy,
sessionReady,
permissionReady,
minimumFeatureSetReady,
serverPrimaryReady: serverPrimaryRequested && minimumFeatureSetReady,
storagePrimaryReady,
triviumPrimaryReady,
jobsReady,
blobReady,
features: Array.from(features).sort(),
missingFeatures,
reason: String(source.reason || (healthy ? "ok" : "not-ready")),
lastError: String(source.lastError || ""),
endpoint: String(source.endpoint || ""),
status: clampInteger(source.status, 0, 0, 999),
latencyMs: Math.max(0, Number(source.latencyMs) || 0),
lastProbeAt: Math.max(0, Number(source.lastProbeAt) || 0),
updatedAt: String(source.updatedAt || ""),
});
}
export function normalizeAuthorityProbeResponse(payload = {}, context = {}) {
const settings = normalizeAuthoritySettings(context.settings || {});
const features = collectAuthorityFeatures(payload);
const readiness = createFeatureReadiness(features);
const missingFeatures = collectMissingFeatures(readiness);
const sessionReady = payload?.sessionReady ?? payload?.session?.ready ?? payload?.session?.active ?? true;
const permissionReady = payload?.permissionReady ?? payload?.permissions?.ready ?? payload?.authorized ?? sessionReady;
const healthy = payload?.healthy ?? payload?.ok ?? true;
return normalizeAuthorityCapabilityState(
{
installed: true,
healthy: Boolean(healthy),
sessionReady: Boolean(sessionReady),
permissionReady: Boolean(permissionReady),
features: Array.from(features),
missingFeatures,
reason: missingFeatures.length ? "missing-required-features" : "ok",
endpoint: context.endpoint || "",
status: context.status || 200,
latencyMs: context.latencyMs || 0,
lastProbeAt: context.nowMs || Date.now(),
updatedAt: new Date(context.nowMs || Date.now()).toISOString(),
},
settings,
);
}
export async function probeAuthorityCapabilities(options = {}) {
const settings = normalizeAuthoritySettings(options.settings || {});
const nowMs = Number(options.nowMs) || Date.now();
if (!settings.enabled || settings.storageMode === "off") {
return normalizeAuthorityCapabilityState(
{
reason: "disabled",
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),
},
settings,
);
}
const fetchImpl = options.fetchImpl || (typeof fetch === "function" ? fetch : null);
if (typeof fetchImpl !== "function") {
return normalizeAuthorityCapabilityState(
{
reason: "fetch-unavailable",
lastError: "fetch unavailable",
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),
},
settings,
);
}
if (options.allowRelativeUrl === false && isRelativeAuthorityUrl(settings.baseUrl)) {
return normalizeAuthorityCapabilityState(
{
reason: "relative-url-unavailable",
lastError: "relative Authority URL cannot be probed in this runtime",
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),
},
settings,
);
}
let headers = { Accept: "application/json" };
if (typeof options.headerProvider === "function") {
try {
headers = { ...headers, ...(options.headerProvider() || {}) };
} catch {
headers = { ...headers };
}
}
let lastError = "";
let lastStatus = 0;
for (const endpoint of buildAuthorityProbeUrls(settings.baseUrl)) {
const startedAt = readNowMs();
try {
const response = await fetchImpl(endpoint, { method: "GET", headers });
const finishedAt = readNowMs();
const status = Number(response?.status || 0);
lastStatus = status;
if (status === 404) continue;
if (status === 401 || status === 403) {
return normalizeAuthorityCapabilityState(
{
installed: true,
healthy: false,
sessionReady: false,
permissionReady: false,
reason: "permission-denied",
lastError: `HTTP ${status}`,
endpoint,
status,
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),
},
settings,
);
}
if (!response?.ok) {
return normalizeAuthorityCapabilityState(
{
installed: status > 0,
healthy: false,
reason: "http-error",
lastError: `HTTP ${status || "unknown"}`,
endpoint,
status,
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),
},
settings,
);
}
let payload = {};
try {
payload = typeof response.json === "function" ? await response.json() : {};
} catch {
payload = {};
}
return normalizeAuthorityProbeResponse(payload, {
settings,
endpoint,
status,
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
nowMs,
});
} catch (error) {
lastError = error?.message || String(error);
}
}
return normalizeAuthorityCapabilityState(
{
installed: false,
healthy: false,
reason: lastStatus === 404 ? "not-installed" : "probe-failed",
lastError,
status: lastStatus,
lastProbeAt: nowMs,
updatedAt: new Date(nowMs).toISOString(),
},
settings,
);
}

View File

@@ -117,6 +117,26 @@ export const defaultSettings = {
embeddingBackendApiUrl: "",
embeddingAutoSuffix: true,
authorityEnabled: "auto",
authorityBaseUrl: "/api/plugins/authority",
authorityPrimaryWhenAvailable: true,
authorityStorageMode: "auto-server-primary",
authorityVectorMode: "auto-primary",
authoritySqlPrimary: true,
authorityTriviumPrimary: true,
authorityGraphQueryEnabled: true,
authorityJobsEnabled: true,
authorityBlobCheckpointEnabled: true,
authorityBrowserCacheMode: "minimal",
authorityOfflineWritePolicy: "queue-local-dirty",
authorityOfflineQueueMaxBytes: 1048576,
authorityOfflineQueueMaxItems: 128,
authorityOfflineQueueMaxAgeMs: 3600000,
authorityVectorSyncChunkSize: 1000,
authorityVectorFailOpen: true,
authorityDiagnosticsEnabled: true,
authorityProbeIntervalMs: 60000,
// Native 性能加速(灰度)
graphUseNativeLayout: true,
graphNativeLayoutThresholdNodes: 280,

View File

@@ -0,0 +1,205 @@
const DEFAULT_MAX_BYTES = 1024 * 1024;
const DEFAULT_MAX_ITEMS = 128;
const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;
function clampInteger(value, fallback, min, max) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(numeric)));
}
function normalizeMode(value = "minimal") {
const normalized = String(value || "minimal").trim().toLowerCase();
return ["minimal", "off", "emergency-snapshot"].includes(normalized)
? normalized
: "minimal";
}
function estimateJsonBytes(value) {
let serialized = "";
try {
serialized = JSON.stringify(value ?? null) || "null";
} catch {
serialized = String(value ?? "");
}
if (typeof TextEncoder === "function") {
return new TextEncoder().encode(serialized).byteLength;
}
return serialized.length * 2;
}
function normalizeQueueItem(item = {}, nowMs = Date.now()) {
const source = item && typeof item === "object" && !Array.isArray(item) ? item : {};
const payload = source.payload && typeof source.payload === "object" ? source.payload : source;
const rawCreatedAt = Number(source.createdAt ?? nowMs);
const createdAt = Number.isFinite(rawCreatedAt)
? Math.max(0, rawCreatedAt)
: nowMs;
const rawBytes = Number(source.bytes ?? estimateJsonBytes(payload));
const bytes = Number.isFinite(rawBytes) ? Math.max(0, rawBytes) : 0;
return {
id: String(source.id || `offline-${createdAt}-${Math.random().toString(36).slice(2, 8)}`),
kind: String(source.kind || "mutation"),
reason: String(source.reason || "authority-offline"),
createdAt,
bytes,
payload,
};
}
function pruneQueue(queue = [], policy = {}, nowMs = Date.now()) {
const maxAgeMs = Math.max(0, Number(policy.maxAgeMs || DEFAULT_MAX_AGE_MS) || 0);
if (!Array.isArray(queue)) return [];
return queue
.map((item) => normalizeQueueItem(item, nowMs))
.filter((item) => maxAgeMs <= 0 || nowMs - item.createdAt <= maxAgeMs)
.sort((left, right) => left.createdAt - right.createdAt);
}
function summarizeQueue(queue = []) {
const items = Array.isArray(queue) ? queue : [];
const bytes = items.reduce((sum, item) => sum + Math.max(0, Number(item?.bytes || 0) || 0), 0);
return {
items: items.length,
bytes,
};
}
export function getAuthorityBrowserStoragePolicy(settings = {}) {
const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
return {
mode: normalizeMode(source.authorityBrowserCacheMode),
offlineWritePolicy: String(source.authorityOfflineWritePolicy || "queue-local-dirty"),
maxBytes: clampInteger(source.authorityOfflineQueueMaxBytes, DEFAULT_MAX_BYTES, 0, 64 * 1024 * 1024),
maxItems: clampInteger(source.authorityOfflineQueueMaxItems, DEFAULT_MAX_ITEMS, 0, 100000),
maxAgeMs: clampInteger(source.authorityOfflineQueueMaxAgeMs, DEFAULT_MAX_AGE_MS, 0, 30 * 24 * 60 * 60 * 1000),
};
}
export function createAuthorityBrowserState(overrides = {}) {
return {
mode: "minimal",
serverRevision: 0,
serverIntegrity: "",
lastProbeAt: 0,
lastCommitAt: 0,
lastError: "",
offlineQueue: [],
offlineQueueBytes: 0,
offlineQueueItems: 0,
offlineQueueOverflow: false,
offlineQueueOverflowReason: "",
updatedAt: "",
...overrides,
};
}
export function normalizeAuthorityBrowserState(input = {}, settings = {}, nowMs = Date.now()) {
const policy = getAuthorityBrowserStoragePolicy(settings);
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
const queue = pruneQueue(source.offlineQueue, policy, nowMs);
const summary = summarizeQueue(queue);
return createAuthorityBrowserState({
mode: policy.mode,
serverRevision: Math.max(0, Number(source.serverRevision || 0) || 0),
serverIntegrity: String(source.serverIntegrity || ""),
lastProbeAt: Math.max(0, Number(source.lastProbeAt || 0) || 0),
lastCommitAt: Math.max(0, Number(source.lastCommitAt || 0) || 0),
lastError: String(source.lastError || ""),
offlineQueue: queue,
offlineQueueBytes: summary.bytes,
offlineQueueItems: summary.items,
offlineQueueOverflow: Boolean(source.offlineQueueOverflow),
offlineQueueOverflowReason: String(source.offlineQueueOverflowReason || ""),
updatedAt: String(source.updatedAt || ""),
});
}
export function recordAuthorityAcceptedRevision(state = {}, accepted = {}, settings = {}, nowMs = Date.now()) {
const current = normalizeAuthorityBrowserState(state, settings, nowMs);
return createAuthorityBrowserState({
...current,
serverRevision: Math.max(current.serverRevision, Number(accepted.revision || 0) || 0),
serverIntegrity: String(accepted.integrity || current.serverIntegrity || ""),
lastCommitAt: Math.max(0, Number(accepted.committedAt || nowMs) || nowMs),
lastError: "",
updatedAt: new Date(nowMs).toISOString(),
});
}
export function enqueueAuthorityOfflineMutation(state = {}, mutation = {}, settings = {}, nowMs = Date.now()) {
const policy = getAuthorityBrowserStoragePolicy(settings);
const current = normalizeAuthorityBrowserState(state, settings, nowMs);
if (policy.mode === "off" || policy.offlineWritePolicy === "off") {
const nextState = createAuthorityBrowserState({
...current,
offlineQueueOverflow: true,
offlineQueueOverflowReason: "offline-queue-disabled",
updatedAt: new Date(nowMs).toISOString(),
});
return { accepted: false, reason: "offline-queue-disabled", state: nextState };
}
const item = normalizeQueueItem(mutation, nowMs);
const nextItems = [...current.offlineQueue, item];
const nextSummary = summarizeQueue(nextItems);
if (policy.maxItems > 0 && nextSummary.items > policy.maxItems) {
const nextState = createAuthorityBrowserState({
...current,
offlineQueueOverflow: true,
offlineQueueOverflowReason: "max-items-exceeded",
updatedAt: new Date(nowMs).toISOString(),
});
return { accepted: false, reason: "max-items-exceeded", state: nextState };
}
if (policy.maxBytes > 0 && nextSummary.bytes > policy.maxBytes) {
const nextState = createAuthorityBrowserState({
...current,
offlineQueueOverflow: true,
offlineQueueOverflowReason: "max-bytes-exceeded",
updatedAt: new Date(nowMs).toISOString(),
});
return { accepted: false, reason: "max-bytes-exceeded", state: nextState };
}
const nextState = createAuthorityBrowserState({
...current,
offlineQueue: nextItems,
offlineQueueBytes: nextSummary.bytes,
offlineQueueItems: nextSummary.items,
offlineQueueOverflow: false,
offlineQueueOverflowReason: "",
updatedAt: new Date(nowMs).toISOString(),
});
return { accepted: true, reason: "queued", item, state: nextState };
}
export function clearAuthorityOfflineQueue(state = {}, settings = {}, nowMs = Date.now()) {
const current = normalizeAuthorityBrowserState(state, settings, nowMs);
return createAuthorityBrowserState({
...current,
offlineQueue: [],
offlineQueueBytes: 0,
offlineQueueItems: 0,
offlineQueueOverflow: false,
offlineQueueOverflowReason: "",
updatedAt: new Date(nowMs).toISOString(),
});
}
export function getAuthorityBrowserStateSnapshot(state = {}, settings = {}, nowMs = Date.now()) {
const normalized = normalizeAuthorityBrowserState(state, settings, nowMs);
return {
mode: normalized.mode,
serverRevision: normalized.serverRevision,
serverIntegrity: normalized.serverIntegrity,
lastProbeAt: normalized.lastProbeAt,
lastCommitAt: normalized.lastCommitAt,
lastError: normalized.lastError,
offlineQueueBytes: normalized.offlineQueueBytes,
offlineQueueItems: normalized.offlineQueueItems,
offlineQueueOverflow: normalized.offlineQueueOverflow,
offlineQueueOverflowReason: normalized.offlineQueueOverflowReason,
updatedAt: normalized.updatedAt,
};
}

View File

@@ -0,0 +1,142 @@
import assert from "node:assert/strict";
import {
clearAuthorityOfflineQueue,
enqueueAuthorityOfflineMutation,
getAuthorityBrowserStateSnapshot,
getAuthorityBrowserStoragePolicy,
normalizeAuthorityBrowserState,
recordAuthorityAcceptedRevision,
} from "../sync/authority-browser-state.js";
import { defaultSettings } from "../runtime/settings-defaults.js";
const policy = getAuthorityBrowserStoragePolicy(defaultSettings);
assert.equal(policy.mode, "minimal");
assert.equal(policy.offlineWritePolicy, "queue-local-dirty");
assert.equal(policy.maxBytes, 1048576);
assert.equal(policy.maxItems, 128);
assert.equal(policy.maxAgeMs, 3600000);
const normalized = normalizeAuthorityBrowserState(
{
serverRevision: 7,
serverIntegrity: "abc",
offlineQueue: [
{
id: "old",
createdAt: 0,
bytes: 10,
payload: { a: 1 },
},
{
id: "fresh",
createdAt: 9000,
bytes: 20,
payload: { b: 2 },
},
],
},
{
...defaultSettings,
authorityOfflineQueueMaxAgeMs: 1000,
},
10000,
);
assert.equal(normalized.serverRevision, 7);
assert.equal(normalized.serverIntegrity, "abc");
assert.equal(normalized.offlineQueueItems, 1);
assert.equal(normalized.offlineQueueBytes, 20);
assert.equal(normalized.offlineQueue[0].id, "fresh");
const acceptedRevision = recordAuthorityAcceptedRevision(
normalized,
{
revision: 11,
integrity: "server-integrity",
},
defaultSettings,
12000,
);
assert.equal(acceptedRevision.serverRevision, 11);
assert.equal(acceptedRevision.serverIntegrity, "server-integrity");
assert.equal(acceptedRevision.lastCommitAt, 12000);
assert.equal(acceptedRevision.offlineQueueItems, 1);
const enqueueResult = enqueueAuthorityOfflineMutation(
acceptedRevision,
{
id: "mutation-1",
kind: "commitDelta",
payload: { upsertNodes: [{ id: "n1" }] },
},
{
...defaultSettings,
authorityOfflineQueueMaxItems: 3,
},
13000,
);
assert.equal(enqueueResult.accepted, true);
assert.equal(enqueueResult.state.offlineQueueItems, 2);
assert.equal(enqueueResult.state.offlineQueueOverflow, false);
const itemOverflow = enqueueAuthorityOfflineMutation(
enqueueResult.state,
{
id: "mutation-overflow",
payload: { upsertNodes: [{ id: "n2" }] },
},
{
...defaultSettings,
authorityOfflineQueueMaxItems: 1,
},
14000,
);
assert.equal(itemOverflow.accepted, false);
assert.equal(itemOverflow.reason, "max-items-exceeded");
assert.equal(itemOverflow.state.offlineQueueItems, 2);
assert.equal(itemOverflow.state.offlineQueueOverflow, true);
const byteOverflow = enqueueAuthorityOfflineMutation(
{},
{
id: "large-mutation",
payload: { text: "x".repeat(64) },
},
{
...defaultSettings,
authorityOfflineQueueMaxBytes: 8,
},
15000,
);
assert.equal(byteOverflow.accepted, false);
assert.equal(byteOverflow.reason, "max-bytes-exceeded");
assert.equal(byteOverflow.state.offlineQueueItems, 0);
assert.equal(byteOverflow.state.offlineQueueOverflow, true);
const disabled = enqueueAuthorityOfflineMutation(
{},
{
id: "disabled-mutation",
payload: { a: 1 },
},
{
...defaultSettings,
authorityBrowserCacheMode: "off",
},
16000,
);
assert.equal(disabled.accepted, false);
assert.equal(disabled.reason, "offline-queue-disabled");
const cleared = clearAuthorityOfflineQueue(enqueueResult.state, defaultSettings, 17000);
assert.equal(cleared.offlineQueueItems, 0);
assert.equal(cleared.offlineQueueBytes, 0);
assert.equal(cleared.offlineQueueOverflow, false);
const snapshot = getAuthorityBrowserStateSnapshot(acceptedRevision, defaultSettings, 18000);
assert.equal(snapshot.serverRevision, 11);
assert.equal(snapshot.serverIntegrity, "server-integrity");
assert.equal(snapshot.offlineQueueItems, 1);
assert.equal("offlineQueue" in snapshot, false);
console.log("authority-browser-state tests passed");

View File

@@ -0,0 +1,116 @@
import assert from "node:assert/strict";
import {
buildAuthorityProbeUrls,
collectAuthorityFeatures,
normalizeAuthorityCapabilityState,
normalizeAuthoritySettings,
probeAuthorityCapabilities,
} from "../runtime/authority-capabilities.js";
import { defaultSettings } from "../runtime/settings-defaults.js";
const normalizedSettings = normalizeAuthoritySettings(defaultSettings);
assert.equal(normalizedSettings.enabled, true);
assert.equal(normalizedSettings.enabledMode, "auto");
assert.equal(normalizedSettings.baseUrl, "/api/plugins/authority");
assert.equal(normalizedSettings.storageMode, "auto-server-primary");
assert.equal(normalizedSettings.vectorMode, "auto-primary");
assert.equal(normalizedSettings.primaryWhenAvailable, true);
assert.deepEqual(buildAuthorityProbeUrls("/api/plugins/authority/"), [
"/api/plugins/authority/v1/diagnostics/probe",
"/api/plugins/authority/v1/probe",
"/api/plugins/authority/probe",
"/api/plugins/authority",
]);
const collected = collectAuthorityFeatures({
features: ["sql.query", "trivium.search"],
services: {
sql: true,
jobs: true,
blob: true,
},
});
assert.equal(collected.has("sql.query"), true);
assert.equal(collected.has("trivium.search"), true);
assert.equal(collected.has("sql"), true);
assert.equal(collected.has("jobs"), true);
assert.equal(collected.has("blob"), true);
const readyState = normalizeAuthorityCapabilityState(
{
installed: true,
healthy: true,
features: ["sql", "trivium", "jobs", "blob"],
},
defaultSettings,
);
assert.equal(readyState.serverPrimaryReady, true);
assert.equal(readyState.storagePrimaryReady, true);
assert.equal(readyState.triviumPrimaryReady, true);
assert.equal(readyState.minimumFeatureSetReady, true);
const missingState = normalizeAuthorityCapabilityState(
{
installed: true,
healthy: true,
features: ["sql"],
},
defaultSettings,
);
assert.equal(missingState.serverPrimaryReady, false);
assert.equal(missingState.triviumPrimaryReady, false);
assert.ok(missingState.missingFeatures.includes("trivium.search"));
const disabledState = await probeAuthorityCapabilities({
settings: {
...defaultSettings,
authorityEnabled: "off",
},
fetchImpl: async () => {
throw new Error("should-not-fetch");
},
nowMs: 1000,
});
assert.equal(disabledState.reason, "disabled");
assert.equal(disabledState.serverPrimaryReady, false);
assert.equal(disabledState.lastProbeAt, 1000);
let requestedUrl = "";
const probedState = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: true,
nowMs: 2000,
fetchImpl: async (url) => {
requestedUrl = url;
return {
ok: true,
status: 200,
async json() {
return {
healthy: true,
sessionReady: true,
permissionReady: true,
features: ["sql", "trivium", "jobs", "blob"],
};
},
};
},
});
assert.equal(requestedUrl, "/api/plugins/authority/v1/diagnostics/probe");
assert.equal(probedState.installed, true);
assert.equal(probedState.healthy, true);
assert.equal(probedState.serverPrimaryReady, true);
assert.equal(probedState.lastProbeAt, 2000);
const relativeUnavailable = await probeAuthorityCapabilities({
settings: defaultSettings,
allowRelativeUrl: false,
fetchImpl: async () => ({ ok: true, status: 200, json: async () => ({}) }),
nowMs: 3000,
});
assert.equal(relativeUnavailable.reason, "relative-url-unavailable");
assert.equal(relativeUnavailable.serverPrimaryReady, false);
console.log("authority-capabilities tests passed");

View File

@@ -68,6 +68,25 @@ assert.equal(defaultSettings.worldInfoFilterMode, "default");
assert.equal(defaultSettings.worldInfoFilterCustomKeywords, "");
assert.equal("maintenanceAutoMinNewNodes" in defaultSettings, false);
assert.equal(defaultSettings.embeddingTransportMode, "direct");
assert.equal(defaultSettings.authorityEnabled, "auto");
assert.equal(defaultSettings.authorityBaseUrl, "/api/plugins/authority");
assert.equal(defaultSettings.authorityPrimaryWhenAvailable, true);
assert.equal(defaultSettings.authorityStorageMode, "auto-server-primary");
assert.equal(defaultSettings.authorityVectorMode, "auto-primary");
assert.equal(defaultSettings.authoritySqlPrimary, true);
assert.equal(defaultSettings.authorityTriviumPrimary, true);
assert.equal(defaultSettings.authorityGraphQueryEnabled, true);
assert.equal(defaultSettings.authorityJobsEnabled, true);
assert.equal(defaultSettings.authorityBlobCheckpointEnabled, true);
assert.equal(defaultSettings.authorityBrowserCacheMode, "minimal");
assert.equal(defaultSettings.authorityOfflineWritePolicy, "queue-local-dirty");
assert.equal(defaultSettings.authorityOfflineQueueMaxBytes, 1048576);
assert.equal(defaultSettings.authorityOfflineQueueMaxItems, 128);
assert.equal(defaultSettings.authorityOfflineQueueMaxAgeMs, 3600000);
assert.equal(defaultSettings.authorityVectorSyncChunkSize, 1000);
assert.equal(defaultSettings.authorityVectorFailOpen, true);
assert.equal(defaultSettings.authorityDiagnosticsEnabled, true);
assert.equal(defaultSettings.authorityProbeIntervalMs, 60000);
assert.equal(defaultSettings.graphUseNativeLayout, true);
assert.equal(defaultSettings.graphNativeLayoutThresholdNodes, 280);
assert.equal(defaultSettings.graphNativeLayoutThresholdEdges, 1600);

View File

@@ -96,6 +96,51 @@ export function createGraphPersistenceState() {
storagePrimary: "indexeddb",
storageMode: "indexeddb",
resolvedLocalStore: "indexeddb:indexeddb",
authority: {
enabledMode: "auto",
baseUrl: "/api/plugins/authority",
installed: false,
healthy: false,
sessionReady: false,
permissionReady: false,
minimumFeatureSetReady: false,
serverPrimaryReady: false,
storagePrimaryReady: false,
triviumPrimaryReady: false,
jobsReady: false,
blobReady: false,
features: [],
missingFeatures: [],
reason: "not-probed",
lastError: "",
endpoint: "",
status: 0,
latencyMs: 0,
lastProbeAt: 0,
updatedAt: "",
},
authorityBrowserState: {
mode: "minimal",
serverRevision: 0,
serverIntegrity: "",
lastProbeAt: 0,
lastCommitAt: 0,
lastError: "",
offlineQueueBytes: 0,
offlineQueueItems: 0,
offlineQueueOverflow: false,
offlineQueueOverflowReason: "",
updatedAt: "",
},
authorityInstalled: false,
authorityHealthy: false,
authorityServerPrimaryReady: false,
authorityStoragePrimaryReady: false,
authorityTriviumPrimaryReady: false,
authorityBrowserCacheMode: "minimal",
authorityOfflineQueueBytes: 0,
authorityOfflineQueueItems: 0,
authorityDegradedReason: "",
localStoreFormatVersion: 1,
localStoreMigrationState: "idle",
opfsWriteLockState: {