mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(memory): surface llama.cpp diagnostics
This commit is contained in:
@@ -14,6 +14,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
const memoryHostEmbeddingMocks = vi.hoisted(() => ({
|
||||
createLocalEmbeddingProvider: vi.fn(),
|
||||
}));
|
||||
const LOCAL_EMBEDDING_RUNTIME_FACTS = Symbol.for("openclaw.localEmbeddingRuntimeFacts");
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/memory-core-host-engine-embeddings", () => ({
|
||||
createLocalEmbeddingProvider: memoryHostEmbeddingMocks.createLocalEmbeddingProvider,
|
||||
@@ -59,7 +60,13 @@ describe("llama.cpp provider plugin", () => {
|
||||
|
||||
it("adapts the worker-backed local embedding provider", async () => {
|
||||
const close = vi.fn();
|
||||
memoryHostEmbeddingMocks.createLocalEmbeddingProvider.mockResolvedValue({
|
||||
const getRuntimeFacts = vi.fn(() => ({
|
||||
engine: "llama.cpp" as const,
|
||||
state: "ready" as const,
|
||||
backend: "metal" as const,
|
||||
buildType: "prebuilt" as const,
|
||||
}));
|
||||
const workerProvider = {
|
||||
id: "local",
|
||||
model: DEFAULT_LLAMA_CPP_EMBEDDING_MODEL,
|
||||
maxInputTokens: 2048,
|
||||
@@ -67,7 +74,11 @@ describe("llama.cpp provider plugin", () => {
|
||||
embedBatchInputs: vi.fn(async () => [[0.3, 0.4]]),
|
||||
embedBatch: vi.fn(async () => [[1, 0]]),
|
||||
close,
|
||||
};
|
||||
Object.defineProperty(workerProvider, LOCAL_EMBEDDING_RUNTIME_FACTS, {
|
||||
value: getRuntimeFacts,
|
||||
});
|
||||
memoryHostEmbeddingMocks.createLocalEmbeddingProvider.mockResolvedValue(workerProvider);
|
||||
const abortController = new AbortController();
|
||||
|
||||
const result = await llamaCppEmbeddingProviderAdapter.create({
|
||||
@@ -89,6 +100,20 @@ describe("llama.cpp provider plugin", () => {
|
||||
|
||||
expect(provider.model).toBe(DEFAULT_LLAMA_CPP_EMBEDDING_MODEL);
|
||||
expect(provider.maxInputTokens).toBe(2048);
|
||||
const adaptedGetRuntimeFacts = Reflect.get(provider, LOCAL_EMBEDDING_RUNTIME_FACTS);
|
||||
if (typeof adaptedGetRuntimeFacts !== "function") {
|
||||
throw new Error("expected llama.cpp runtime facts carrier");
|
||||
}
|
||||
expect(adaptedGetRuntimeFacts()).toEqual({
|
||||
engine: "llama.cpp",
|
||||
state: "ready",
|
||||
backend: "metal",
|
||||
buildType: "prebuilt",
|
||||
});
|
||||
expect(result.runtime?.cacheKeyData).toEqual({
|
||||
provider: "local",
|
||||
model: DEFAULT_LLAMA_CPP_EMBEDDING_MODEL,
|
||||
});
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
expect(memoryHostEmbeddingMocks.createLocalEmbeddingProvider).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -104,9 +129,9 @@ describe("llama.cpp provider plugin", () => {
|
||||
nodeLlamaCppImportUrl: expect.stringContaining("node-llama-cpp"),
|
||||
},
|
||||
);
|
||||
const workerProvider =
|
||||
const createdWorkerProvider =
|
||||
await memoryHostEmbeddingMocks.createLocalEmbeddingProvider.mock.results[0].value;
|
||||
expect(workerProvider.embedBatchInputs).toHaveBeenCalledWith([{ text: "doc" }], {
|
||||
expect(createdWorkerProvider.embedBatchInputs).toHaveBeenCalledWith([{ text: "doc" }], {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ type LlamaCppEmbeddingProviderRuntimeOptions = {
|
||||
};
|
||||
|
||||
const LLAMA_CPP_EMBEDDING_PROVIDER_ID = "local";
|
||||
const LOCAL_EMBEDDING_RUNTIME_FACTS = Symbol.for("openclaw.localEmbeddingRuntimeFacts");
|
||||
export const DEFAULT_LLAMA_CPP_EMBEDDING_MODEL =
|
||||
"hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf";
|
||||
const DEFAULT_LLAMA_CPP_EMBEDDING_MODEL_CACHE_FILE_NAME =
|
||||
@@ -159,8 +160,18 @@ function resolveNodeLlamaCppImportUrl(): string {
|
||||
return pathToFileURL(requireFromPlugin.resolve("node-llama-cpp")).href;
|
||||
}
|
||||
|
||||
function copyLocalRuntimeFacts(source: object, target: object): void {
|
||||
const getRuntimeFacts = Reflect.get(source, LOCAL_EMBEDDING_RUNTIME_FACTS);
|
||||
if (typeof getRuntimeFacts === "function") {
|
||||
Object.defineProperty(target, LOCAL_EMBEDDING_RUNTIME_FACTS, {
|
||||
enumerable: false,
|
||||
value: getRuntimeFacts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function adaptMemoryEmbeddingProvider(provider: MemoryEmbeddingProvider): EmbeddingProvider {
|
||||
return {
|
||||
const adapted: EmbeddingProvider = {
|
||||
id: LLAMA_CPP_EMBEDDING_PROVIDER_ID,
|
||||
model: provider.model,
|
||||
maxInputTokens: provider.maxInputTokens,
|
||||
@@ -180,6 +191,8 @@ function adaptMemoryEmbeddingProvider(provider: MemoryEmbeddingProvider): Embedd
|
||||
},
|
||||
close: provider.close,
|
||||
};
|
||||
copyLocalRuntimeFacts(provider, adapted);
|
||||
return adapted;
|
||||
}
|
||||
|
||||
export async function createLlamaCppMemoryEmbeddingProvider(
|
||||
@@ -198,6 +211,9 @@ export async function createLlamaCppMemoryEmbeddingProvider(
|
||||
);
|
||||
const identifiedProvider =
|
||||
identity.model === provider.model ? provider : { ...provider, model: identity.model };
|
||||
if (identifiedProvider !== provider) {
|
||||
copyLocalRuntimeFacts(provider, identifiedProvider);
|
||||
}
|
||||
return {
|
||||
provider: identifiedProvider,
|
||||
runtime: createLlamaCppEmbeddingProviderRuntime(identity),
|
||||
|
||||
@@ -74,6 +74,36 @@ type MemoryManagerPurpose = Parameters<typeof getMemorySearchManager>[0]["purpos
|
||||
|
||||
type MemorySourceName = "memory" | "sessions";
|
||||
|
||||
type LlamaCppRuntimeStatus = {
|
||||
state?: string;
|
||||
backend?: string;
|
||||
buildType?: string;
|
||||
deviceNames?: string[];
|
||||
memory?: {
|
||||
totalBytes: number;
|
||||
usedBytes: number;
|
||||
freeBytes: number;
|
||||
unifiedBytes: number;
|
||||
observedAtMs: number;
|
||||
};
|
||||
offload?: {
|
||||
supported: boolean;
|
||||
offloadedLayers?: number;
|
||||
totalLayers?: number;
|
||||
};
|
||||
context?: {
|
||||
requestedSize: number | "auto";
|
||||
};
|
||||
loadError?: string;
|
||||
};
|
||||
|
||||
function readLlamaCppRuntimeStatus(
|
||||
status: ReturnType<MemoryManager["status"]>,
|
||||
): LlamaCppRuntimeStatus | null {
|
||||
const runtime = asRecord(asRecord(status.custom)?.llamaCppRuntime);
|
||||
return runtime?.engine === "llama.cpp" ? (runtime as LlamaCppRuntimeStatus) : null;
|
||||
}
|
||||
|
||||
function formatMemoryIndexIdentityWarning(
|
||||
status: ReturnType<MemoryManager["status"]>,
|
||||
agentId: string,
|
||||
@@ -96,6 +126,20 @@ function formatMemoryIndexIdentityWarning(
|
||||
};
|
||||
}
|
||||
|
||||
function formatRuntimeBytes(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
let value = bytes / 1024;
|
||||
let unit = units[0];
|
||||
for (let index = 1; index < units.length && value >= 1024; index += 1) {
|
||||
value /= 1024;
|
||||
unit = units[index];
|
||||
}
|
||||
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
|
||||
}
|
||||
|
||||
type SourceScan = {
|
||||
source: MemorySourceName;
|
||||
totalFiles: number | null;
|
||||
@@ -910,6 +954,43 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
||||
lines.push(`${label("Embeddings error")} ${warn(embeddingProbe.error)}`);
|
||||
}
|
||||
}
|
||||
const llamaCppRuntime = opts.deep ? readLlamaCppRuntimeStatus(status) : null;
|
||||
if (llamaCppRuntime) {
|
||||
const runtime = llamaCppRuntime;
|
||||
const backend = runtime.backend ?? "unknown";
|
||||
const build = runtime.buildType ? ` (${runtime.buildType})` : "";
|
||||
lines.push(`${label("llama.cpp")} ${info(backend)}${muted(build)}`);
|
||||
if (runtime.deviceNames?.length) {
|
||||
lines.push(`${label("Devices")} ${info(runtime.deviceNames.join(", "))}`);
|
||||
}
|
||||
if (runtime.memory) {
|
||||
const unified =
|
||||
runtime.memory.unifiedBytes > 0
|
||||
? ` · ${formatRuntimeBytes(runtime.memory.unifiedBytes)} unified`
|
||||
: "";
|
||||
lines.push(
|
||||
`${label("VRAM snapshot")} ${info(`${formatRuntimeBytes(runtime.memory.usedBytes)} used · ${formatRuntimeBytes(runtime.memory.freeBytes)} free · ${formatRuntimeBytes(runtime.memory.totalBytes)} total${unified}`)} ${muted(`(${new Date(runtime.memory.observedAtMs).toISOString()})`)}`,
|
||||
);
|
||||
}
|
||||
if (runtime.offload) {
|
||||
const layers =
|
||||
typeof runtime.offload.offloadedLayers === "number" &&
|
||||
typeof runtime.offload.totalLayers === "number"
|
||||
? `${runtime.offload.offloadedLayers}/${runtime.offload.totalLayers} layers`
|
||||
: runtime.offload.supported
|
||||
? "supported"
|
||||
: "unsupported";
|
||||
lines.push(`${label("GPU offload")} ${info(layers)}`);
|
||||
}
|
||||
if (runtime.context) {
|
||||
lines.push(
|
||||
`${label("Requested context")} ${info(`${runtime.context.requestedSize} tokens`)}`,
|
||||
);
|
||||
}
|
||||
if (runtime.loadError) {
|
||||
lines.push(`${label("llama.cpp error")} ${warn(runtime.loadError)}`);
|
||||
}
|
||||
}
|
||||
const identityWarning = formatMemoryIndexIdentityWarning(status, agentId);
|
||||
if (identityWarning) {
|
||||
lines.push(`${label("Index identity")} ${warn(identityWarning.reason)}`);
|
||||
|
||||
@@ -472,6 +472,14 @@ describe("memory cli", () => {
|
||||
provider: "auto",
|
||||
requestedProvider: "auto",
|
||||
vector: { enabled: true },
|
||||
custom: {
|
||||
llamaCppRuntime: {
|
||||
engine: "llama.cpp",
|
||||
state: "ready",
|
||||
backend: "metal",
|
||||
buildType: "prebuilt",
|
||||
},
|
||||
},
|
||||
}),
|
||||
close,
|
||||
});
|
||||
@@ -483,6 +491,7 @@ describe("memory cli", () => {
|
||||
expect(probeEmbeddingAvailability).not.toHaveBeenCalled();
|
||||
expectLogged(log, "Provider: auto");
|
||||
expectLogged(log, "Vector store: unknown");
|
||||
expectNotLogged(log, "llama.cpp:");
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -589,7 +598,35 @@ describe("memory cli", () => {
|
||||
probeVectorStoreAvailability,
|
||||
probeVectorAvailability,
|
||||
probeEmbeddingAvailability,
|
||||
status: () => makeMemoryStatus({ files: 1, chunks: 1 }),
|
||||
status: () =>
|
||||
makeMemoryStatus({
|
||||
files: 1,
|
||||
chunks: 1,
|
||||
custom: {
|
||||
llamaCppRuntime: {
|
||||
engine: "llama.cpp",
|
||||
state: "ready",
|
||||
backend: "metal",
|
||||
buildType: "prebuilt",
|
||||
deviceNames: ["Apple M4 Max"],
|
||||
memory: {
|
||||
totalBytes: 64 * 1024 ** 3,
|
||||
usedBytes: 8 * 1024 ** 3,
|
||||
freeBytes: 56 * 1024 ** 3,
|
||||
unifiedBytes: 64 * 1024 ** 3,
|
||||
observedAtMs: Date.parse("2026-07-10T12:00:00.000Z"),
|
||||
},
|
||||
offload: {
|
||||
supported: true,
|
||||
offloadedLayers: 20,
|
||||
totalLayers: 24,
|
||||
},
|
||||
context: {
|
||||
requestedSize: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
close,
|
||||
});
|
||||
|
||||
@@ -600,6 +637,14 @@ describe("memory cli", () => {
|
||||
expect(probeVectorAvailability).toHaveBeenCalled();
|
||||
expect(probeEmbeddingAvailability).toHaveBeenCalled();
|
||||
expectLogged(log, "Embeddings: ready");
|
||||
expectLogged(log, "llama.cpp: metal (prebuilt)");
|
||||
expectLogged(log, "Devices: Apple M4 Max");
|
||||
expectLogged(
|
||||
log,
|
||||
"VRAM snapshot: 8.0 GB used · 56 GB free · 64 GB total · 64 GB unified (2026-07-10T12:00:00.000Z)",
|
||||
);
|
||||
expectLogged(log, "GPU offload: 20/24 layers");
|
||||
expectLogged(log, "Requested context: 4096 tokens");
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ type CreateEmbeddingProviderOptions = MemoryEmbeddingProviderCreateOptions & {
|
||||
|
||||
const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai";
|
||||
const LOCAL_LLAMA_CPP_PROVIDER_ID = "local";
|
||||
const LOCAL_EMBEDDING_RUNTIME_FACTS = Symbol.for("openclaw.localEmbeddingRuntimeFacts");
|
||||
|
||||
function createMissingLlamaCppProviderError(): Error {
|
||||
return new Error(
|
||||
@@ -52,7 +53,7 @@ function createMissingLlamaCppProviderError(): Error {
|
||||
function adaptGenericEmbeddingProvider(
|
||||
provider: GenericEmbeddingProvider,
|
||||
): MemoryEmbeddingProvider {
|
||||
return {
|
||||
const adapted: MemoryEmbeddingProvider = {
|
||||
id: provider.id,
|
||||
model: provider.model,
|
||||
...(typeof provider.maxInputTokens === "number"
|
||||
@@ -75,6 +76,14 @@ function adaptGenericEmbeddingProvider(
|
||||
}),
|
||||
...(provider.close ? { close: provider.close } : {}),
|
||||
};
|
||||
const getRuntimeFacts = Reflect.get(provider, LOCAL_EMBEDDING_RUNTIME_FACTS);
|
||||
if (typeof getRuntimeFacts === "function") {
|
||||
Object.defineProperty(adapted, LOCAL_EMBEDDING_RUNTIME_FACTS, {
|
||||
enumerable: false,
|
||||
value: getRuntimeFacts,
|
||||
});
|
||||
}
|
||||
return adapted;
|
||||
}
|
||||
|
||||
function adaptGenericRuntime(
|
||||
|
||||
@@ -2158,6 +2158,62 @@ describe("memory index", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes already-created local runtime facts without probing embeddings", async () => {
|
||||
const cfg = createCfg({});
|
||||
const { getRequiredMemoryIndexManager } = await import("./test-manager-helpers.js");
|
||||
const manager = await getRequiredMemoryIndexManager({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
purpose: "status",
|
||||
});
|
||||
try {
|
||||
const getRuntimeFacts = vi.fn(() => ({
|
||||
engine: "llama.cpp" as const,
|
||||
state: "ready" as const,
|
||||
backend: "cuda" as const,
|
||||
buildType: "prebuilt" as const,
|
||||
deviceNames: ["NVIDIA Test GPU"],
|
||||
offload: {
|
||||
supported: true,
|
||||
offloadedLayers: 24,
|
||||
totalLayers: 24,
|
||||
},
|
||||
context: {
|
||||
requestedSize: 4096,
|
||||
},
|
||||
}));
|
||||
const provider = {
|
||||
id: "local",
|
||||
model: "test-model.gguf",
|
||||
embedQuery: vi.fn(async () => [1, 0, 0, 0]),
|
||||
embedBatch: vi.fn(async (texts: string[]) => texts.map(() => [1, 0, 0, 0])),
|
||||
};
|
||||
Object.defineProperty(provider, Symbol.for("openclaw.localEmbeddingRuntimeFacts"), {
|
||||
value: getRuntimeFacts,
|
||||
});
|
||||
const fields = manager as unknown as {
|
||||
provider: typeof provider | null;
|
||||
};
|
||||
fields.provider = provider;
|
||||
|
||||
expect(manager.status().custom?.llamaCppRuntime).toMatchObject({
|
||||
state: "ready",
|
||||
backend: "cuda",
|
||||
deviceNames: ["NVIDIA Test GPU"],
|
||||
offload: {
|
||||
offloadedLayers: 24,
|
||||
totalLayers: 24,
|
||||
},
|
||||
context: {
|
||||
requestedSize: 4096,
|
||||
},
|
||||
});
|
||||
expect(getRuntimeFacts).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
await manager.close?.();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps metadata after unchanged in-place force reindex", async () => {
|
||||
const cfg = createCfg({});
|
||||
const manager = await getFreshManager(cfg);
|
||||
|
||||
@@ -69,6 +69,17 @@ import {
|
||||
type MemoryReadonlyRecoveryState,
|
||||
} from "./manager-sync-control.js";
|
||||
import { applyTemporalDecayToHybridResults } from "./temporal-decay.js";
|
||||
|
||||
const LOCAL_EMBEDDING_RUNTIME_FACTS = Symbol.for("openclaw.localEmbeddingRuntimeFacts");
|
||||
|
||||
function getLocalEmbeddingRuntimeFacts(provider: EmbeddingProvider | null): unknown {
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
const getRuntimeFacts = Reflect.get(provider, LOCAL_EMBEDDING_RUNTIME_FACTS);
|
||||
return typeof getRuntimeFacts === "function" ? getRuntimeFacts() : undefined;
|
||||
}
|
||||
|
||||
const SNIPPET_MAX_CHARS = 700;
|
||||
const VECTOR_TABLE = MEMORY_INDEX_VECTOR_TABLE;
|
||||
const FTS_TABLE = MEMORY_INDEX_FTS_TABLE;
|
||||
@@ -1206,6 +1217,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
||||
lastProvider: this.batchFailureLastProvider,
|
||||
},
|
||||
custom: {
|
||||
llamaCppRuntime: getLocalEmbeddingRuntimeFacts(this.provider),
|
||||
searchMode: providerInfo.searchMode,
|
||||
providerState: this.providerLifecycle,
|
||||
providerUnavailableReason: this.providerUnavailableReason,
|
||||
|
||||
@@ -233,6 +233,28 @@ describe("probeGatewayMemoryStatus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("carries last-known llama.cpp facts from the gateway", async () => {
|
||||
callGateway.mockResolvedValue({
|
||||
embedding: { ok: true },
|
||||
embeddingRuntime: {
|
||||
engine: "llama.cpp",
|
||||
state: "ready",
|
||||
backend: "metal",
|
||||
buildType: "prebuilt",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(probeGatewayMemoryStatus({ cfg })).resolves.toMatchObject({
|
||||
checked: true,
|
||||
ready: true,
|
||||
runtimeFacts: {
|
||||
state: "ready",
|
||||
backend: "metal",
|
||||
buildType: "prebuilt",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("treats outer gateway timeouts as inconclusive (skipped: false)", async () => {
|
||||
// A transport timeout must NOT be treated as a skipped probe. It is a real
|
||||
// diagnostic signal and the renderer should warn for key-optional providers.
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
isGatewayCredentialsRequiredError,
|
||||
} from "../gateway/call.js";
|
||||
import { isGatewaySecretRefUnavailableError } from "../gateway/credentials.js";
|
||||
import type { DoctorMemoryStatusPayload } from "../gateway/server-methods/doctor.js";
|
||||
import type {
|
||||
DoctorMemoryEmbeddingRuntimePayload,
|
||||
DoctorMemoryStatusPayload,
|
||||
} from "../gateway/server-methods/doctor.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
@@ -26,6 +29,7 @@ type GatewayMemoryProbe = {
|
||||
checked: boolean;
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
runtimeFacts?: DoctorMemoryEmbeddingRuntimePayload;
|
||||
/**
|
||||
* True when the probe was intentionally skipped by the gateway (probe: false
|
||||
* path). Distinct from checked: false caused by a network timeout or
|
||||
@@ -170,6 +174,7 @@ export async function probeGatewayMemoryStatus(params: {
|
||||
checked: gatewayChecked,
|
||||
ready: payload.embedding.ok,
|
||||
error: payload.embedding.error,
|
||||
...(payload.embeddingRuntime ? { runtimeFacts: payload.embeddingRuntime } : {}),
|
||||
skipped: !gatewayChecked,
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
@@ -266,6 +266,46 @@ describe("noteMemorySearchHealth", () => {
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports last-known llama.cpp runtime facts from the gateway", async () => {
|
||||
resolveMemorySearchConfig.mockReturnValue({
|
||||
provider: "local",
|
||||
local: {},
|
||||
remote: {},
|
||||
});
|
||||
|
||||
await noteMemorySearchHealth(cfg, {
|
||||
gatewayMemoryProbe: {
|
||||
checked: true,
|
||||
ready: true,
|
||||
runtimeFacts: {
|
||||
engine: "llama.cpp",
|
||||
state: "ready",
|
||||
backend: "cuda",
|
||||
buildType: "prebuilt",
|
||||
deviceNames: ["NVIDIA Test GPU"],
|
||||
offload: {
|
||||
supported: true,
|
||||
offloadedLayers: 24,
|
||||
totalLayers: 24,
|
||||
},
|
||||
context: {
|
||||
requestedSize: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
[
|
||||
"llama.cpp runtime: cuda, prebuilt",
|
||||
"Devices: NVIDIA Test GPU",
|
||||
"GPU offload: 24/24 layers",
|
||||
"Requested context: 4096 tokens",
|
||||
].join("\n"),
|
||||
"Memory search",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn when local provider readiness probe was intentionally skipped", async () => {
|
||||
resolveMemorySearchConfig.mockReturnValue({
|
||||
provider: "local",
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../agents/model-auth.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { DoctorMemoryEmbeddingRuntimePayload } from "../gateway/server-methods/doctor.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import {
|
||||
checkQmdBinaryAvailability,
|
||||
@@ -63,6 +64,19 @@ type MemoryEmbeddingProviderDoctorMetadata = {
|
||||
autoSelectPriority?: number;
|
||||
};
|
||||
|
||||
function formatLocalRuntimeDoctorNote(facts: DoctorMemoryEmbeddingRuntimePayload): string {
|
||||
const backend = facts.backend ?? "unknown";
|
||||
const build = facts.buildType ? `, ${facts.buildType}` : "";
|
||||
const devices = facts.deviceNames?.length ? `\nDevices: ${facts.deviceNames.join(", ")}` : "";
|
||||
const offload =
|
||||
typeof facts.offload?.offloadedLayers === "number" &&
|
||||
typeof facts.offload.totalLayers === "number"
|
||||
? `\nGPU offload: ${facts.offload.offloadedLayers}/${facts.offload.totalLayers} layers`
|
||||
: "";
|
||||
const context = facts.context ? `\nRequested context: ${facts.context.requestedSize} tokens` : "";
|
||||
return `llama.cpp runtime: ${backend}${build}${devices}${offload}${context}`;
|
||||
}
|
||||
|
||||
const BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA: MemoryEmbeddingProviderDoctorMetadata[] = [
|
||||
{
|
||||
providerId: "github-copilot",
|
||||
@@ -419,6 +433,7 @@ export async function noteMemorySearchHealth(
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
skipped?: boolean;
|
||||
runtimeFacts?: DoctorMemoryEmbeddingRuntimePayload;
|
||||
};
|
||||
noteFn?: typeof note;
|
||||
includeWorkspaceMemoryHealth?: boolean;
|
||||
@@ -514,6 +529,9 @@ export async function noteMemorySearchHealth(
|
||||
|
||||
if (provider === "local") {
|
||||
const suggestedRemoteProvider = resolveSuggestedRemoteMemoryProvider();
|
||||
if (opts?.gatewayMemoryProbe?.runtimeFacts?.state === "ready") {
|
||||
noteFn(formatLocalRuntimeDoctorNote(opts.gatewayMemoryProbe.runtimeFacts), "Memory search");
|
||||
}
|
||||
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,6 +322,61 @@ describe("doctor.memory.status", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns llama.cpp runtime facts created by the deep embedding probe", async () => {
|
||||
const close = vi.fn().mockResolvedValue(undefined);
|
||||
let probed = false;
|
||||
getMemorySearchManager.mockResolvedValue({
|
||||
manager: {
|
||||
status: () => ({
|
||||
provider: "local",
|
||||
...(probed
|
||||
? {
|
||||
custom: {
|
||||
llamaCppRuntime: {
|
||||
engine: "llama.cpp",
|
||||
state: "ready",
|
||||
backend: "cuda",
|
||||
buildType: "prebuilt",
|
||||
deviceNames: ["NVIDIA Test GPU"],
|
||||
offload: {
|
||||
supported: true,
|
||||
offloadedLayers: 24,
|
||||
totalLayers: 24,
|
||||
},
|
||||
context: {
|
||||
requestedSize: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
probeEmbeddingAvailability: vi.fn(async () => {
|
||||
probed = true;
|
||||
return { ok: true };
|
||||
}),
|
||||
close,
|
||||
},
|
||||
});
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemoryStatus(respond, { params: { probe: true } });
|
||||
|
||||
expect(respondPayload(respond).embeddingRuntime).toMatchObject({
|
||||
state: "ready",
|
||||
backend: "cuda",
|
||||
deviceNames: ["NVIDIA Test GPU"],
|
||||
offload: {
|
||||
offloadedLayers: 24,
|
||||
totalLayers: 24,
|
||||
},
|
||||
context: {
|
||||
requestedSize: 4096,
|
||||
},
|
||||
});
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not live-probe embedding readiness by default", async () => {
|
||||
const close = vi.fn().mockResolvedValue(undefined);
|
||||
const probeEmbeddingAvailability = vi.fn().mockResolvedValue({ ok: true });
|
||||
|
||||
@@ -124,9 +124,34 @@ export type DoctorMemoryStatusPayload = {
|
||||
checkedAtMs?: number;
|
||||
cacheExpiresAtMs?: number;
|
||||
};
|
||||
embeddingRuntime?: DoctorMemoryEmbeddingRuntimePayload;
|
||||
dreaming?: DoctorMemoryDreamingPayload;
|
||||
};
|
||||
|
||||
export type DoctorMemoryEmbeddingRuntimePayload = {
|
||||
engine: "llama.cpp";
|
||||
state: "ready" | "failed";
|
||||
backend?: "metal" | "cuda" | "vulkan" | "cpu";
|
||||
buildType?: "localBuild" | "prebuilt";
|
||||
deviceNames?: string[];
|
||||
memory?: {
|
||||
totalBytes: number;
|
||||
usedBytes: number;
|
||||
freeBytes: number;
|
||||
unifiedBytes: number;
|
||||
observedAtMs: number;
|
||||
};
|
||||
offload?: {
|
||||
supported: boolean;
|
||||
offloadedLayers?: number;
|
||||
totalLayers?: number;
|
||||
};
|
||||
context?: {
|
||||
requestedSize: number | "auto";
|
||||
};
|
||||
loadError?: string;
|
||||
};
|
||||
|
||||
export type DoctorMemoryDreamDiaryPayload = {
|
||||
agentId: string;
|
||||
found: boolean;
|
||||
@@ -727,11 +752,14 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
|
||||
try {
|
||||
const status = manager.status();
|
||||
let status = manager.status();
|
||||
const shouldProbe = shouldProbeMemoryEmbeddings(params);
|
||||
let embedding = shouldProbe
|
||||
? await manager.probeEmbeddingAvailability()
|
||||
: (manager.getCachedEmbeddingAvailability?.() ?? SKIPPED_MEMORY_EMBEDDING_PROBE);
|
||||
if (shouldProbe) {
|
||||
status = manager.status();
|
||||
}
|
||||
if (!embedding.ok && !embedding.error) {
|
||||
embedding = { ok: false, error: "memory embeddings unavailable" };
|
||||
}
|
||||
@@ -774,6 +802,12 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
agentId,
|
||||
provider: status.provider,
|
||||
embedding,
|
||||
embeddingRuntime: (() => {
|
||||
const runtime = asOptionalRecord(asOptionalRecord(status.custom)?.llamaCppRuntime);
|
||||
return runtime?.engine === "llama.cpp"
|
||||
? (runtime as DoctorMemoryEmbeddingRuntimePayload)
|
||||
: undefined;
|
||||
})(),
|
||||
dreaming: {
|
||||
...dreamingConfig,
|
||||
...storeStats,
|
||||
|
||||
Reference in New Issue
Block a user