mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
perf(status): offload memory-presence inspection (#150988)
This commit is contained in:
parent
a1cc9b38c1
commit
db46918013
12 changed files with 359 additions and 86 deletions
|
|
@ -2411,7 +2411,6 @@ src/commands/setup.ts 1
|
|||
src/commands/status-all/channels.ts 2
|
||||
src/commands/status-all/gateway.ts 1
|
||||
src/commands/status.scan.bootstrap-shared.ts 1
|
||||
src/commands/status.scan.shared.ts 1
|
||||
src/commands/system-agent-with-inference.ts 2
|
||||
src/commands/tasks-json.ts 2
|
||||
src/commands/tasks.ts 2
|
||||
|
|
|
|||
|
|
@ -212,6 +212,14 @@ Only pending reads coalesce; completed results are not cached. Physical integrit
|
|||
verification remains with full registry restoration and Doctor, while known
|
||||
database failures and quarantine still refuse summary reads.
|
||||
|
||||
Offline `status --json --all` checks for existing built-in memory data through
|
||||
memory-core's retrieval worker before constructing a memory manager. The check
|
||||
retains current and shipped table recognition, missing-store behavior, and
|
||||
best-effort read failures without creating or migrating a database. Custom memory
|
||||
slots and explicitly configured memory retain their existing selection paths.
|
||||
This moves only the presence check; memory-manager diagnostics keep their own
|
||||
lifecycle and execution contracts.
|
||||
|
||||
Gateway, embedded, and TUI session lists use resident materialized rows and the
|
||||
subagent registry's owner-maintained memory snapshot. Each durable session store
|
||||
is hydrated when first admitted, replaced, or reintroduced; departing stores lose
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export type { PreviewRemHarnessOptions, PreviewRemHarnessResult } from "./src/re
|
|||
export { configureMemoryCoreDreamingState } from "./src/dreaming-state.js";
|
||||
export { filterMemorySearchHitsBySessionVisibility } from "./src/session-search-visibility.js";
|
||||
export { captureMemoryRebuildNotice } from "./src/memory-rebuild-notice.js";
|
||||
export { inspectMemoryIndexPresence } from "./src/memory/manager-status-presence.runtime.js";
|
||||
export {
|
||||
MEMORY_MANAGED_LOCAL_EMBEDDING_SETUP_CHECK_ID,
|
||||
pluginStateIsolatedDoctorCheckIds,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ const indexing = new WorkerTaskPool<MemoryIndexTask, MemoryIndexTaskResult>({
|
|||
|
||||
type MemoryReadTarget = { databasePath: string; agentId: string };
|
||||
|
||||
export async function runMemoryPresenceInspection(databasePath: string): Promise<boolean> {
|
||||
ensureSqliteLibrarySelected();
|
||||
const result = await retrieval.run(
|
||||
{ kind: "presence", databasePath },
|
||||
{ inputBytes: databasePath.length * 2 },
|
||||
);
|
||||
if (result.kind !== "presence") {
|
||||
throw new Error("Invalid memory presence worker result");
|
||||
}
|
||||
return result.present;
|
||||
}
|
||||
|
||||
export async function runMemoryKeywordSearch(
|
||||
target: MemoryReadTarget,
|
||||
query: MemoryKeywordWorkerQuery,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { openOpenClawAgentDatabaseReadOnly } from "openclaw/plugin-sdk/memory-co
|
|||
import { serveWorkerTasks } from "openclaw/plugin-sdk/process-runtime";
|
||||
import { bm25RankToScore, buildFtsQuery } from "./keyword-query.js";
|
||||
import { searchChunksByEmbedding, searchKeyword, searchPathKeyword } from "./manager-search.js";
|
||||
import { inspectMemoryIndexPresenceInWorker } from "./manager-status-presence.js";
|
||||
|
||||
type KeywordParameters = Omit<
|
||||
Parameters<typeof searchKeyword>[0],
|
||||
|
|
@ -17,15 +18,15 @@ export type MemoryVectorWorkerQuery = Omit<
|
|||
Parameters<typeof searchChunksByEmbedding>[0],
|
||||
"db" | "signal"
|
||||
>;
|
||||
export type MemorySearchWorkerInput = {
|
||||
databasePath: string;
|
||||
agentId: string;
|
||||
} & (
|
||||
| { kind: "keyword"; query: MemoryKeywordWorkerQuery }
|
||||
| { kind: "vector"; query: MemoryVectorWorkerQuery }
|
||||
);
|
||||
export type MemorySearchWorkerInput =
|
||||
| { kind: "presence"; databasePath: string }
|
||||
| ({ databasePath: string; agentId: string } & (
|
||||
| { kind: "keyword"; query: MemoryKeywordWorkerQuery }
|
||||
| { kind: "vector"; query: MemoryVectorWorkerQuery }
|
||||
));
|
||||
type QueryResult<T> = { rows: T; error?: string };
|
||||
export type MemorySearchWorkerOutput =
|
||||
| { kind: "presence"; present: boolean }
|
||||
| {
|
||||
kind: "keyword";
|
||||
body: QueryResult<Awaited<ReturnType<typeof searchKeyword>>>;
|
||||
|
|
@ -34,8 +35,12 @@ export type MemorySearchWorkerOutput =
|
|||
| { kind: "vector"; rows: Awaited<ReturnType<typeof searchChunksByEmbedding>> };
|
||||
|
||||
serveWorkerTasks(async (input): Promise<MemorySearchWorkerOutput> => {
|
||||
// SAFETY: The paired runtime constructs the request; the canonical reader validates the database owner.
|
||||
// SAFETY: The paired runtime constructs the private request union.
|
||||
const request = input as MemorySearchWorkerInput;
|
||||
if (request.kind === "presence") {
|
||||
// This pre-manager probe also recognizes shipped memory-only databases.
|
||||
return { kind: "presence", present: inspectMemoryIndexPresenceInWorker(request.databasePath) };
|
||||
}
|
||||
const opened = openOpenClawAgentDatabaseReadOnly({
|
||||
agentId: request.agentId,
|
||||
path: request.databasePath,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
export async function inspectMemoryIndexPresence(databasePath: string): Promise<boolean> {
|
||||
const { runMemoryPresenceInspection } = await import("./manager-cpu-worker-runtime.js");
|
||||
try {
|
||||
return await runMemoryPresenceInspection(databasePath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
64
extensions/memory-core/src/memory/manager-status-presence.ts
Normal file
64
extensions/memory-core/src/memory/manager-status-presence.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/memory-core-host-engine-knn";
|
||||
import {
|
||||
MEMORY_INDEX_CHUNKS_TABLE,
|
||||
MEMORY_INDEX_META_TABLE,
|
||||
MEMORY_INDEX_SOURCES_TABLE,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-schema";
|
||||
|
||||
const MEMORY_INDEX_META_KEY = "memory_index_meta_v1";
|
||||
|
||||
/** Inspect existing memory tables without constructing or migrating a manager. */
|
||||
export function inspectMemoryIndexPresenceInWorker(databasePath: string): boolean {
|
||||
let db: DatabaseSync | undefined;
|
||||
try {
|
||||
db = openNodeSqliteDatabase(databasePath, { readOnly: true });
|
||||
const builtInMemoryTableSets = [
|
||||
{
|
||||
meta: MEMORY_INDEX_META_TABLE,
|
||||
sources: MEMORY_INDEX_SOURCES_TABLE,
|
||||
chunks: MEMORY_INDEX_CHUNKS_TABLE,
|
||||
},
|
||||
{ meta: "meta", sources: "files", chunks: "chunks" },
|
||||
] as const;
|
||||
const builtInMemoryTables = builtInMemoryTableSets.flatMap(({ meta, sources, chunks }) => [
|
||||
meta,
|
||||
sources,
|
||||
chunks,
|
||||
]);
|
||||
const tableNames = new Set(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${builtInMemoryTables.map(() => "?").join(", ")})`,
|
||||
)
|
||||
.all(...builtInMemoryTables)
|
||||
.map((row) => row.name)
|
||||
.filter((name): name is string => typeof name === "string"),
|
||||
);
|
||||
for (const tables of builtInMemoryTableSets) {
|
||||
if (
|
||||
tableNames.has(tables.meta) &&
|
||||
db
|
||||
.prepare(`SELECT 1 AS ok FROM ${tables.meta} WHERE key = ? LIMIT 1`)
|
||||
.get(MEMORY_INDEX_META_KEY)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
for (const tableName of [tables.sources, tables.chunks]) {
|
||||
if (
|
||||
tableNames.has(tableName) &&
|
||||
db.prepare(`SELECT 1 AS ok FROM ${tableName} LIMIT 1`).get()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
db?.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
1
extensions/memory-core/status-api.ts
Normal file
1
extensions/memory-core/status-api.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { inspectMemoryIndexPresence } from "./src/memory/manager-status-presence.runtime.js";
|
||||
|
|
@ -105,7 +105,7 @@ const rawSqliteAllowPathGroups = {
|
|||
],
|
||||
"read-only SQLite status probes": [
|
||||
"src/commands/doctor-db-bloat.read.ts",
|
||||
"src/commands/status.scan.shared.ts",
|
||||
"extensions/memory-core/src/memory/manager-status-presence.ts",
|
||||
],
|
||||
"doctor SQLite maintenance and legacy state migration": [
|
||||
"src/commands/doctor-agent-memory-schema.ts",
|
||||
|
|
|
|||
240
src/commands/status.memory-presence.worker.test.ts
Normal file
240
src/commands/status.memory-presence.worker.test.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync, StatementSync } from "node:sqlite";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, expect, it, vi } from "vitest";
|
||||
import { buildMemoryIndexStrictSchema } from "../../packages/memory-host-sdk/src/host/memory-schema-base.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import type { OutputRuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
applyStatusScanDefaults,
|
||||
createStatusMemorySearchManager,
|
||||
createStatusScanSharedMocks,
|
||||
loadStatusScanModuleForTest,
|
||||
} from "./status.scan.test-helpers.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker((cleanup) =>
|
||||
afterEach(async () => {
|
||||
try {
|
||||
const pools = [...(poolRun?.pools ?? [])];
|
||||
await Promise.all(pools.map((pool) => pool.rotate()));
|
||||
for (const pool of pools) {
|
||||
expect(pool.isClosed).toBe(false);
|
||||
expect(pool.getSnapshot()).toMatchObject({ workers: 0, activeTasks: 0, pendingTasks: 0 });
|
||||
}
|
||||
cleanup();
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
}),
|
||||
);
|
||||
const mocks = {
|
||||
...createStatusScanSharedMocks("memory-presence-worker"),
|
||||
callGateway: vi.fn(async () => null),
|
||||
resolveMemorySearchConfig: vi.fn(),
|
||||
};
|
||||
const writeJson = vi.fn<(value: unknown, space?: number) => void>();
|
||||
const runtime: OutputRuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
writeStdout: vi.fn(),
|
||||
writeJson,
|
||||
};
|
||||
let statusJsonCommand: typeof import("./status-json.js").statusJsonCommand;
|
||||
let sqliteOwner: typeof import("../infra/node-sqlite.js");
|
||||
let poolRun: Awaited<ReturnType<typeof observeWorkerPools>> | undefined;
|
||||
|
||||
async function observeWorkerPools() {
|
||||
const { resolveBundledPublicSurfaceLocation } = await import("../plugin-sdk/facade-loader.js");
|
||||
const { getCachedPluginModuleLoader } = await import("../plugins/plugin-module-loader-cache.js");
|
||||
const { preparePluginLoaderAliases } = await import("../plugins/sdk-alias.js");
|
||||
const location = resolveBundledPublicSurfaceLocation({
|
||||
dirName: "memory-core",
|
||||
artifactBasename: "status-api.js",
|
||||
preferSource: false,
|
||||
});
|
||||
if (!location) {
|
||||
throw new Error("Memory status artifact was not resolved");
|
||||
}
|
||||
// The public artifact uses its native module graph, outside Vitest's module cache.
|
||||
const facadeUrl = new URL("../plugin-sdk/facade-loader.ts", import.meta.url).href;
|
||||
const loader = getCachedPluginModuleLoader({
|
||||
modulePath: location.modulePath,
|
||||
importerUrl: facadeUrl,
|
||||
loaderFilename: facadeUrl,
|
||||
preferBuiltDist: true,
|
||||
});
|
||||
const sdkPath = preparePluginLoaderAliases({
|
||||
modulePath: location.modulePath,
|
||||
moduleUrl: facadeUrl,
|
||||
argv1: process.argv[1],
|
||||
}).resolveAlias("openclaw/plugin-sdk/process-runtime");
|
||||
if (!sdkPath) {
|
||||
throw new Error("Memory worker SDK was not resolved");
|
||||
}
|
||||
const { WorkerTaskPool } = loader(sdkPath) as typeof import("../plugin-sdk/process-runtime.js");
|
||||
type Pool = InstanceType<typeof WorkerTaskPool>;
|
||||
const pools = new Set<Pool>();
|
||||
let nextFailure: Error | undefined;
|
||||
// oxlint-disable-next-line typescript/unbound-method -- Every intercepted call supplies the actual pool receiver via apply.
|
||||
const originalRun = WorkerTaskPool.prototype.run;
|
||||
const run = vi.spyOn(WorkerTaskPool.prototype, "run").mockImplementation(function (
|
||||
this: Pool,
|
||||
...args: Parameters<typeof originalRun>
|
||||
) {
|
||||
pools.add(this);
|
||||
if (nextFailure) {
|
||||
const failure = nextFailure;
|
||||
nextFailure = undefined;
|
||||
return Promise.reject(failure);
|
||||
}
|
||||
return originalRun.apply(this, args);
|
||||
});
|
||||
return {
|
||||
run,
|
||||
pools,
|
||||
rejectNextInspection(error: Error) {
|
||||
nextFailure = error;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
applyStatusScanDefaults(mocks);
|
||||
await loadStatusScanModuleForTest(mocks, { fastJson: true });
|
||||
vi.doMock("../gateway/connection-details.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../gateway/connection-details.js")>(
|
||||
"../gateway/connection-details.js",
|
||||
)),
|
||||
buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
|
||||
buildGatewayConnectionDetailsWithResolvers: mocks.buildGatewayConnectionDetails,
|
||||
}));
|
||||
vi.doMock("./status-runtime-shared.ts", () => ({
|
||||
resolveStatusRuntimeSnapshot: async () => ({
|
||||
gatewayService: { label: "test-service", installed: false },
|
||||
nodeService: { label: "test-service", installed: false },
|
||||
}),
|
||||
}));
|
||||
vi.doMock("../state/backup-run-records.js", () => ({
|
||||
readBackupRunFreshness: async () => ({}),
|
||||
}));
|
||||
vi.doMock("../infra/update-run-status.js", () => ({ readUpdateRunStatus: () => ({}) }));
|
||||
vi.resetModules();
|
||||
({ statusJsonCommand } = await import("./status-json.js"));
|
||||
sqliteOwner = await import("../infra/node-sqlite.js");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
for (const mock of Object.values(mocks)) {
|
||||
mock.mockClear();
|
||||
}
|
||||
writeJson.mockClear();
|
||||
const memoryManager = createStatusMemorySearchManager();
|
||||
memoryManager.manager.status.mockReturnValue({ files: 1, chunks: 0, dirty: false });
|
||||
applyStatusScanDefaults(mocks, { memoryManager });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
poolRun?.run.mockRestore();
|
||||
const { resetFacadeLoaderStateForTest } = await import("../plugin-sdk/facade-loader.js");
|
||||
const { waitForPluginCacheRetirement } = await import("../plugins/plugin-cache.js");
|
||||
resetFacadeLoaderStateForTest();
|
||||
await waitForPluginCacheRetirement();
|
||||
});
|
||||
|
||||
it.each(["populated", "empty", "unrelated", "missing", "unavailable"] as const)(
|
||||
"keeps offline JSON-all %s memory presence off the caller's SQLite thread",
|
||||
async (kind) => {
|
||||
const stateDir = tempDirs.make("openclaw-status-memory-presence-");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
const { resolveDefaultMemoryDatabasePath } = await import("./status.scan-memory.js");
|
||||
const databasePath = resolveDefaultMemoryDatabasePath("main");
|
||||
if (kind !== "missing") {
|
||||
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||
const database = new DatabaseSync(databasePath);
|
||||
try {
|
||||
database.exec(
|
||||
kind === "unrelated"
|
||||
? "CREATE TABLE unrelated_feature (id INTEGER PRIMARY KEY) STRICT"
|
||||
: buildMemoryIndexStrictSchema({
|
||||
embeddingCacheTable: "memory_embedding_cache",
|
||||
includeEmbeddingCache: false,
|
||||
}),
|
||||
);
|
||||
if (kind === "populated" || kind === "unavailable") {
|
||||
database.exec(
|
||||
"INSERT INTO memory_index_sources (path, source, hash, mtime, size) VALUES ('MEMORY.md', 'memory', 'synthetic', 1, 10)",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
const before = kind === "missing" ? undefined : fs.readFileSync(databasePath);
|
||||
mocks.resolveMemorySearchConfig.mockReturnValue({ store: { databasePath } });
|
||||
const observer = (poolRun ??= await observeWorkerPools());
|
||||
if (kind === "unavailable") {
|
||||
// Exercise dispatch rejection without creating a failed native worker.
|
||||
observer.rejectNextInspection(new Error("Synthetic inspection unavailable"));
|
||||
}
|
||||
const nativeCalls = [
|
||||
vi.spyOn(sqliteOwner, "openNodeSqliteDatabase"),
|
||||
vi.spyOn(DatabaseSync.prototype, "prepare"),
|
||||
vi.spyOn(DatabaseSync.prototype, "exec"),
|
||||
vi.spyOn(DatabaseSync.prototype, "close"),
|
||||
vi.spyOn(StatementSync.prototype, "all"),
|
||||
vi.spyOn(StatementSync.prototype, "get"),
|
||||
vi.spyOn(StatementSync.prototype, "run"),
|
||||
];
|
||||
try {
|
||||
const samples = [];
|
||||
for (let attempt = 0; attempt < (kind === "populated" ? 4 : 1); attempt++) {
|
||||
writeJson.mockClear();
|
||||
mocks.getMemorySearchManager.mockClear();
|
||||
observer.run.mockClear();
|
||||
for (const call of nativeCalls) {
|
||||
call.mockClear();
|
||||
}
|
||||
const started = performance.now();
|
||||
await statusJsonCommand({ all: true }, runtime);
|
||||
const durationMs = performance.now() - started;
|
||||
if (kind === "missing") {
|
||||
expect(observer.run).not.toHaveBeenCalled();
|
||||
} else {
|
||||
expect(observer.run).toHaveBeenCalled();
|
||||
expect(observer.pools.size).toBeGreaterThan(0);
|
||||
}
|
||||
expect(writeJson).toHaveBeenCalledOnce();
|
||||
expect(writeJson).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gateway: expect.objectContaining({ reachable: false }),
|
||||
memory:
|
||||
kind === "populated" ? { agentId: "main", files: 1, chunks: 0, dirty: false } : null,
|
||||
}),
|
||||
2,
|
||||
);
|
||||
expect(mocks.getMemorySearchManager).toHaveBeenCalledTimes(kind === "populated" ? 1 : 0);
|
||||
const parentSqlCalls = nativeCalls.map((call) => call.mock.calls.length);
|
||||
expect(parentSqlCalls).toEqual([0, 0, 0, 0, 0, 0, 0]);
|
||||
samples.push({ attempt, durationMs, parentSqlCalls });
|
||||
}
|
||||
console.info(
|
||||
JSON.stringify({
|
||||
fixture: kind,
|
||||
timingScope: "handler after same-loader SDK observer setup",
|
||||
samples,
|
||||
json: writeJson.mock.calls[0]?.[0],
|
||||
}),
|
||||
);
|
||||
if (before) {
|
||||
expect(fs.readFileSync(databasePath)).toEqual(before);
|
||||
} else {
|
||||
expect(fs.existsSync(databasePath)).toBe(false);
|
||||
}
|
||||
} finally {
|
||||
for (const call of nativeCalls) {
|
||||
call.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// This file owns the cross-command contracts reused by normal, JSON, and status-all scans.
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
|
|
@ -14,7 +13,6 @@ import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
|||
import { isLoopbackGatewayUrl } from "../gateway/net.js";
|
||||
import { resolveGatewayProbeTarget } from "../gateway/probe-target.js";
|
||||
import type { GatewayProbeResult, probeGateway as probeGatewayFn } from "../gateway/probe.js";
|
||||
import { openNodeSqliteDatabase } from "../infra/node-sqlite.js";
|
||||
import type { MemoryProviderStatus } from "../memory-host-sdk/engine-storage.js";
|
||||
import { defaultSlotIdForKey } from "../plugins/slots.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
|
|
@ -28,10 +26,13 @@ import { isProbeReachable } from "./gateway-status/helpers.js";
|
|||
const gatewayProbeModuleLoader = createLazyImportLoader(() => import("./status.gateway-probe.js"));
|
||||
const probeGatewayModuleLoader = createLazyImportLoader(() => import("../gateway/probe.js"));
|
||||
const gatewayCallModuleLoader = createLazyImportLoader(() => import("../gateway/call.js"));
|
||||
const memoryEngineStorageModuleLoader = createLazyImportLoader(
|
||||
() => import("../memory-host-sdk/engine-storage.js"),
|
||||
);
|
||||
const MEMORY_INDEX_META_KEY = "memory_index_meta_v1";
|
||||
const memoryPresenceModuleLoader = createLazyImportLoader(async () => {
|
||||
const { loadBundledPluginPublicArtifactModuleSync } =
|
||||
await import("../plugins/public-surface-loader.js");
|
||||
return loadBundledPluginPublicArtifactModuleSync<{
|
||||
inspectMemoryIndexPresence: (databasePath: string) => Promise<boolean>;
|
||||
}>({ dirName: "memory-core", artifactBasename: "status-api.js" });
|
||||
});
|
||||
|
||||
export function resolveStatusGatewayProbeTimeoutMs(opts: {
|
||||
timeoutMs?: number;
|
||||
|
|
@ -56,65 +57,8 @@ async function hasBuiltInMemoryState(databasePath: string): Promise<boolean> {
|
|||
if (!existsSync(databasePath)) {
|
||||
return false;
|
||||
}
|
||||
const { MEMORY_INDEX_CHUNKS_TABLE, MEMORY_INDEX_META_TABLE, MEMORY_INDEX_SOURCES_TABLE } =
|
||||
await memoryEngineStorageModuleLoader.load();
|
||||
let db: DatabaseSync | undefined;
|
||||
try {
|
||||
db = openNodeSqliteDatabase(databasePath, { readOnly: true });
|
||||
const builtInMemoryTableSets = [
|
||||
{
|
||||
meta: MEMORY_INDEX_META_TABLE,
|
||||
sources: MEMORY_INDEX_SOURCES_TABLE,
|
||||
chunks: MEMORY_INDEX_CHUNKS_TABLE,
|
||||
},
|
||||
{
|
||||
meta: "meta",
|
||||
sources: "files",
|
||||
chunks: "chunks",
|
||||
},
|
||||
] as const;
|
||||
const builtInMemoryTables = builtInMemoryTableSets.flatMap(({ meta, sources, chunks }) => [
|
||||
meta,
|
||||
sources,
|
||||
chunks,
|
||||
]);
|
||||
const tableNames = new Set(
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${builtInMemoryTables.map(() => "?").join(", ")})`,
|
||||
)
|
||||
.all(...builtInMemoryTables) as Array<{ name?: unknown }>
|
||||
)
|
||||
.map((row) => row.name)
|
||||
.filter((name): name is string => typeof name === "string"),
|
||||
);
|
||||
for (const tables of builtInMemoryTableSets) {
|
||||
if (
|
||||
tableNames.has(tables.meta) &&
|
||||
db
|
||||
.prepare(`SELECT 1 AS ok FROM ${tables.meta} WHERE key = ? LIMIT 1`)
|
||||
.get(MEMORY_INDEX_META_KEY)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
for (const tableName of [tables.sources, tables.chunks]) {
|
||||
if (
|
||||
tableNames.has(tableName) &&
|
||||
db.prepare(`SELECT 1 AS ok FROM ${tableName} LIMIT 1`).get()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
db?.close();
|
||||
} catch {}
|
||||
}
|
||||
const { inspectMemoryIndexPresence } = await memoryPresenceModuleLoader.load();
|
||||
return await inspectMemoryIndexPresence(databasePath);
|
||||
}
|
||||
|
||||
export type MemoryStatusSnapshot = MemoryProviderStatus & {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,2 @@
|
|||
/**
|
||||
* Core-facing facade for memory backend storage config resolution. Keep this
|
||||
* path stable while the shared SDK package owns provider status semantics.
|
||||
*/
|
||||
export {
|
||||
MEMORY_INDEX_CHUNKS_TABLE,
|
||||
MEMORY_INDEX_META_TABLE,
|
||||
MEMORY_INDEX_SOURCES_TABLE,
|
||||
resolveMemoryBackendConfig,
|
||||
type MemoryProviderStatus,
|
||||
} from "../../packages/memory-host-sdk/src/engine-storage.js";
|
||||
/** Core-facing memory status types; runtime storage belongs to the memory plugin. */
|
||||
export type { MemoryProviderStatus } from "../../packages/memory-host-sdk/src/engine-storage.js";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue